@scattered-light/loomiere 0.5.3 → 0.7.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.
Files changed (39) hide show
  1. package/build/build-tokens.mjs +33 -6
  2. package/exporters/chakra.mjs +74 -0
  3. package/exporters/component-styles.mjs +64 -0
  4. package/exporters/design-system.mjs +23 -7
  5. package/exporters/mui.mjs +60 -0
  6. package/figma/library/pages/18-custom/01-custom.js +57 -0
  7. package/figma/library/pages/18-custom/02-labels.js +7 -0
  8. package/figma/library/pages/18-custom/03-doc.js +28 -0
  9. package/figma/library/pages/18-custom/04-date-picker.js +327 -0
  10. package/figma/library/pages/18-custom/05-date-picker-labels.js +9 -0
  11. package/figma/library/pages/18-custom/06-date-picker-doc.js +25 -0
  12. package/figma/library/pages/18-custom/07-money-field.js +107 -0
  13. package/figma/library/pages/18-custom/08-money-field-labels.js +8 -0
  14. package/figma/library/pages/18-custom/09-money-field-doc.js +101 -0
  15. package/figma/library/pages/18-custom/10-splitter.js +100 -0
  16. package/figma/library/pages/18-custom/11-splitter-labels.js +8 -0
  17. package/figma/library/pages/18-custom/12-splitter-doc.js +92 -0
  18. package/figma/library/pages/18-custom/13-empty-state.js +114 -0
  19. package/figma/library/pages/18-custom/14-empty-state-labels.js +8 -0
  20. package/figma/library/pages/18-custom/15-empty-state-doc.js +58 -0
  21. package/figma/library/pages/18-custom/16-collection.js +255 -0
  22. package/figma/library/pages/18-custom/17-collection-doc.js +43 -0
  23. package/figma/library/run-order.json +1 -0
  24. package/lib/contrast.mjs +17 -0
  25. package/package.json +1 -1
  26. package/tokens/README.md +10 -3
  27. package/tokens/component/badge.json +53 -0
  28. package/tokens/component/collection.json +49 -0
  29. package/tokens/component/list-detail.json +29 -0
  30. package/tokens/component/money-field.json +9 -0
  31. package/tokens/component/splitter.json +25 -0
  32. package/tokens/semantic/accessibility.json +4 -0
  33. package/tokens/semantic/calendar.json +75 -0
  34. package/tokens/semantic/empty.json +79 -0
  35. package/tokens/semantic/field.json +4 -0
  36. package/tokens/semantic/popup.json +2 -2
  37. package/tokens/semantic/table.json +8 -0
  38. package/tools/brand-builder/builder.js +45 -0
  39. package/tools/brand-builder/index.html +6 -0
