Z User commited on
Commit
7c1922e
·
1 Parent(s): 0699c28

sync work in progress

Browse files
public/dssl/engine.js CHANGED
@@ -19,6 +19,22 @@
19
 
20
  const LAYER_RANK = { tokens: 0, base: 1, components: 2, utilities: 3, pin: 4 };
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  // state pseudo-classes that may appear inside a rule selector. Used to strip
23
  // them back OFF when collecting recompute candidates (see stateEvent).
24
  const STATE_PSEUDO_RE = /:(hover|focus|focus-visible|active|focus-within|visited|target|disabled|enabled|checked|placeholder-shown|read-only|read-write|open|closed|empty|required|invalid|valid|link|indeterminate)\b/g;
@@ -326,9 +342,26 @@ export class DsslEngine {
326
  let cancelAnims = false;
327
  let stack = false;
328
 
 
 
329
  for (const rule of matched) {
330
  const imp = rule.layer === 'pin' ? 'important' : '';
331
- for (const d of rule.decls) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  if (d.prop === '@animate') {
333
  if (d.value === null) { cancelAnims = true; continue; }
334
  animSpecs.push({ ...d.value, key: rule.id + '|' + rule.media.join('&') + '|' + rule.signals.map(s => s.name + s.op + s.value).join('&') });
@@ -346,7 +379,6 @@ export class DsslEngine {
346
  if (!this._warned) this._warned = new Set();
347
  if (!this._warned.has(msg)) { this._warned.add(msg); console.warn(msg); }
348
  }
349
- }
350
  }
351
 
352
  this.ownMap.set(el, newOwned);
 
19
 
20
  const LAYER_RANK = { tokens: 0, base: 1, components: 2, utilities: 3, pin: 4 };
21
 
22
+ // v0.3.4 — alignment implies a formatting context.
23
+ // place-items / place-content / align-* / justify-* only arrange children
24
+ // inside flex or grid containers. On a plain <span> or <button> they used to
25
+ // silently do nothing — which is exactly how the feature icons, the theme
26
+ // toggle and the token dots ended up pinned top-left inside their circles.
27
+ // The language promises loud failures over silent ones, so when a winning
28
+ // declaration asks for alignment but the element has no layout context, the
29
+ // engine PROMOTES it: inline elements → inline-grid (flow preserved), blockish
30
+ // elements → grid. Explicit `display:` in the same cascade still wins, because
31
+ // the promotion is applied first and "later wins" is the whole cascade.
32
+ const ALIGN_PROPS = new Set([
33
+ 'place-items', 'place-content', 'align-items', 'justify-items',
34
+ 'align-content', 'justify-content',
35
+ ]);
36
+ const HAS_CONTEXT_RE = /^(flex|inline-flex|grid|inline-grid)$/;
37
+
38
  // state pseudo-classes that may appear inside a rule selector. Used to strip
39
  // them back OFF when collecting recompute candidates (see stateEvent).
40
  const STATE_PSEUDO_RE = /:(hover|focus|focus-visible|active|focus-within|visited|target|disabled|enabled|checked|placeholder-shown|read-only|read-write|open|closed|empty|required|invalid|valid|link|indeterminate)\b/g;
 
342
  let cancelAnims = false;
343
  let stack = false;
344
 
345
+ // flatten matched decls in cascade order (the promotion below may prepend)
346
+ const flat = [];
347
  for (const rule of matched) {
348
  const imp = rule.layer === 'pin' ? 'important' : '';
349
+ for (const d of rule.decls) flat.push({ d, imp, rule });
350
+ }
351
+
352
+ // v0.3.4: alignment implies a formatting context — see ALIGN_PROPS above.
353
+ // Read AFTER the wipe so the natural (parent-blockified) display decides;
354
+ // none/contents are never promoted (promoting would unhide or boxify them).
355
+ if (flat.some(x => ALIGN_PROPS.has(x.d.prop))) {
356
+ let disp = '';
357
+ try { disp = getComputedStyle(el).display; } catch { }
358
+ if (disp && !HAS_CONTEXT_RE.test(disp) && disp !== 'none' && disp !== 'contents') {
359
+ const inlineCtx = disp === 'inline' || disp.startsWith('inline-');
360
+ flat.unshift({ imp: '', d: { prop: 'display', value: inlineCtx ? 'inline-grid' : 'grid' } });
361
+ }
362
+ }
363
+
364
+ for (const { d, imp, rule } of flat) {
365
  if (d.prop === '@animate') {
366
  if (d.value === null) { cancelAnims = true; continue; }
367
  animSpecs.push({ ...d.value, key: rule.id + '|' + rule.media.join('&') + '|' + rule.signals.map(s => s.name + s.op + s.value).join('&') });
 
379
  if (!this._warned) this._warned = new Set();
380
  if (!this._warned.has(msg)) { this._warned.add(msg); console.warn(msg); }
381
  }
 
382
  }
383
 
384
  this.ownMap.set(el, newOwned);
public/dssl/index.js CHANGED
@@ -18,7 +18,7 @@ import { parse } from './parser.js';
18
  import { resolve, DEFAULT_TOKENS } from './resolver.js';
19
  import { DsslEngine } from './engine.js';
20
 
21
- export const VERSION = '0.3.3';
22
 
23
  let counter = 0;
24
  const engines = new Map();
 
18
  import { resolve, DEFAULT_TOKENS } from './resolver.js';
19
  import { DsslEngine } from './engine.js';
20
 
21
+ export const VERSION = '0.3.4';
22
 
23
  let counter = 0;
24
  const engines = new Map();
public/dssl/resolver.js CHANGED
@@ -344,6 +344,26 @@ const SEMANTIC = {
344
  }
345
  return [{ prop: 'place-items', value: `${p[0]} ${p[1]}` }, { prop: 'place-content', value: `${p[0]} ${p[1]}` }];
346
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  cover(v, ctx) {
348
  const pos = (v || 'absolute').trim();
349
  return [{ prop: 'position', value: pos }, { prop: 'inset', value: '0' }];
 
344
  }
345
  return [{ prop: 'place-items', value: `${p[0]} ${p[1]}` }, { prop: 'place-content', value: `${p[0]} ${p[1]}` }];
346
  },
