gitoria
All repositories: gitoria
20.3 KB
// The framework's STYLESHEET — the Style convention of COMPONENTS §4, as the// archived hl:web implemented it (archive/hlweb-v1/plugins-web/web_css.hl), re-read// 2026-09-12 after a version built from the doc alone got most of it wrong.//// STRUCTURE FROM THE SYNTAX, VALUES FROM THE INSTANCE. A hybrid's keys come back// alphabetical, so the ORDER of rules, declarations and media conditions is read// off the syntax (hlSyntax entries are authored order) while every VALUE is read// off the constructed instance at the same path — the language has already folded// the statics (`background = dark`) and merged `base + { … }` by then.//// WHAT A RULE NAME MEANS (the archive's `placesFor`, per file):// `@root` / `':root'` a CONTRIBUTION: collected from every file into ONE// `:root { }` at the top of the sheet, in order// `'@media …'` / `@media` a BLOCK of rules: collected at the END of the sheet,// (`@supports`, the same shape and place for the other two queries// `@container`) (ticket #66, proposal 5)// one `@media` per distinct query, first-seen order;// `@media { minWidth = 960 … }` composes the query from// conditions (number → px, string verbatim)// `'@keyframes name'` an ANIMATION: its stops (`'from'`, `to`, `'50%'`) with// their declarations, emitted where it stands (ticket #66, 4)// selector-shaped verbatim (`'.icon'`, `'#page-anchor'`, `'a:hover'`)// referenced `Style.x` the class the View wrote (tag-prefix strip): `.x`, or// `tag.stripped`// `#x` id the View writes `id = 'x'` on an element: `#x` (R3)// `#x` local otherwise matched by NAME to this file's elements `x` (and// a composed child's root for `#Child`): `tag.<file class>`// own-name a rule named like an element of this file's View (or// body/html): the TAG selector, no class// anything else DEAD: dropped and listed// In a STYLES FILE (root members, no View) a bare name is a tag, kebab-cased.// NESTED under a rule, a name of PSEUDOS is that rule's pseudo-class, not a// descendant: `a { hover { … } }` → `a:hover` (ticket #66, proposal 1).// A rule with no declarations and nested rules is a CONTAINER: not emitted.// Two rules emitting one selector with different bodies: refused.import HlwCssVar from './cssvar.hl'static NL = ""static SELECTOR_CHARS = ":-.#>[]()*+~,/ %@=^|!"static HEADER = "/* GENERATED by hl:web — ONE global stylesheet.Components author their Style rules per file; only the rules a View actuallyREFERENCES are emitted (dead rules are dropped and listed at boot). Class namesfollow the tag-prefix strip rule: rule `headerMain` referenced inside `header`emits class=\"main\" under the TAG-QUALIFIED selector `header.main`; under anyother tag it keeps `.headerMain`. */"Boolean minify = falseview = null // the View walk: kebab, className, localClass live theregen = 0 // the analysis generation the syntax handles belong to// per sheetparts = []seen = {}dead = []mediaOrder = []mediaParts = []rootDecls = []// ---- names ----------------------------------------------------------------------// ---- css variables (creator's ruling 2026-09-13) --------------------------------// `bg = var('#FFF')` declares a custom property as a VALUE; the walker names it after// its member, writes it into the sheet's one `:root`, and rewrites every use.static counter = { n = 0 }// `var('#FFF', dark = '#000')` carries its dark-scheme value too (ticket #66,// proposal 2): the sheet writes it into `:root` under `(prefers-color-scheme: dark)`static var = (value, dark = null) => {counter.n = counter.n + 1return new HlwCssVar(value = value, id = counter.n, dark = dark)}static DARK = '@media (prefers-color-scheme: dark)'// ---- units as functions (ticket #66, proposal 3) ---------------------------------// a number is emitted unitless as written; `rem(1.5)`, `px(12)`, `pct(50)` answer the// TEXT with its unit — '1.5rem', '12px', '50%' — so a value says what it measuresstatic rem = (n) => { return '' + n + 'rem' }static px = (n) => { return '' + n + 'px' }static pct = (n) => { return '' + n + '%' }vars = {} // id → { name value } for the sheet being builtvarNames = {} // name → trueisVar(v) { return v != null && hlTypeName(v) == 'Instance' }// the variables a file's root members declare, named after the membercollectVars(f) {for (r of fileRules(f)) {if (!r.isBlock && isVar(r.value)) {r.value.name = r.keyvars['' + r.value.id] = { name = r.key; value = r.value.value; }varNames[r.key] = true}}// a token the file INHERITS: its own instance's copy, under the parent's name (ticket #39)if (f.aliases != null) {for (a of f.aliases) {a.value.name = a.keyvars['' + a.value.id] = { name = a.key; value = a.value.value; }}}return null}// a value's text with every variable reference resolved: a variable itself →// var(--name); a composed string carries placeholders var(--hlv<id>) → var(--name)varText(v) {if (isVar(v)) {let d = vars['' + v.id]if (d == null) { hlError("a css variable is used before any styles file declared it as a root member — `name = var(…)` at the root of the styles file") }return 'var(--' + view.kebab(d.name) + ')'}let s = '' + vif (s.indexOf('var(--hlv') < 0) { return s }for (id of vars.keys()) { s = s.replaceAll('var(--hlv' + id + ')', 'var(--' + view.kebab(vars[id].name) + ')') }return s}// the pseudo-classes a nested entry may name, camelCase as written (kebab-cased on emit)static PSEUDOS = ['hover', 'focus', 'focusVisible', 'focusWithin', 'active', 'visited', 'target','disabled', 'enabled', 'checked', 'required', 'optional', 'valid', 'invalid', 'readOnly','placeholderShown', 'firstChild', 'lastChild', 'onlyChild', 'firstOfType', 'lastOfType', 'empty']isPseudo(name) { return PSEUDOS.includes(name) }// the selectors a nested entry composes under its enclosing rule's `sels`: a// pseudo-class sticks to each (`a:hover`), anything else is a descendant (`a span`)nestedSels(f, sels, key) {let out = []if (isPseudo(key)) {for (p of sels) { out.push(p + ':' + view.kebab(key)) }return out}let kidSels = selectorsFor(f, key)for (p of sels) { for (s of kidSels) { out.push(p + ' ' + s) } }return out}isKeyframes(name) { return name == '@keyframes' || name.startsWith('@keyframes ') }isRootBlock(name) { return name == '@root' || name == ':root' }// THE THREE QUERIES share one shape: `@media`, `@supports`, `@container` — bare with// conditions as scalars, or spelled out in the keystatic QUERIES = ['@media', '@supports', '@container']queryKind(name) {for (k of QUERIES) {if (name == k || name.startsWith(k + ' ') || name.startsWith(k + '(')) { return k }}return ''}isMediaBlock(name) { return queryKind(name) != '' }mediaLiteral(name) { return name.slice(queryKind(name).length, name.length).trim() }localName(name) { return name.startsWith('#') ? name.slice(1) : '' }selectorShaped(name) {if (name.length == 0) { return false }if ('0123456789'.indexOf(name.charAt(0)) >= 0) { return true }let i = 0while (i < name.length) {if (SELECTOR_CHARS.indexOf(name.charAt(i)) >= 0) { return true }i = i + 1}return false}// ---- one block's syntax, with its values --------------------------------------// the entries of a hybrid node: [{ key node value isBlock }] in authored order,// values read off `hybrid` (the instance's evaluated member at the same path)entriesOf(key, node, hybrid) {let out = []let n = node == 0 ? null : hlSyntax(gen, key, node)if (n == null || n.tag != 'object_expression') {// A MERGED RULE (`extR = baseR + { top = 'auto' }`, COMPONENTS §4: "Style gets it// for free") has no literal to read: its entries are the merged VALUE's, and a// nested block is read the same way. It used to read as no entries at all, so the// View wrote the class and the sheet carried no rule for it (mission 322 row 9).if (isHybridValue(hybrid)) {for (k of hybrid.keys()) { out.push({ key = k; node = 0; value = hybrid[k]; isBlock = isHybridValue(hybrid[k]); }) }}return out}for (h of n.entries) {let e = hlSyntax(gen, key, h)if (e != null && e.tag == 'object_property') {let v = hlSyntax(gen, key, e.value)let val = hybrid != null ? hybrid[e.key] : nullout.push({ key = e.key; node = e.value; value = val; isBlock = v != null && (v.tag == 'object_expression' || (v.tag == 'binary_expression' && isHybridValue(val))); })}}guardMedia(key, out)return out}// a hybrid VALUE (a rule), as opposed to a scalar declaration or a var() tokenisHybridValue(v) { return v != null && hlTypeName(v) == 'Hybrid' }// TWO BLOCKS UNDER ONE `@media` NAME (creator, 2026-09-13; measured in the social// app's styles file). This walk reads a block's STRUCTURE off the syntax but its// VALUES off the CONSTRUCTED INSTANCE, by name — and a name declared twice holds// one value, the second. Both blocks then read the second one's hybrid, so the// first comes out with no conditions and no declarations (`@media (prefers-color-scheme: )`)// and nothing in the sheet says why. Refused here rather than emitted garbled.guardMedia(key, entries) {let mediaSeen = {}for (e of entries) {if (isMediaBlock(e.key)) {if (mediaSeen[e.key] != null) {hlError(key + " declares '" + e.key + "' twice. A block's values are read off the constructed instance BY NAME, so the second binding is what BOTH blocks read and the first one comes out empty. Spell the two queries out as distinct keys — '@media (prefers-color-scheme: dark)' = { … } and '@media (min-width: 640px)' = { … } — or move the second block into a component's own Style")}mediaSeen[e.key] = true}}return null}// a value as CSS text: a string as written, a number unitless, a static already// folded, a css variable (or a string composed with one) resolved to var(--name)text(v) {return varText(v)}declLine(k, v) {if (minify) { return view.kebab(k) + ':' + text(v) + ';' }return ' ' + view.kebab(k) + ': ' + text(v) + ';' + NL}// a media condition: `minWidth = 960` → `(min-width: 960px)`, a string verbatimcondition(k, v) {if (hlTypeName(v) == 'Number') { return '(' + view.kebab(k) + ': ' + v + 'px)' }return '(' + view.kebab(k) + ': ' + v + ')'}// the whole prelude, `@media (a) and (b)`: an enclosing query of the SAME kind is// joined with `and`; one of another kind cannot be, in a sheet written flatqueryText(kind, outer, literal, conds, name) {let parts = []if (outer != '') {if (!outer.startsWith(kind + ' ')) { hlError("'" + name + "' stands inside '" + outer + "' — two kinds of query cannot be joined into one; write the block beside it, not inside") }parts.push(outer.slice(kind.length + 1, outer.length))}if (literal != '') { parts.push(literal) }for (c of conds) { parts.push(c) }let out = ''for (p of parts) { out = out == '' ? p : out + ' and ' + p }return kind + ' ' + out}// ---- where a rule lands (the archive's placesFor) -------------------------------// `f` is the file block: { key cls refs tags hasView isStyles }; refs are what the// View wrote ([{ tag rule }]), tags the element names the file's own View renders.selectorsFor(f, name) {if (f.isStyles) {if (selectorShaped(name)) { return [name] }return [view.domTag(name)]}let local = localName(name)// the View writes `id = 'x'`: `#x` is that id, nothing else (ticket #51, R3)if (local != '' && f.ids != null && f.ids.includes(local)) { return [name] }if (local != '' && !selectorShaped(local)) {let out = []for (r of f.refs) {if (r.rule == name) {let sel = view.domTag(r.tag) + '.' + view.localClass(f.key)if (!out.includes(sel)) { out.push(sel) }}}return out}if (selectorShaped(name)) { return [name] }let classes = []for (r of f.refs) {if (r.rule == name) {let cls = view.className(name, r.tag)if (!classes.includes(cls)) { classes.push(cls) }}}if (classes.length == 0) {if (f.tags.includes(name) || name == 'body' || name == 'html') { return [view.domTag(name)] }return []}let out = []for (cls of classes) {if (cls == name) { out.push('.' + view.kebab(cls)) }else { out.push(view.domTag(name.slice(0, name.length - cls.length)) + '.' + view.kebab(cls)) }}return out}// ---- emission -------------------------------------------------------------------putRule(f, name, selector, body, query) {let first = seen[query + '|' + selector]if (first != null) {if (first.body == body) { return null }hlError("style rule '" + selector + "' is emitted twice with different bodies — '" + first.rule + "' in " + first.file + " and '" + name + "' in " + f.key + " both emit that selector; put the declarations in ONE rule, or rename one of them")}seen[query + '|' + selector] = { body = body; rule = name; file = f.key; }if (query == '') {parts.push(minify ? selector + '{' + body + '}' : NL + selector + ' {' + NL + body + '}' + NL)} else {mediaPut(query, selector, body)}return null}mediaPut(query, selector, body) {if (!mediaOrder.includes(query)) { mediaOrder.push(query) }// inside a media block every line is indented two spaces (the archive's goldens)let inner = minify ? selector + '{' + body + '}' : ' ' + selector + ' {' + NL + ' ' + body.replace(NL + ' ', NL + ' ') + ' }' + NLmediaParts.push({ query = query; text = inner; })return null}// ONE RULE: its own declarations under `sels`, its nested rules composed below it// (descendant), its media blocks under the query; a container emits nothing itselfemitRule(f, name, sels, entries, query) {let body = ''let kids = []let medias = []for (e of entries) {if (e.isBlock) {if (isMediaBlock(e.key)) { medias.push(e) } else { kids.push(e) }} else if (e.value != null) {body = body + declLine(e.key, e.value)}}if (body != '') {for (sel of sels) { putRule(f, name, sel, body, query) }}for (k of kids) {if (isKeyframes(k.key)) { hlError("'" + k.key + "' inside '" + name + "' in " + f.key + " — an animation is not a rule's child: write it at the top of the Style or of the styles file") }let composed = nestedSels(f, sels, k.key)if (composed.length == 0) {dead.push(f.cls + '.' + name + ' ' + k.key)} else {emitRule(f, name + ' ' + k.key, composed, entriesOf(f.key, k.node, k.value), query)}}for (m of medias) {if (query != '') { hlError("'" + m.key + "' inside '" + name + "' is already under a query — state both conditions in ONE block") }mediaBlock(f, m.key, m.node, m.value, sels, name, '')}return null}// ONE `@media` BLOCK, at the top of a Style (prefixes empty) or inside a rule// (prefixes = the enclosing rule's selectors): scalar entries are CONDITIONS// (only for the bare `@media` spelling), block entries are RULES under the query;// inside a rule, scalar entries are the ENCLOSING RULE's declarations under it.mediaBlock(f, name, node, hybrid, prefixes, outer, outerQuery) {let literal = mediaLiteral(name)let conds = []let own = ''let rules = []let overrides = ''for (e of entriesOf(f.key, node, hybrid)) {if (e.isBlock) { rules.push(e) }// a css VARIABLE re-assigned under the query: `bg = '#000'` inside// `@media { prefersColorScheme = 'dark' … }` is `:root { --bg: #000 }` thereelse if (prefixes.length == 0 && varNames[e.key] != null) { overrides = overrides + declLine('--' + view.kebab(e.key), e.value) }else if (literal == '' && prefixes.length == 0) { conds.push(condition(e.key, e.value)) }else if (prefixes.length > 0) { own = own + declLine(e.key, e.value) }else { hlError("'" + name + "' already spells its query out, so '" + e.key + "' is a declaration directly inside an at-rule — put it in a rule inside the block") }}let q = queryText(queryKind(name), outerQuery, literal, conds, name)if (overrides != '') { putRule(f, name, ':root', overrides, q) }if (own != '') { for (sel of prefixes) { putRule(f, outer, sel, own, q) } }for (r of rules) {if (isMediaBlock(r.key)) { mediaBlock(f, r.key, r.node, r.value, prefixes, outer, q) }else if (isKeyframes(r.key)) { hlError("'" + r.key + "' inside '" + name + "' in " + f.key + " — an animation is not scoped by a query: write it at the top of the Style or of the styles file") }else {let composed = prefixes.length > 0 ? nestedSels(f, prefixes, r.key) : selectorsFor(f, r.key)if (composed.length == 0) { dead.push(f.cls + '.' + r.key + ' (in ' + name + ')') }else { emitRule(f, r.key, composed, entriesOf(f.key, r.node, r.value), q) }}}return null}// a file's rules in authored order: [{ key node value isBlock }] — a component's// Style entries, or a styles file's root members (its inherits' first)fileRules(f) {return f.rules}// ONE `'@keyframes name' = { 'from' = { … } to { … } '50%' = { … } }`: every entry is// a stop, its key the stop's selector verbatim, its entries declarationskeyframes(f, r) {let name = r.key.slice(10, r.key.length).trim()if (name == '') { hlError("'@keyframes' in " + f.key + " names no animation — write the name into the key: '@keyframes fade' = { 'from' = { opacity = 0 } to { opacity = 1 } }") }let body = ''for (st of entriesOf(f.key, r.node, r.value)) {if (!st.isBlock) { hlError("'" + st.key + "' in '" + r.key + "' (" + f.key + ") is a declaration, and an animation holds stops — 'from', to, '50%' — each a block of declarations") }let decls = ''for (d of entriesOf(f.key, st.node, st.value)) {if (d.isBlock) { hlError("'" + d.key + "' in the stop '" + st.key + "' of '" + r.key + "' (" + f.key + ") is a block — a stop holds declarations only") }if (d.value != null) { decls = decls + (minify ? declLine(d.key, d.value) : ' ' + declLine(d.key, d.value)) }}body = body + (minify ? st.key + '{' + decls + '}' : ' ' + st.key + ' {' + NL + decls + ' }' + NL)}putRule(f, r.key, '@keyframes ' + name, body, '')return null}// ---- the sheet ------------------------------------------------------------------// `files` are the blocks in cascade order: the styles file (with its inherits// walked first by the caller), then every served component.sheet(files) {parts = []seen = {}dead = []mediaOrder = []mediaParts = []rootDecls = []vars = {}varNames = {}for (f of files) { collectVars(f) }// the declared variables come first in :root, in declaration order; their dark// values open the dark-scheme query with a :root of their own (an explicit// `@media { prefersColorScheme = 'dark' … }` re-assignment lands after it and wins)let darkBody = ''for (f of files) {for (r of fileRules(f)) {if (!r.isBlock && isVar(r.value)) {rootDecls.push(declLine('--' + view.kebab(r.key), r.value.value))if (r.value.dark != null) { darkBody = darkBody + declLine('--' + view.kebab(r.key), r.value.dark) }}}}if (darkBody != '') { mediaPut(DARK, ':root', darkBody) }for (f of files) {for (r of fileRules(f)) {if (!r.isBlock) {// a scalar root member of a styles file is not a rule (a variable was taken above)} else if (isMediaBlock(r.key)) {mediaBlock(f, r.key, r.node, r.value, [], '', '')} else if (isKeyframes(r.key)) {keyframes(f, r)} else if (isRootBlock(r.key)) {for (d of entriesOf(f.key, r.node, r.value)) {if (d.isBlock) { hlError("'" + r.key + "' holds custom properties only — '" + d.key + "' is a rule; put it beside the block, not inside it") }rootDecls.push(declLine(d.key, d.value))}} else {let sels = selectorsFor(f, r.key)if (sels.length == 0) { dead.push(f.cls + '.' + r.key + ' bound to nothing (no reference, no matching element)') }else { emitRule(f, r.key, sels, entriesOf(f.key, r.node, r.value), '') }}}}let out = minify ? '' : HEADERif (rootDecls.length > 0) {let body = ''for (d of rootDecls) { body = body + d }out = out + (minify ? ':root{' + body + '}' : NL + ':root {' + NL + body + '}' + NL)}for (p of parts) { out = out + p }for (q of mediaOrder) {let inner = ''for (p of mediaParts) { if (p.query == q) { inner = inner + p.text } }out = out + (minify ? q + '{' + inner + '}' : NL + q + ' {' + NL + inner + '}' + NL)}if (dead.length > 0) {for (d of dead) { console.log('style DROPPED: ' + d) }}return out}
Branches
- mainmain branch
Latest commits
- 110c2799mission 002 (code order) 1/4: .hl files out of the root — lib/ (api, git, localtime, markdown, repos, sshgate, sshkeys, tickets, tokens, transport, users), components/styles.hl; jsoncheck.hl removed (imported nowhere); import paths only. gates 200/0, 46/0, 44/0; real-data reads + writes identicalmre
- fdfb4b1bgitoria: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); gates 200/0, 46/0, 44/0mre
- 5b46ac84antcolony#40: LOG.md — missions 069/072 are antcolony missions (report paths on Byrodin)mre
- 5602ff41gitoria: Hybriel master 190aa11d (fc838894 GC correctness, #127 mountKids by reference, #126, #48) — tracker README flat; gates 200/0, 46/0, 44/0mre
- e85eaf01gitoria: 069 round 2 — hybriel 1a096ad3 not adopted (Markdown SSR still grows); browser gate waits for the server-side logout before restartmre
- 09ce4f3fgitoria: mission 069 re-vendor hybriel 8efba065 stopped (big SSR pages grow + slow down); lambda audit clean; old vendor keptmre
- 3dc43108antcolony#40: mission references point to the moved missionsmre
- 8d9450fdantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
- 205d5fe4gitoria: Hybriel master ff51cf46; ssh keys/tokens no double rows (session sync); gates follow #20mre
- 9b27cb26gitoria#21: installable app (manifest, service worker, offline start page), own iconmre
- 68dcb603deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- e2deed6dgitoria#20: "Add code" only on the Code page of an empty repository, no collapsiblemre
- 8bb97ffddeploy.sh: never send .git or .gitignore to Byrodinmre
- fd981932State of 2026-09-27; bin/ no longer tracked (Hybriel commit is in README)mre
- 4a2d7125initial commitmre