@scattered-light/loomiere 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -285,6 +285,34 @@ const inLook = (selectors, name) =>
285
285
  .map((s) => `${s.trim()}[data-look='${name}']`)
286
286
  .join(', ');
287
287
 
288
+ /** Pair class name → Map of variable → value, read from pairBlocks' text. */
289
+ const pairDeclarations = (text) =>
290
+ new Map([...(text ?? '').matchAll(/^\.[\w-]+-pair-([\w-]+) \{\n([\s\S]*?)\n\}/gm)].map(([, name, body]) => [name, declarationsIn(body)]));
291
+
292
+ /**
293
+ * What a look's pair classes need beyond the brand's. The pairs and their re-pointed parts are
294
+ * the brand's, but a look's `tokens` can change the tokens that lead to them: a token the look
295
+ * points elsewhere (or newly at a re-pointed one) gets the look's value inside each pair, under
296
+ * [data-look='<name>'] .<prefix>-pair-<pair>, which outranks the brand's pair class.
297
+ */
298
+ function lookPairBlocks(name, parts, base) {
299
+ const own = pairDeclarations(parts.rules.pairs);
300
+ const brand = pairDeclarations(base.rules.pairs);
301
+ const names = (m) => [...m.keys()].sort().join(',');
302
+ if (names(own) !== names(brand)) throw new Error(`Look "${name}" changes the pairs rules, not only values. A look can change colors, fonts and sizes, but it has to keep the brand's palettes, pairs, statuses and emphasis levels.`);
303
+ const blocks = [];
304
+ for (const [pair, decls] of brand) {
305
+ const mine = own.get(pair);
306
+ const lines = [];
307
+ for (const [v, value] of mine) if (decls.get(v) !== value) lines.push(`${v}: ${value};`);
308
+ // No longer leads to a re-pointed token in this look: restate the look's own value so the
309
+ // brand's declaration in the pair class doesn't win.
310
+ for (const v of decls.keys()) if (!mine.has(v) && parts.root[v] !== undefined) lines.push(`${v}: ${parts.root[v]};`);
311
+ if (lines.length) blocks.push(`[data-look='${name}'] .${prefix}-pair-${pair} {\n${indent(lines.join('\n'), 2)}\n}`);
312
+ }
313
+ return blocks.join('\n\n');
314
+ }
315
+
288
316
  /** [rootBlocks, modeBlocks] for every look against the brand's own parts. */