347
+ // v0.3.4 — the one-word centering primitive, because "how do I center this
348
+ // thing" is the first question everyone asks. Emits the alignment pair only;
349
+ // the ENGINE guarantees a formatting context for alignment declarations
350
+ // (inline elements → inline-grid, blocks → grid), so `center:` can never
351
+ // silently no-op the way raw place-items did on spans and buttons.
352
+ // center: both axes · center: x horizontal only · center: y vertical only
353
+ center(v, ctx) {
354
+ const mode = (v || '').trim().toLowerCase() || 'both';
355
+ if (mode === 'x') {
356
+ return [{ prop: 'justify-items', value: 'center' }, { prop: 'justify-content', value: 'center' }];
357
+ }
358
+ if (mode === 'y') {
359
+ return [{ prop: 'align-items', value: 'center' }, { prop: 'align-content', value: 'center' }];
360
+ }
361
+ if (mode !== 'both') {
362
+ ctx.errors.push({ file: ctx.file, line: ctx.line, message: `unknown center axis "${v}"`, hint: didYouMean(mode, ['both', 'x', 'y']) });
363
+ return [];
364
+ }
365
+ return [{ prop: 'place-items', value: 'center center' }, { prop: 'place-content', value: 'center center' }];
366
+ },
367
  cover(v, ctx) {
368
  const pos = (v || 'absolute').trim();
369
  return [{ prop: 'position', value: pos }, { prop: 'inset', value: '0' }];
public/styles/docs.dssl CHANGED
@@ -268,7 +268,7 @@
268
  .tok-dot {
269
  size: 34
270
  circle: 34
271
- place: center
272
  font-family: font.mono
273
  font-size: 11
274
  border: 1 solid color.line
 
268
  .tok-dot {
269
  size: 34
270
  circle: 34
271
+ center: both
272
  font-family: font.mono
273
  font-size: 11
274
  border: 1 solid color.line
public/styles/home.dssl CHANGED
@@ -366,7 +366,7 @@ motion rise-in {
366
  .feature-icon {
367
  size: 40
368
  circle: 40
369
- place: center
370
  background: color.accent/12
371
  color: color.accent
372
  font-size: 19
 
366
  .feature-icon {
367
  size: 40
368
  circle: 40
369
+ center: both
370
  background: color.accent/12
371
  color: color.accent
372
  font-size: 19
public/styles/site.dssl CHANGED
@@ -126,7 +126,10 @@ select { use focus-ring }
126
 
127
  .theme-btn {
128
  size: 36
129
- place: center
 
 
 
130
  color: color.muted
131
  transition: 150
132
  border: 1 solid color.ink/40
 
126
 
127
  .theme-btn {
128
  size: 36
129
+ // auto (system) mode shows the sun+moon PAIR, light/dark show one icon.
130
+ // `center:` centers a single-content box, but a two-item grid would stack
131
+ // the pair — an explicit flex row centers either state (v0.3.4)
132
+ row: center gap 2
133
  color: color.muted
134
  transition: 150
135
  border: 1 solid color.ink/40
scripts/dssl-smoke.mjs CHANGED
@@ -73,6 +73,14 @@ gallery {
73
  .span-2 { grid-column: span 2 }
74
  .auto-grid { grid-template-columns: repeat(auto-fill, minmax(200, 1fr)) }
75
 
 
 
 
 
 
 
 
 
76
  @media (width >= 768) {
77
  .card { padding: space.6 }
78
  }
@@ -152,6 +160,15 @@ const span2 = doc.rules.find(r => r.sel === '.span-2');
152
  assert(span2 && span2.decls.some(d => d.value === 'span 2'), `grid-column: span 2 unitless (got: ${span2?.decls[0]?.value})`);
153
  const autoGrid = doc.rules.find(r => r.sel === '.auto-grid');
154
  assert(autoGrid && autoGrid.decls.some(d => d.value === 'repeat(auto-fill, minmax(200px, 1fr))'), `repeat(auto-fill, minmax) works (got: ${autoGrid?.decls[0]?.value})`);
 
 
 
 
 
 
 
 
 
155
  assert(doc.rules.some(r => r.sel === '.card' && r.media.length === 1), 'media rule for .card');
156
  assert(doc.rules.some(r => r.media.includes('(prefers-color-scheme: dark)')), '@media dark sugar');
157
  assert(doc.rules.some(r => r.media.includes('(prefers-color-scheme: light)')), '@media light sugar');
 
73
  .span-2 { grid-column: span 2 }
74
  .auto-grid { grid-template-columns: repeat(auto-fill, minmax(200, 1fr)) }
75
 
76
+ // v0.3.4: the one-word centering primitive
77
+ .icon-chip {
78
+ size: 40
79
+ center: both
80
+ }
81
+ .chip-x { center: x }
82
+ .chip-y { center: y }
83
+
84
  @media (width >= 768) {
85
  .card { padding: space.6 }
86
  }
 
160
  assert(span2 && span2.decls.some(d => d.value === 'span 2'), `grid-column: span 2 unitless (got: ${span2?.decls[0]?.value})`);
161
  const autoGrid = doc.rules.find(r => r.sel === '.auto-grid');
162
  assert(autoGrid && autoGrid.decls.some(d => d.value === 'repeat(auto-fill, minmax(200px, 1fr))'), `repeat(auto-fill, minmax) works (got: ${autoGrid?.decls[0]?.value})`);
163
+ // v0.3.4: center: primitive — alignment pair, engine promotes the context
164
+ const chip = doc.rules.find(r => r.sel === '.icon-chip');
165
+ assert(chip && chip.decls.some(d => d.prop === 'place-items' && d.value === 'center center'), `center: expands to place-items center center`);
166
+ assert(chip && chip.decls.some(d => d.prop === 'place-content' && d.value === 'center center'), `center: expands to place-content center center`);
167
+ const chipX = doc.rules.find(r => r.sel === '.chip-x');
168
+ assert(chipX && chipX.decls.some(d => d.prop === 'justify-items' && d.value === 'center') && chipX.decls.some(d => d.prop === 'justify-content' && d.value === 'center'), 'center: x axis emits justify pair');
169
+ const chipY = doc.rules.find(r => r.sel === '.chip-y');
170
+ assert(chipY && chipY.decls.some(d => d.prop === 'align-items' && d.value === 'center') && chipY.decls.some(d => d.prop === 'align-content' && d.value === 'center'), 'center: y axis emits align pair');
171
+ assert(!doc.errors.some(e => e.message.includes('center')), 'no false center errors');
172
  assert(doc.rules.some(r => r.sel === '.card' && r.media.length === 1), 'media rule for .card');
173
  assert(doc.rules.some(r => r.media.includes('(prefers-color-scheme: dark)')), '@media dark sugar');
174
  assert(doc.rules.some(r => r.media.includes('(prefers-color-scheme: light)')), '@media light sugar');
src/app/layout.tsx CHANGED
@@ -109,6 +109,7 @@ export default function RootLayout({
109
  ))}
110
  {/* engine starts downloading with the document — module defer rules
111
  mean it executes after parse with the full SSR DOM present */}
 
112
  <script type="module" src="/dssl/boot.js" />
113
  </head>
114
  <body>{children}</body>
 
109
  ))}
110
  {/* engine starts downloading with the document — module defer rules
111
  mean it executes after parse with the full SSR DOM present */}
112
+ {/* eslint-disable-next-line @next/next/no-sync-scripts -- type="module" is deferred by spec; the rule cannot see that */}
113
  <script type="module" src="/dssl/boot.js" />
114
  </head>
115
  <body>{children}</body>
src/components/dssl/SiteRoot.tsx CHANGED
@@ -120,7 +120,7 @@ export default function SiteRoot() {
120
  </nav>
121
 
122
  <div className="nav-side">
123
- <a href="#/changelog" className="ver-badge" onClick={(e) => { e.preventDefault(); go("changelog"); }}>v0.3.3</a>
124
  <button type="button" className="theme-btn" onClick={cycleTheme} aria-label="Toggle color theme" title="Theme: auto / light / dark">
125
  <span className="icon-sun"><SunIcon /></span>
126
  <span className="icon-moon"><MoonIcon /></span>
@@ -148,7 +148,7 @@ export default function SiteRoot() {
148
  <div className="wrap footer-inner">
149
  <div className="footer-col">
150
  <div className="footer-note">
151
- <strong>DSSL</strong> — domain specific style language. Built v0.1 → v0.3.3 while
152
  building this site; every feature it needed, it grew, and every bug you sent got
153
  fixed for real. This page runs on its own engine, no CSS files attached —
154
  the engine styles this DOM directly.
 
120
  </nav>
121
 
122
  <div className="nav-side">
123
+ <a href="#/changelog" className="ver-badge" onClick={(e) => { e.preventDefault(); go("changelog"); }}>v0.3.4</a>
124
  <button type="button" className="theme-btn" onClick={cycleTheme} aria-label="Toggle color theme" title="Theme: auto / light / dark">
125
  <span className="icon-sun"><SunIcon /></span>
126
  <span className="icon-moon"><MoonIcon /></span>
 
148
  <div className="wrap footer-inner">
149
  <div className="footer-col">
150
  <div className="footer-note">
151
+ <strong>DSSL</strong> — domain specific style language. Built v0.1 → v0.3.4 while
152
  building this site; every feature it needed, it grew, and every bug you sent got
153
  fixed for real. This page runs on its own engine, no CSS files attached —
154
  the engine styles this DOM directly.
src/components/dssl/views/ChangelogView.tsx CHANGED
@@ -210,6 +210,23 @@ radius { md: 14 → md: 4 }
210
  color { paper: #faf8f4 → #f5f0e4, accent: oklch(…) → #7c2d2d }
211
  font { serif: "Charter", "Iowan Old Style", "Palatino", … } // new`,
212
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  ];
214
 
215
  export default function ChangelogView() {
 
210
  color { paper: #faf8f4 → #f5f0e4, accent: oklch(…) → #7c2d2d }
211
  font { serif: "Charter", "Iowan Old Style", "Palatino", … } // new`,
212
  },
213
+ {
214
+ version: "v0.3.4",
215
+ date: "bug report → fix",
216
+ title: "Centering that always works",
217
+ why: "user report: “the {} and stuff in The Engine are still not centered in their circle”",
218
+ kind: "add",
219
+ body:
220
+ "Every glyph chip on this site asked for centering — place: center — and place-items/place-content only arrange children inside flex or grid containers. On a <span> or a <button> they silently did nothing, so the feature icons, the theme toggle and the docs token dots all hugged the top-left of their circles. The fix is engine-level, because the engine is the only part that knows what an element actually is: alignment declarations now IMPLY a formatting context. When a winning declaration asks to align but the element has no layout context, the engine promotes it — inline elements become inline-grid (flow preserved), blocks become grid — while an explicit display: still wins the cascade. And the vocabulary gained the word people actually reach for first: center: both / x / y.",
221
+ code: `.icon-chip {
222
+ size: 40
223
+ circle: 40
224
+ center: both // works on a span, a button, anything
225
+ }
226
+
227
+ // engine rule: alignment implies a formatting context
228
+ // inline → inline-grid · block → grid · explicit display: still wins`,
229
+ },
230
  ];
231
 
232
  export default function ChangelogView() {
src/components/dssl/views/CheatSheetView.tsx CHANGED
@@ -73,6 +73,8 @@ column: gap 8 start
73
  grid: 3
74
  grid: auto-fill 220
75
  stack: center // overlay children
 
 
76
  place: center align: center
77
  cover: fixed sticky: 0
78
  size: 40 100% circle: 36
 
73
  grid: 3
74
  grid: auto-fill 220
75
  stack: center // overlay children
76
+ center: both // center: x / center: y — the engine guarantees a
77
+ // formatting context, so it can never silently no-op
78
  place: center align: center
79
  cover: fixed sticky: 0
80
  size: 40 100% circle: 36
src/components/dssl/views/DocsView.tsx CHANGED
@@ -324,7 +324,8 @@ background: // values can continue
324
  <tr><td><code>row</code> / <code>column</code></td><td><code>row: gap 8 center wrap</code></td><td>flexbox + only the words you name (between, reverse, inline…)</td></tr>
325
  <tr><td><code>grid</code></td><td><code>grid: 3</code> · <code>grid: auto-fill 220</code></td><td>repeat(3, 1fr) · repeat(auto-fill, minmax(220px, 1fr))</td></tr>
326
  <tr><td><code>stack</code></td><td><code>stack: center</code></td><td>overlay grid; children share cell 1/1</td></tr>
327
- <tr><td><code>place</code> / <code>align</code></td><td><code>place: center</code></td><td>place-items/content · align-items</td></tr>
 
328
  <tr><td><code>cover</code></td><td><code>cover: fixed</code></td><td>position + inset: 0</td></tr>
329
  <tr><td><code>sticky</code></td><td><code>sticky: 0</code></td><td>position: sticky; top: 0px</td></tr>
330
  <tr><td><code>font</code></td><td><code>font: 16/1.4 ink</code></td><td>font-size / line-height / color</td></tr>
 
324
  <tr><td><code>row</code> / <code>column</code></td><td><code>row: gap 8 center wrap</code></td><td>flexbox + only the words you name (between, reverse, inline…)</td></tr>
325
  <tr><td><code>grid</code></td><td><code>grid: 3</code> · <code>grid: auto-fill 220</code></td><td>repeat(3, 1fr) · repeat(auto-fill, minmax(220px, 1fr))</td></tr>
326
  <tr><td><code>stack</code></td><td><code>stack: center</code></td><td>overlay grid; children share cell 1/1</td></tr>
327
+ <tr><td><code>place</code> / <code>align</code></td><td><code>place: center</code></td><td>place-items/content · align-items — alignment implies a formatting context (see <code>center</code>)</td></tr>
328
+ <tr><td><code>center</code></td><td><code>center: both</code> · <code>center: x</code> · <code>center: y</code></td><td>the centering primitive. The engine promotes any element to a grid/flex context first (inline → inline-grid, block → grid; an explicit <code>display:</code> still wins), so centering works on a span, a button, anything</td></tr>
329
  <tr><td><code>cover</code></td><td><code>cover: fixed</code></td><td>position + inset: 0</td></tr>
330
  <tr><td><code>sticky</code></td><td><code>sticky: 0</code></td><td>position: sticky; top: 0px</td></tr>
331
  <tr><td><code>font</code></td><td><code>font: 16/1.4 ink</code></td><td>font-size / line-height / color</td></tr>
src/components/dssl/views/SpecView.tsx CHANGED
@@ -11,7 +11,7 @@ export default function SpecView() {
11
  <p className="page-sub">
12
  The v0.1 draft was put to a seven-point vote. This document is the ratified
13
  v0.2 result — every decision below is implemented by the engine serving this
14
- page. The engine itself has kept growing since: it is now at v0.3.3 (see the
15
  changelog for what the site demanded).
16
  </p>
17
  </div>
 
11
  <p className="page-sub">
12
  The v0.1 draft was put to a seven-point vote. This document is the ratified
13
  v0.2 result — every decision below is implemented by the engine serving this
14
+ page. The engine itself has kept growing since: it is now at v0.3.4 (see the
15
  changelog for what the site demanded).
16
  </p>
17
  </div>