@@ -0,0 +1,75 @@
1
+ {
2
+ "calendar": {
3
+ "$description": "A month grid for picking a date or a period. Colors come from the item, emphasis and interactive concepts so a calendar follows the same selection language as a list. Sizes that are only about the grid live here because no other concept owns a day cell.",
4
+ "day-size": {
5
+ "$value": "{space.10}",
6
+ "$type": "dimension",
7
+ "$description": "A day cell on a pointer. space.10 keeps seven columns inside a phone; touch layouts raise the cell to the minimum touch target in CSS."
8
+ },
9
+ "day-radius": {
10
+ "$value": "{radius.pill}",
11
+ "$type": "dimension",
12
+ "$description": "Selected days are circles. The pill radius is the same full radius badges and switches use."
13
+ },
14
+ "day-color": {
15
+ "$value": "{text.primary}",
16
+ "$type": "color",
17
+ "$description": "A day in the month being shown."
18
+ },
19
+ "day-color-outside": {
20
+ "$value": "{text.tertiary}",
21
+ "$type": "color",
22
+ "$description": "A day from the previous or next month, still selectable, drawn back so it does not compete with this month."
23
+ },
24
+ "day-color-disabled": {
25
+ "$value": "{text.disabled}",
26
+ "$type": "color",
27
+ "$description": "A day outside the allowed period, or one a length rule rejects. It stays in the tab order so the reason can be read."
28
+ },
29
+ "day-background-hover": {
30
+ "$value": "{item.background-hover}",
31
+ "$type": "color",
32
+ "$description": "The same hover tint as a list item, so a day feels like a choice in a list."
33
+ },
34
+ "range-background": {
35
+ "$value": "{interactive.tint-active}",
36
+ "$type": "color",
37
+ "$description": "The band between the start and end of a period. Same tint as a selected list item, quieter than the start and end circles."
38
+ },
39
+ "range-color": {
40
+ "$value": "{text.on-tint-strong}",
41
+ "$type": "color",
42
+ "$description": "Day numbers that sit on the range band. The on-tint text color is the one that stays readable on interactive.tint-active."
43
+ },
44
+ "today-ring": {
45
+ "$value": "{border.accent}",
46
+ "$type": "color",
47
+ "$description": "An inset ring on today, including when today is also selected, so today is never only a color of the number."
48
+ },
49
+ "weekday-color": {
50
+ "$value": "{text.tertiary}",
51
+ "$type": "color",
52
+ "$description": "Weekday initials and week numbers are labels, not choices."
53
+ },
54
+ "grid-gap": {
55
+ "$value": "{space.0-5}",
56
+ "$type": "dimension",
57
+ "$description": "Space between week rows. Days in a week share an edge, so the range band reads as one shape."
58
+ },
59
+ "month-gap": {
60
+ "$value": "{space.4}",
61
+ "$type": "dimension",
62
+ "$description": "Space between months shown side by side, the same step as panel padding so the two columns line up."
63
+ },
64
+ "header-gap": {
65
+ "$value": "{space.2}",
66
+ "$type": "dimension",
67
+ "$description": "Space between the month title and the weekday row."
68
+ },
69
+ "panel-padding": {
70
+ "$value": "{space.4}",
71
+ "$type": "dimension",
72
+ "$description": "Padding inside the popup. The shared popup padding is one step and is too tight for a calendar."
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,79 @@
1
+ {
2
+ "empty": {
3
+ "$description": "The block a page, pane, table, list or popup shows when there is nothing to show. Sizes only. The media tint is the subtle emphasis level (data-status and data-color recolor it), and the title and description stay in the normal text colors so a long line stays readable. Table, list, popup and pane all read these, so one empty state matches in every place.",
4
+ "padding-block": {
5
+ "compact": {
6
+ "$value": "{space.6}",
7
+ "$description": "24px above and below a compact empty state. The same room a popup already gives a plain 'No results' line, so the two stay one height."
8
+ },
9
+ "standard": {
10
+ "$value": "{space.10}",
11
+ "$description": "40px. A pane or a section needs more air than a table row, or the block looks stuck to the content around it."
12
+ },
13
+ "spacious": {
14
+ "$value": "{space.16}",
15
+ "$description": "64px. A page with nothing else on it. The extra space above keeps the block off the top of the viewport."
16
+ }
17
+ },
18
+ "padding-inline": {
19
+ "compact": {
20
+ "$value": "{space.4}",
21
+ "$description": "16px. Enough that a compact row doesn't touch the cell or popup edge."
22
+ },
23
+ "standard": {
24
+ "$value": "{space.6}",
25
+ "$description": "24px. A pane can be wide; the inline padding keeps the block off the sides before the description's own cap does."
26
+ },
27
+ "spacious": {
28
+ "$value": "{space.8}",
29
+ "$description": "32px. A full page, a step wider than a pane."
30
+ }
31
+ },
32
+ "gap": {
33
+ "compact": {
34
+ "$value": "{space.3}",
35
+ "$description": "12px between the icon, the text and the actions in a table row or a popup. Tight, because the row is short."
36
+ },
37
+ "standard": {
38
+ "$value": "{space.4}",
39
+ "$description": "16px. The stack in a pane: media, then words, then actions."
40
+ },
41
+ "spacious": {
42
+ "$value": "{space.6}",
43
+ "$description": "24px. A page-sized stack needs the steps to separate, or the circle sits on the title."
44
+ }
45
+ },
46
+ "text-gap": {
47
+ "compact": {
48
+ "$value": "{space.1}",
49
+ "$description": "4px between the title and the description in a compact row. They read as one message."
50
+ },
51
+ "standard": {
52
+ "$value": "{space.1}",
53
+ "$description": "4px. Same as compact: the description belongs to the title, so it stays close."
54
+ },
55
+ "spacious": {
56
+ "$value": "{space.2}",
57
+ "$description": "8px. The page title is larger, so the description steps down a little further."
58
+ }
59
+ },
60
+ "media-size": {
61
+ "standard": {
62
+ "$value": "{space.12}",
63
+ "$description": "48px circle around a magnify icon (20px). The icon needs a field, or it reads as a bullet."
64
+ },
65
+ "spacious": {
66
+ "$value": "{space.16}",
67
+ "$description": "64px circle around a maximize icon (24px). A page can carry a larger mark. Compact has no circle: the icon is icon-size.condense on its own."
68
+ }
69
+ },
70
+ "description-max-width": {
71
+ "$value": "{measure.narrow}",
72
+ "$description": "45ch. A description is one sentence. Past a caption's measure it stretches across a wide pane and stops being a sentence."
73
+ },
74
+ "actions-gap": {
75
+ "$value": "{space.2}",
76
+ "$description": "8px between actions, the same gap a control uses between its icon and its label. The first action is the main one; the rest sit beside it."
77
+ }
78
+ }
79
+ }
@@ -72,6 +72,10 @@
72
72
  "opacity-disabled": {
73
73
  "$value": "0.6",
74
74
  "$description": "A disabled field fades as a whole, label included."
75
+ },
76
+ "affix-color": {
77
+ "$value": "{text.secondary}",
78
+ "$description": "A prefix or suffix drawn inside the field (a currency symbol, a unit). Muted so it reads as part of the control, not as the value."
75
79
  }
76
80
  }
77
81
  }
@@ -78,8 +78,8 @@
78
78
  "$description": "Space between a group label and its first row."
79
79
  },