289
317
  function lookBlocks(dictionary, base) {
290
318
  const roots = [];
@@ -293,8 +321,12 @@ function lookBlocks(dictionary, base) {
293
321
  for (const [rule, text] of Object.entries(parts.rules)) {
294
322
  // Color vision blocks list the data colors by name, so a look with its own names has its own.
295
323
  if (rule === 'vision') continue;
324
+ // A look's `tokens` can change what leads to a re-pointed token; lookPairBlocks restates those.
325
+ if (rule === 'pairs') continue;
296
326
  if (text !== base.rules[rule]) throw new Error(`Look "${name}" changes the ${rule} rules, not only values. A look can change colors, fonts and sizes, but it has to keep the brand's palettes, pairs, statuses and emphasis levels.`);
297
327
  }
328
+ const pairRules = lookPairBlocks(name, parts, base);
329
+ if (pairRules) modeRules.push(pairRules);
298
330
  const changed = Object.entries(parts.root).filter(([v, value]) => base.root[v] !== value);
299
331
  if (changed.length) roots.push(`/* Look: ${label} (data-look='${name}'). */\n[data-look='${name}'] {\n${indent(changed.map(([v, value]) => `${v}: ${value};`).join('\n'), 2)}\n}`);
300
332
  if (parts.rules.vision !== base.rules.vision) {
@@ -345,8 +345,8 @@ function brandBook({ theme, brand, byPath, cssName, usageOf, tokens, tiers, styl
345
345
  '',
346
346
  '`window.Loomiere` holds two kinds of component:',
347
347
  '',
348
- `- **Wrappers** put their own classes on, so use them bare: ${['Button', 'Input', 'Field', 'Fieldset', 'Form', 'NavigationMenu'].filter(has).map((n) => `\`Loomiere.${n}\``).join(', ')}. \`Loomiere.Button\` also takes \`emphasis\` (\`bold|standard|subtle|quiet\`) and \`palette\` props, and a Button holding only an icon draws itself square (it sets \`data-icon-only\`). A plain \`.Button\`, such as a Popover or Toolbar trigger, is drawn square when it has an \`aria-label\` and a single icon inside, or when you set \`data-icon-only\` on it.`,
349
- '- **Everything else is the bare Base UI part** under its usual name (`Loomiere.Popover.Popup`, `Loomiere.ToggleGroup`). It draws nothing until you give each part its class, which is the component and part names joined: `<Loomiere.Popover.Popup className="PopoverPopup">`, `<Loomiere.Toggle className="Toggle">`. The component READMEs list every class.',
348
+ `- **Wrappers** put their own classes on, so use them bare: ${['Button', 'Input', 'Field', 'Fieldset', 'Form', 'NavigationMenu', 'Select', 'NumberField', 'OTPField', 'Autocomplete', 'Combobox', 'Checkbox', 'Radio', 'CheckboxGroup', 'Toggle', 'ToggleGroup', 'Dialog', 'AlertDialog', 'Meter', 'Progress', 'Slider', 'Menu', 'ContextMenu', 'Menubar', 'Popover', 'PreviewCard', 'Tooltip', 'Drawer', 'Accordion', 'Collapsible', 'Tabs', 'Switch', 'Toast', 'Avatar', 'Toolbar', 'ScrollArea', 'Separator'].filter(has).map((n) => `\`Loomiere.${n}\``).join(', ')}. \`Loomiere.Button\` also takes \`emphasis\` (\`bold|standard|subtle|quiet\`) and \`palette\` props, and a Button holding only an icon draws itself square (it sets \`data-icon-only\`). Wrapped parts that are buttons (\`Dialog.Trigger\`, \`Menu.Trigger\`, \`Toolbar.Button\`, \`Drawer.Close\`...) take the same \`emphasis\` and \`palette\` props and draw square the same way. \`Popover.Trigger\`, \`Tooltip.Trigger\`, \`Select.Trigger\` and \`Dialog.Close\` become Buttons only when they set \`emphasis\`, \`palette\` or \`className=\"Button\"\`. A plain \`.Button\` on a bare part is drawn square when it has an \`aria-label\` and a single icon inside, or when you set \`data-icon-only\` on it.`,
349
+ '- **Table is plain HTML**, not a Base UI part, so it has no wrapper. Use the classes in its stylesheet.',
350
350
  '',
351
351
  '`Loomiere.Base` holds the bare Base UI parts for the wrappers too (`Loomiere.Base.Button`), for when you need the part without its classes.',
352
352
  '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scattered-light/loomiere",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Turns a small brand file into a full design token theme: OKLCH color ramps, a type scale, light, dark and high-contrast modes, plus Tailwind, MUI, Chakra, AG Grid and Figma exports.",
5
5
  "keywords": ["design-tokens", "theme", "style-dictionary", "dtcg", "oklch", "base-ui", "ag-grid", "tailwind"],
6
6
  "repository": {
package/tokens/README.md CHANGED
@@ -66,6 +66,7 @@ A look is another way the same brand can look: an old and a new brand, a quiet a
66
66
  - It is merged over the brand: objects merge key by key and lists replace. A palette, a pair, a concept, an emphasis or status entry, or a font's metrics replaces the brand's whole entry, so a look's palette never keeps keys from the brand's.
67
67
  - `tokens` picks a value for any semantic or component token, for what the brand inputs can't say (square buttons, a thicker focus ring). It works at the top of the brand file too.
68
68
  - Every look has the same tokens; only their values change. Components, Figma bindings and code written against one look work in all of them. Pair names and role rules must match across looks; the build says so if they don't.
69
+ - A look's `tokens` can point a token at something else in that look only, such as Highlighter's selected table rows at the highlighter yellow: `"tokens": { "table.row-selected": "{scope.secondary.soft-hover}" }`. Point at a scope slot (`{scope.<palette>.<slot>}`) or a semantic token rather than a palette step, so the value still changes with light, dark and high contrast. Brand pair classes pick up the look's value too.
69
70
 
70
71
  In CSS a look is `data-look="<name>"` on the root element, next to `data-theme`. With no `data-look` the brand's own look applies. `tokens.css` holds one `[data-look]` block with only the variables that change, plus that look's dark, high contrast and color vision blocks. The contrast check runs on every look, and `tokens.json` lists the looks under `looks` with what each one changes. In Figma each look is a mode of the Brand collection (see [figma/README.md](../figma/README.md)), and Storybook has a Look toolbar.
71
72