80
80
  "empty-padding-block": {
81
- "$value": "{space.6}",
82
- "$description": "Room around a 'No results' message, so an empty list doesn't collapse to a sliver."
81
+ "$value": "{empty.padding-block.compact}",
82
+ "$description": "Room around a plain 'No results' line. The same padding as a compact empty state, so a sentence and an EmptyState in that slot take the same space. An EmptyState drops this padding and uses its own."
83
83
  },
84
84
  "min-width": {
85
85
  "$value": "200px",
@@ -41,6 +41,14 @@
41
41
  "$value": "{item.background-selected}",
42
42
  "$description": "Selected rows. Same tint as the chosen menu row."
43
43
  },
44
+ "row-current": {
45
+ "$value": "{surface.tray}",
46
+ "$description": "The open row beside a detail pane. The tray, a step off the page, so secondary text on it still passes AA in dark. A selected row keeps its own tint, so a checked row and the row you are reading can both show."
47
+ },
48
+ "group-header-background": {
49
+ "$value": "{surface.tray}",
50
+ "$description": "A group header. The tray, like the column header, so a group reads as chrome rather than another data row."
51
+ },
44
52
  "column-hover": {
45
53
  "$value": "{fill.subtle}",
46
54
  "$description": "Column under the pointer, when column hover is on. Neutral so it never reads as a selection."
@@ -578,6 +578,43 @@ function paint(level, dark, palette) {
578
578
  return Object.fromEntries(Object.entries(STYLES[style]).map(([part, slot]) => [part, slot === 'transparent' ? 'transparent' : scopeHex(palette ?? color, slot, dark)]));
579
579
  }
580
580
 
581
+ /**
582
+ * One week in the sample: a selected start, a range band, today, and a selected end.
583
+ * accent soft-active, ink and border are the same steps as interactive.tint-active,
584
+ * text.on-tint-strong and border.accent. The ring width is stroke.thin, which stroke.default points at.
585
+ */
586
+ function weekRow(dark, accent, slot, text, muted) {
587
+ const selected = paint('bold', dark);
588
+ const band = slot(accent, 'soft-active');
589
+ const onBand = slot(accent, 'ink');
590
+ const ring = slot(accent, 'border');
591
+ const ringPx = pxAt(state.result?.files?.stroke?.stroke?.thin?.$value ?? 1, 0) || 1;
592
+ const days = [
593
+ { n: '18', kind: 'out' },
594
+ { n: '19', kind: 'start' },
595
+ { n: '20', kind: 'mid' },
596
+ { n: '21', kind: 'today' },
597
+ { n: '22', kind: 'mid' },
598
+ { n: '23', kind: 'end' },
599
+ { n: '24', kind: 'rest' },
600
+ ];
601
+ return days.map((day) => {
602
+ if (day.kind === 'start' || day.kind === 'end') {
603
+ return `<span class="rday" data-uses="${usesOfBold()}" title="Selected day: bold" style="background:${selected.bg};color:${selected.fg}">${day.n}</span>`;
604
+ }
605
+ if (day.kind === 'mid' || day.kind === 'today') {
606
+ const edge = day.kind === 'today' ? `box-shadow:inset 0 0 0 ${ringPx}px ${ring}` : '';
607
+ return `<span class="rday mid" data-uses="accent" title="${day.kind === 'today' ? 'Today, inside the range' : 'In range'}" style="background:${band};color:${onBand};${edge}">${day.n}</span>`;
608
+ }
609
+ return `<span class="rday" style="color:${day.kind === 'out' ? muted : text}">${day.n}</span>`;
610
+ }).join('');
611
+ }
612
+
613
+ function usesOfBold() {
614
+ const r = withDefaults(state.brand.roles);
615
+ return `bold ${r.emphasis.bold.color}`;
616
+ }
617
+
581
618
  function rolesScene(dark) {
582
619
  const r = withDefaults(state.brand.roles);
583
620
  const ch = r.chrome, ac = r.accent;
@@ -614,12 +651,14 @@ function rolesScene(dark) {
614
651
  <div class="rb-row">${btn('bold', 'Open', { on: raised })}${btn('standard', 'Edit', { on: raised })}${btn('subtle', 'Copy', { on: raised })}${btn('quiet', 'Archive', { on: raised })}</div></div>
615
652
  <div class="rform">
616
653
  <span class="ri" data-uses="chrome" style="border-color:${field};color:${muted}">Search surveys</span>
654
+ <span class="ri" data-uses="chrome" style="border-color:${field};color:${text}"><span style="color:${muted}">$</span>1,250.00</span>
617
655
  <span class="ri" data-uses="accent" style="border-color:${field};outline:2px solid ${slot(ac, 'focus')};outline-offset:2px;color:${text}">Focused field</span>
618
656
  <span class="rb-row">
619
657
  <span class="rcheck-row" data-uses="accent"><i class="rcheck" style="background:${slot(ac, 'solid')};color:${slot(ac, 'on-solid')}">✓</i>Checked</span>
620
658
  <span class="rcheck-row" data-uses="accent"><i class="rswitch" style="background:${slot(ac, 'solid')}"><i style="background:${slot(ac, 'on-solid')}"></i></i>On</span>
621
659
  <span class="rcheck-row" data-uses="chrome"><i class="rswitch off" style="background:${slot(ch, 'soft-active')}"><i style="background:${page}"></i></i>Off</span>
622
660
  </span>
661
+ <div class="rweek" aria-hidden="true">${weekRow(dark, ac, slot, text, muted)}</div>
623
662
  </div>
624
663
  <div class="rmenu" data-uses="chrome" style="background:${raised};border-color:${line}">
625
664
  <span>Rename</span><span data-uses="${uses('subtle')}" style="background:${hl.bg};color:${hl.fg}" title="Highlighted item: the subtle level">Duplicate</span><span>Move to…</span>
@@ -629,6 +668,12 @@ function rolesScene(dark) {
629
668
  ${['error', 'success'].filter((p) => state.brand.color.palettes[p]).map((p) => `<div class="ralert" data-uses="palette" style="background:${slot(p, 'soft')};color:${slot(p, 'on-soft')};border-color:${slot(p, 'border-subtle')}">
630
669
  <b>${p === 'error' ? 'Upload failed' : 'Saved'}</b> ${p === 'error' ? 'The file is over 20 MB.' : 'Your changes are live.'}</div>`).join('')}
631
670
  <div class="rladder">${tag('Emphasis ladder')}<div class="rb-row">${LEVELS.map((l) => btn(l, l, { rate: true })).join('')}</div></div>
671
+ <div class="rladder">${tag('Status badges, subtle')}<div class="rb-row">${['info', 'success', 'warning', 'error'].filter((p) => state.brand.color.palettes[p]).map((p) => { const e = paint('subtle', dark, p); return `<span class="rbadge" data-uses="${uses('subtle')} palette ${p}" title="subtle ${p}" style="background:${e.bg};color:${e.fg};border-color:${e.border}">${esc(p)}</span>`; }).join('')}</div></div>
672
+ <div class="rempty" data-uses="chrome" style="color:${text}">
673
+ <i data-uses="${uses('subtle')}" style="background:${hl.bg};color:${hl.fg}" title="Empty state media: the subtle level"><svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path fill="currentColor" d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5v-4.99h3.42c.77 1.76 2.54 3 4.58 3s3.81-1.24 4.58-3H19V19z"/></svg></i>
674
+ <b>No surveys yet</b>
675
+ ${btn('bold', 'New survey')}
676
+ </div>
632
677
  <div class="rladder">${tag('Bold in every palette (data-color)')}<div class="rb-row">${spots.map((p) => btn('bold', p, { palette: p, rate: true })).join('')}</div></div>
633
678
  </div>`;
634
679
  }
@@ -212,6 +212,12 @@ body[data-vision="achromat"] .preview { filter: url(#sim-achromat); }
212
212
  .rb-wrap { display: grid; gap: 2px; justify-items: center; }
213
213
  .rb-wrap small { font-size: 0.72rem; font-family: var(--mono); }
214
214
  .rb { display: inline-block; padding: 6px 14px; border: 1px solid; border-radius: 6px; font-weight: 600; white-space: nowrap; }
215
+ .rbadge { display: inline-flex; align-items: center; height: 20px; padding: 0 8px; border: 1px solid; border-radius: 999px; font: 500 0.75rem/1 var(--body); white-space: nowrap; }
216
+ .rweek { display: grid; grid-template-columns: repeat(7, 28px); gap: 2px; }
217
+ .rday { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 999px; font: 600 0.75rem/1 var(--body); box-sizing: border-box; }
218
+ .rday.mid { border-radius: 0; }
219
+ .rempty { display: grid; justify-items: center; gap: 8px; padding: 8px 0; text-align: center; }
220
+ .rempty i { display: grid; place-items: center; width: 40px; height: 40px; border-radius: 999px; font: 600 12px/1 var(--body); }
215
221
  .ri { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; min-width: 110px; border: 1px solid; border-radius: 6px; }
216
222
  .rcheck { display: inline-grid; place-items: center; width: 16px; height: 16px; border-radius: 4px; font: normal 600 11px/1 var(--body); }
217
223
  .rtabs { display: flex; gap: 18px; border-bottom: 1px solid; }