@scattered-light/loomiere 0.0.0-stage → 0.1.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 (73) hide show
  1. package/README.md +60 -2
  2. package/bin/loomiere.mjs +112 -0
  3. package/build/build-tokens.mjs +485 -0
  4. package/exporters/README.md +45 -0
  5. package/exporters/ag-grid-settings.mjs +177 -0
  6. package/exporters/ag-grid.mjs +112 -0
  7. package/exporters/chakra.mjs +630 -0
  8. package/exporters/component-styles.mjs +207 -0
  9. package/exporters/dtcg.mjs +31 -0
  10. package/exporters/figma.mjs +100 -0
  11. package/exporters/index.mjs +38 -0
  12. package/exporters/mui.mjs +666 -0
  13. package/exporters/tailwind.mjs +35 -0
  14. package/exporters/theme.mjs +93 -0
  15. package/generator/brand.mjs +328 -0
  16. package/generator/brand.schema.json +657 -0
  17. package/generator/color.mjs +130 -0
  18. package/generator/data-colors.mjs +104 -0
  19. package/generator/format-brand.mjs +60 -0
  20. package/generator/generate.mjs +43 -0
  21. package/generator/pairs.mjs +236 -0
  22. package/generator/roles.mjs +258 -0
  23. package/generator/vision.mjs +167 -0
  24. package/lib/config.mjs +71 -0
  25. package/lib/contrast.mjs +134 -0
  26. package/lib/lab.mjs +108 -0
  27. package/lib/studio.mjs +127 -0
  28. package/lib/tool-server.mjs +96 -0
  29. package/package.json +51 -4
  30. package/templates/brand.json +104 -0
  31. package/tokens/README.md +219 -0
  32. package/tokens/component/avatar.json +10 -0
  33. package/tokens/component/button.json +12 -0
  34. package/tokens/component/drawer.json +26 -0
  35. package/tokens/component/navigation-menu.json +13 -0
  36. package/tokens/component/popover.json +13 -0
  37. package/tokens/component/preview-card.json +13 -0
  38. package/tokens/component/slider.json +13 -0
  39. package/tokens/component/switch.json +41 -0
  40. package/tokens/component/toast.json +21 -0
  41. package/tokens/component/toggle.json +9 -0
  42. package/tokens/component/toolbar.json +9 -0
  43. package/tokens/component/tooltip.json +9 -0
  44. package/tokens/foundation/layering.json +57 -0
  45. package/tokens/foundation/layout.json +75 -0
  46. package/tokens/foundation/motion.json +41 -0
  47. package/tokens/foundation/type.json +92 -0
  48. package/tokens/semantic/accessibility.json +39 -0
  49. package/tokens/semantic/bar.json +45 -0
  50. package/tokens/semantic/check.json +57 -0
  51. package/tokens/semantic/color.json +270 -0
  52. package/tokens/semantic/content.json +124 -0
  53. package/tokens/semantic/disclosure.json +49 -0
  54. package/tokens/semantic/elevation.json +72 -0
  55. package/tokens/semantic/field.json +77 -0
  56. package/tokens/semantic/focus.json +36 -0
  57. package/tokens/semantic/gauge.json +45 -0
  58. package/tokens/semantic/icon-button.json +33 -0
  59. package/tokens/semantic/icon.json +30 -0
  60. package/tokens/semantic/item.json +65 -0
  61. package/tokens/semantic/modal.json +86 -0
  62. package/tokens/semantic/motion.json +20 -0
  63. package/tokens/semantic/notice.json +37 -0
  64. package/tokens/semantic/popup.json +89 -0
  65. package/tokens/semantic/scheme.json +11 -0
  66. package/tokens/semantic/shape.json +54 -0
  67. package/tokens/semantic/spacing.json +110 -0
  68. package/tokens/semantic/table.json +73 -0
  69. package/tokens/semantic/typography.json +312 -0
  70. package/tools/brand-builder/builder.js +1119 -0
  71. package/tools/brand-builder/index.html +329 -0
  72. package/tools/pair-lab/index.html +119 -0
  73. package/tools/pair-lab/lab.js +249 -0
@@ -0,0 +1,329 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <meta charset="utf-8">
4
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
5
+ <title>Brand Builder</title>
6
+ <meta name="claude:viewport-fit-sides" content="cover">
7
+ <link rel="icon" href="data:,">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Barlow+Semi+Condensed:wght@400;500;600;700&family=Funnel+Display:wght@500;600;700&family=Roboto+Mono:wght@400;500&display=swap">
10
+ <style>
11
+ /* Layout: a sticky top bar (brand, pins, save), a section rail on wide screens, and one band per
12
+ brand-file section with its inputs on the left and a live preview on the right. The chrome
13
+ wears Mojave itself: stone neutrals, mirage accent, Funnel Display over Barlow Semi Condensed. */
14
+ :root {
15
+ --bg: #f8fbf9; --surface: #ffffff; --sunk: #eff3ee; --fg: #181e1a; --muted: #4f5852;
16
+ --line: #e7ece4; --strong: #7c8980; --accent: #883ac3; --on-accent: #ffffff; --warn: #9a4a00; --bad: #b42318;
17
+ --display: 'Funnel Display', 'Barlow Semi Condensed', system-ui, sans-serif;
18
+ --body: 'Barlow Semi Condensed', system-ui, sans-serif;
19
+ --mono: 'Roboto Mono', ui-monospace, monospace;
20
+ --checker: repeating-conic-gradient(#e7ece4 0 25%, #ffffff 0 50%) 0 0 / 12px 12px;
21
+ }
22
+ @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) {
23
+ --bg: #121714; --surface: #1b211d; --sunk: #232a25; --fg: #eff3ee; --muted: #a5b4aa;
24
+ --line: #2f3732; --strong: #7c8980; --accent: #d4a2fc; --on-accent: #181e1a; --warn: #f5b05a; --bad: #ff8a80;
25
+ --checker: repeating-conic-gradient(#2f3732 0 25%, #1b211d 0 50%) 0 0 / 12px 12px; color-scheme: dark } }
26
+ :root[data-theme="dark"] {
27
+ --bg: #121714; --surface: #1b211d; --sunk: #232a25; --fg: #eff3ee; --muted: #a5b4aa;
28
+ --line: #2f3732; --strong: #7c8980; --accent: #d4a2fc; --on-accent: #181e1a; --warn: #f5b05a; --bad: #ff8a80;
29
+ --checker: repeating-conic-gradient(#2f3732 0 25%, #1b211d 0 50%) 0 0 / 12px 12px; color-scheme: dark }
30
+
31
+ * { box-sizing: border-box; }
32
+ [hidden] { display: none !important; }
33
+ html { scroll-padding-top: 132px; }
34
+ body { margin: 0; background: var(--bg); color: var(--fg); font: 400 16px/1.45 var(--body); }
35
+ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-area-inset-top, 0px); z-index: 10; background: var(--surface); }
36
+ h1, h2, h3 { font-family: var(--display); margin: 0; text-wrap: balance; }
37
+ h2 { font-size: 1.5rem; font-weight: 600; letter-spacing: -0.01em; }
38
+ h3 { font-size: 1.05rem; font-weight: 600; }
39
+ p { margin: 0; }
40
+ code, .mono { font-family: var(--mono); font-size: 0.85em; }
41
+ .muted { color: var(--muted); }
42
+ button, input, select, textarea { font: inherit; color: inherit; }
43
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
44
+
45
+ .bar { position: sticky; top: env(safe-area-inset-top, 0px); z-index: 5; background: var(--surface); border-bottom: 1px solid var(--line);
46
+ padding-left: max(16px, env(safe-area-inset-left, 0px)); padding-right: max(16px, env(safe-area-inset-right, 0px)); padding-block: 10px; }
47
+ .bar-in { max-width: 1440px; margin: 0 auto; display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px; }
48
+ .bar h1 { font-size: 1.3rem; font-weight: 700; letter-spacing: -0.01em; margin-right: auto; display: flex; gap: 10px; align-items: baseline; }
49
+ .bar h1 small { font: 500 0.8rem var(--mono); color: var(--muted); }
50
+ .dirty { color: var(--accent); }
51
+ .bar-tools { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
52
+ .bar-tools select { width: auto; }
53
+ .status { font-size: 0.9rem; color: var(--muted); min-width: 0; flex-basis: 100%; text-align: right; }
54
+ .status.bad { color: var(--bad); }
55
+ .status:empty { display: none; }
56
+
57
+ .btn { font: 600 0.95rem var(--body); padding: 7px 16px; border-radius: 999px; border: 1px solid var(--line); background: transparent; color: var(--fg); cursor: pointer; white-space: nowrap; }
58
+ .btn:hover { border-color: var(--strong); }
59
+ .btn.primary { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
60
+ .btn:disabled { opacity: .5; cursor: not-allowed; }
61
+ .btn.small { font-size: 0.8rem; padding: 3px 10px; }
62
+ .icon-btn { width: 30px; height: 30px; border-radius: 8px; border: 1px solid var(--line); background: transparent; cursor: pointer; color: var(--muted); line-height: 1; flex: none; }
63
+ .icon-btn:hover { color: var(--fg); border-color: var(--strong); }
64
+ .toggle { display: inline-flex; gap: 6px; align-items: center; font-size: 0.9rem; cursor: pointer; }
65
+
66
+ .page { max-width: 1440px; margin: 0 auto; display: grid; grid-template-columns: 180px minmax(0, 1fr); gap: 32px;
67
+ padding-left: max(16px, env(safe-area-inset-left, 0px)); padding-right: max(16px, env(safe-area-inset-right, 0px)); padding-block: 24px 80px; }
68
+ .rail { position: sticky; top: calc(env(safe-area-inset-top, 0px) + 128px); align-self: start; display: grid; gap: 2px; }
69
+ .rail a { color: var(--muted); text-decoration: none; padding: 5px 10px; border-radius: 8px; font-weight: 500; }
70
+ .rail a:hover { color: var(--fg); background: var(--sunk); }
71
+ .rail .rail-label { font: 500 0.7rem var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--strong); padding: 12px 10px 4px; }
72
+ main { display: grid; gap: 28px; min-width: 0; }
73
+
74
+ .band { display: grid; gap: 16px; padding-bottom: 28px; border-bottom: 1px solid var(--line); min-width: 0; }
75
+ .band-head { display: grid; gap: 4px; max-width: 72ch; }
76
+ .band-body { display: grid; grid-template-columns: minmax(280px, 380px) minmax(0, 1fr); gap: 20px; align-items: start; }
77
+ .edit { display: grid; gap: 14px; min-width: 0; }
78
+ .preview { min-width: 0; display: grid; gap: 14px; }
79
+ .panel { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; padding: 14px; display: grid; gap: 12px; min-width: 0; }
80
+
81
+ .field { display: grid; gap: 4px; min-width: 0; }
82
+ .field > span, .lbl { font: 500 0.72rem var(--mono); letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }
83
+ .field small { color: var(--muted); font-size: 0.82rem; }
84
+ .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; min-width: 0; }
85
+ .row > .field { flex: 1 1 110px; }
86
+ input[type=text], input[type=number], select, textarea { width: 100%; min-width: 0; padding: 6px 8px; border-radius: 8px; border: 1px solid var(--line); background: var(--bg); }
87
+ input[type=number] { font-variant-numeric: tabular-nums; }
88
+ input.hex { font-family: var(--mono); font-size: 0.85rem; width: 92px; flex: none; }
89
+ input[type=color] { width: 34px; height: 30px; padding: 0; border: 1px solid var(--line); border-radius: 8px; background: none; cursor: pointer; flex: none; }
90
+ input[type=range] { width: 100%; accent-color: var(--accent); }
91
+ input[type=checkbox] { accent-color: var(--accent); width: 16px; height: 16px; }
92
+ .color-in { display: flex; gap: 6px; align-items: center; }
93
+ .list { display: grid; gap: 6px; }
94
+ .map-row { display: grid; grid-template-columns: minmax(0, 1fr) 90px 30px; gap: 6px; align-items: center; }
95
+ .map-row.locks { grid-template-columns: minmax(0, 1fr) 90px auto 30px; }
96
+
97
+ .notice { border: 1px solid var(--line); border-left: 3px solid var(--warn); background: var(--surface); border-radius: 8px; padding: 10px 12px; font-size: 0.92rem; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
98
+ .notice.bad { border-left-color: var(--bad); }
99
+ .notice p { flex: 1 1 260px; }
100
+
101
+ /* Ramps */
102
+ .palettes { display: grid; gap: 16px; }
103
+ .pal { display: grid; grid-template-columns: minmax(280px, 380px) minmax(0, 1fr); gap: 20px; align-items: start; }
104
+ .pal-head { display: flex; align-items: baseline; gap: 8px; }
105
+ .pal-head .role { font: 500 0.75rem var(--mono); color: var(--muted); }
106
+ .ramp-wrap { overflow-x: auto; }
107
+ .ramp { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(52px, 1fr); border-radius: 10px; overflow: hidden; border: 1px solid var(--line); min-width: max-content; }
108
+ .sw { min-height: 92px; padding: 7px 6px; display: grid; align-content: space-between; gap: 6px; font-variant-numeric: tabular-nums; position: relative; }
109
+ .sw b { font: 600 0.95rem var(--display); }
110
+ .sw .r { font: 500 0.68rem var(--mono); }
111
+ .sw .h { font: 400 0.62rem var(--mono); opacity: .85; }
112
+ .sw .tag { position: absolute; top: 6px; right: 6px; font: 600 0.58rem var(--mono); letter-spacing: 0.04em; }
113
+ .ends { display: flex; gap: 14px; flex-wrap: wrap; font: 400 0.8rem var(--mono); color: var(--muted); margin-top: 6px; }
114
+ .ends i { display: inline-block; width: 10px; height: 10px; border-radius: 3px; border: 1px solid var(--line); vertical-align: -1px; margin-right: 4px; }
115
+
116
+ /* Contrast tables */
117
+ details.panel > summary, details.more > summary { cursor: pointer; display: flex; gap: 10px; align-items: baseline; list-style: none; }
118
+ details.panel > summary::-webkit-details-marker, details.more > summary::-webkit-details-marker { display: none; }
119
+ details.panel > summary::before, details.more > summary::before { content: "▸"; color: var(--muted); }
120
+ details[open].panel > summary::before, details[open].more > summary::before { content: "▾"; }
121
+ details.more > summary { font: 500 0.72rem var(--mono); letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }
122
+ details.more[open] > summary { margin-bottom: 10px; }
123
+ .curve { width: 100%; height: auto; display: block; margin-top: 8px; }
124
+ .curve text { fill: var(--muted); font: 400 10px var(--mono); }
125
+ .curve .tick { stroke: var(--line); }
126
+ .scale-svg { width: 100%; height: auto; display: block; }
127
+ .scale-svg text { fill: var(--muted); font: 500 10px var(--mono); }
128
+ .scale-svg .tick { stroke: var(--line); }
129
+ .scale-svg .lvl { stroke: var(--strong); stroke-dasharray: 3 3; }
130
+
131
+ /* Alerts */
132
+ .alerts { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 10px; }
133
+ .alert { border-radius: 10px; padding: 12px 14px; border: 1px solid; display: grid; gap: 4px; }
134
+ .alert b { font: 600 1rem var(--display); }
135
+ .alert span { font-size: 0.92rem; }
136
+
137
+ /* Data */
138
+ .chips { display: flex; flex-wrap: wrap; gap: 6px; }
139
+ .chip { display: grid; gap: 2px; width: 74px; font: 400 0.68rem var(--mono); color: var(--muted); }
140
+ .chip i { height: 34px; border-radius: 6px; border: 1px solid var(--line); }
141
+ .chip.flag i { outline: 2px solid var(--warn); outline-offset: 1px; }
142
+ .chart text { fill: var(--muted); font: 400 10px var(--mono); }
143
+ .chart .grid { stroke: var(--line); }
144
+ .warn-list { display: grid; gap: 4px; font-size: 0.88rem; color: var(--muted); padding: 0; margin: 0; list-style: none; }
145
+ .warn-list li { display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: center; }
146
+ .warn-list li > span:first-child::before { content: "▲ "; color: var(--warn); font-size: 0.7em; }
147
+ .fix { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px 2px 4px; border: 1px solid var(--line); border-radius: 999px;
148
+ background: var(--surface); color: var(--fg); font: 500 0.75rem var(--mono); cursor: pointer; }
149
+ .fix:hover { border-color: var(--strong); }
150
+ .fix i { width: 14px; height: 14px; border-radius: 50%; border: 1px solid var(--line); }
151
+ /* Color vision simulation (Machado et al. 2009, full strength), applied to previews only. */
152
+ body[data-vision="protan"] .preview { filter: url(#sim-protan); }
153
+ body[data-vision="deutan"] .preview { filter: url(#sim-deutan); }
154
+ body[data-vision="tritan"] .preview { filter: url(#sim-tritan); }
155
+ body[data-vision="achromat"] .preview { filter: url(#sim-achromat); }
156
+ .sim-defs { position: absolute; width: 0; height: 0; }
157
+
158
+ /* Type */
159
+ .presets { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 10px; }
160
+ .preset { border: 1px solid var(--line); border-radius: 10px; padding: 10px; display: grid; gap: 8px; background: var(--surface); min-width: 0; }
161
+ .preset.on-min, .preset.on-max { border-color: var(--accent); }
162
+ .preset .ladder { display: grid; gap: 4px; align-content: end; flex: 1; overflow-x: hidden; }
163
+ .preset { grid-template-rows: auto 1fr auto; }
164
+ .preset .ladder span { white-space: nowrap; line-height: 1.05; }
165
+ .preset .name { font: 600 0.9rem var(--body); display: flex; justify-content: space-between; gap: 6px; }
166
+ .preset .name em { font: 400 0.75rem var(--mono); color: var(--muted); font-style: normal; }
167
+ .preset .row .btn[aria-pressed="true"] { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
168
+ .type-table { width: 100%; border-collapse: collapse; }
169
+ .steps { flex-basis: 100%; display: flex; flex-wrap: wrap; gap: 6px; }
170
+ .step { font: 600 0.9rem var(--body); padding: 6px 14px 6px 8px; border-radius: 999px; border: 1px solid var(--line); background: transparent; color: var(--muted); cursor: pointer; display: inline-flex; gap: 8px; align-items: center; }
171
+ .step b { display: inline-grid; place-items: center; width: 22px; height: 22px; border-radius: 50%; background: var(--sunk); color: var(--fg); font-size: 0.8rem; }
172
+ .step.on { color: var(--fg); border-color: var(--accent); }
173
+ .step.on b { background: var(--accent); color: var(--on-accent); }
174
+ .step-end { display: flex; justify-content: space-between; gap: 12px; padding-block: 8px 32px; }
175
+ .palette-strip { display: flex; flex-wrap: wrap; gap: 6px 12px; font-size: 0.8rem; color: var(--muted); }
176
+ .palette-strip span { display: inline-flex; gap: 5px; align-items: center; }
177
+ .palette-strip i { width: 14px; height: 14px; border-radius: 4px; }
178
+ .scenes { display: grid; gap: 12px; }
179
+ /* The role settings stay in view while the sample screens scroll past. */
180
+ .ctable-wrap { overflow-x: auto; }
181
+ .ctable { border-collapse: collapse; width: 100%; min-width: 760px; font-size: 13px; }
182
+ .ctable th, .ctable td { text-align: left; vertical-align: top; padding: 8px 10px; border-bottom: 1px solid var(--line); }
183
+ .ctable thead th { font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
184
+ .ctable tbody th { width: 220px; font-weight: 400; }
185
+ .ctable tbody th b { display: block; }
186
+ .ctable tbody th code { display: block; color: var(--muted); font-size: 12px; margin-bottom: 4px; }
187
+ .ctable .cgroup th { padding-top: 18px; font-weight: 600; border-bottom: 0; }
188
+ .cswatch { width: 240px; }
189
+ .cswatch > i { float: left; width: 44px; height: 44px; border-radius: 8px; margin-right: 10px; border: 6px solid var(--cbg); box-shadow: 0 0 0 1px var(--line); }
190
+ .cswatch > span { display: grid; gap: 4px; }
191
+ .cpick { display: flex; gap: 6px; flex-wrap: wrap; }
192
+ .cpick select { max-width: 150px; }
193
+ .chex { width: 84px; font-family: ui-monospace, monospace; }
194
+ .cused code { display: inline-block; font-size: 11.5px; margin: 0 4px 4px 0; }
195
+ #roles .edit { position: sticky; top: calc(env(safe-area-inset-top, 0px) + 128px); }
196
+ @media (max-width: 860px) { #roles .edit { position: static; } }
197
+ .uses { font: normal 500 0.7rem var(--mono); letter-spacing: 0.04em; text-transform: uppercase; }
198
+ .rapp-bar { display: flex; flex-wrap: wrap; gap: 12px 18px; align-items: center; padding: 10px 14px; border: 1px solid; border-radius: 10px; }
199
+ .rapp-bar nav { display: flex; gap: 14px; margin-right: auto; font-weight: 600; }
200
+ .rform { display: grid; gap: 10px; align-content: start; }
201
+ .rcheck-row { display: inline-flex; gap: 6px; align-items: center; }
202
+ .rswitch { position: relative; display: inline-block; width: 28px; height: 16px; border-radius: 999px; }
203
+ .rswitch i { position: absolute; top: 2px; left: 14px; width: 12px; height: 12px; border-radius: 50%; }
204
+ .rswitch.off i { left: 2px; }
205
+ .rladder { display: grid; gap: 6px; }
206
+ @keyframes flash { 0%, 40% { box-shadow: 0 0 0 3px var(--accent); } 100% { box-shadow: 0 0 0 3px transparent; } }
207
+ .flash { animation: flash 1.2s ease-out; border-radius: 6px; }
208
+ @media (prefers-reduced-motion: reduce) { .flash { animation: none; outline: 2px solid var(--accent); } }
209
+ .scene { display: grid; gap: 14px; padding: 16px; border-radius: 12px; border: 1px solid var(--line); min-width: 0; }
210
+ .scene-label { font: 600 0.75rem var(--body); letter-spacing: 0.06em; text-transform: uppercase; }
211
+ .rb-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-start; }
212
+ .rb-wrap { display: grid; gap: 2px; justify-items: center; }
213
+ .rb-wrap small { font-size: 0.72rem; font-family: var(--mono); }
214
+ .rb { display: inline-block; padding: 6px 14px; border: 1px solid; border-radius: 6px; font-weight: 600; white-space: nowrap; }
215
+ .ri { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; min-width: 110px; border: 1px solid; border-radius: 6px; }
216
+ .rcheck { display: inline-grid; place-items: center; width: 16px; height: 16px; border-radius: 4px; font: normal 600 11px/1 var(--body); }
217
+ .rtabs { display: flex; gap: 18px; border-bottom: 1px solid; }
218
+ .rtabs span { padding: 6px 0; border-bottom: 2px solid; margin-bottom: -1px; font-weight: 600; }
219
+ .rgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
220
+ .rmenu { display: grid; padding: 4px; border: 1px solid; border-radius: 8px; align-content: start; }
221
+ .rmenu span { padding: 6px 10px; border-radius: 5px; }
222
+ .rcard { display: grid; gap: 6px; padding: 14px; border: 1px solid; border-radius: 10px; }
223
+ .ralert { padding: 10px 12px; border: 1px solid; border-radius: 8px; }
224
+ .level { min-width: 72px; align-self: center; text-transform: capitalize; }
225
+ .vcards { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; }
226
+ .vcard { display: grid; gap: 10px; align-content: start; padding: 14px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface); }
227
+ .vcard.open { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
228
+ .vcard header { display: grid; gap: 2px; }
229
+ .vcard header b { font: 600 1rem var(--display); }
230
+ .vcard-keys { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; }
231
+ .vcard-keys i { height: 26px; border-radius: 5px; }
232
+ .vcard .btn { justify-self: start; }
233
+ .vision-data { display: flex; height: 18px; border-radius: 5px; overflow: hidden; }
234
+ .vision-data i { flex: 1; }
235
+ .vision-detail { margin-top: 12px; }
236
+ .vtable { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
237
+ .vtable th, .vtable td { padding: 4px 8px; border-bottom: 1px solid var(--line); text-align: left; white-space: nowrap; }
238
+ .vtable thead th { font: 500 0.7rem var(--mono); text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); }
239
+ .vtable th[scope=row] { font-weight: 500; }
240
+ .vtable .vgroup { font: 500 0.7rem var(--mono); text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); padding-top: 14px; }
241
+ .sw-s { display: inline-block; width: 30px; height: 22px; border-radius: 4px; margin-right: 3px; vertical-align: middle; border: 1px solid var(--line); }
242
+ .metrics { width: 100%; border-collapse: collapse; font: 400 0.8rem var(--mono); }
243
+ .metrics td, .metrics th { padding: 4px 6px; border-bottom: 1px solid var(--line); text-align: left; }
244
+ .metrics th { font-weight: 500; color: var(--muted); }
245
+ .type-table td, .type-table th { padding: 6px 8px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: baseline; }
246
+ .type-table th { font: 500 0.7rem var(--mono); text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); }
247
+ .type-table td.n { font: 400 0.8rem var(--mono); color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
248
+ .type-table td.s { white-space: nowrap; max-width: 0; width: 100%; overflow: hidden; text-overflow: ellipsis; }
249
+ .pin { font: 600 0.6rem var(--mono); color: var(--accent); border: 1px solid currentColor; border-radius: 4px; padding: 0 4px; margin-left: 4px; }
250
+ .base-row td { background: var(--sunk); }
251
+ .table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: 10px; background: var(--surface); }
252
+
253
+ /* Space, shape, elevation, motion */
254
+ .bars { display: grid; gap: 4px; }
255
+ .bars div { display: grid; grid-template-columns: 52px 56px minmax(0, 1fr); gap: 8px; align-items: center; font: 400 0.75rem var(--mono); color: var(--muted); font-variant-numeric: tabular-nums; }
256
+ .bars i { height: 12px; background: var(--accent); border-radius: 2px; display: block; max-width: 100%; }
257
+ .tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 12px; }
258
+ .tile { display: grid; gap: 6px; font: 400 0.75rem var(--mono); color: var(--muted); }
259
+ .tile i { aspect-ratio: 1; max-width: 100%; background: var(--sunk); border: 1.5px solid var(--strong); display: block; }
260
+ .lines { display: grid; gap: 10px; }
261
+ .lines div { display: grid; grid-template-columns: 110px minmax(0, 1fr); gap: 10px; align-items: center; font: 400 0.75rem var(--mono); color: var(--muted); }
262
+ .lines i { display: block; background: var(--fg); }
263
+ .shadows { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 22px; padding: 18px 18px 40px; background: var(--sunk); border-radius: 12px; }
264
+ .shadows div { background: #ffffff; color: #181e1a; border-radius: 10px; height: 76px; display: grid; place-items: center; font: 500 0.8rem var(--mono); }
265
+ .track { position: relative; height: 26px; border-radius: 999px; background: var(--sunk); }
266
+ .track i { position: absolute; left: 4px; top: 4px; width: 18px; height: 18px; border-radius: 50%; background: var(--accent); transition-property: transform; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
267
+ .track.go i { transform: translateX(calc(var(--w) - 26px)); }
268
+ .motion-rows { display: grid; gap: 10px; }
269
+ .motion-rows div { display: grid; grid-template-columns: 110px minmax(0, 1fr); gap: 10px; align-items: center; font: 400 0.8rem var(--mono); color: var(--muted); }
270
+
271
+ /* Pairs */
272
+ .pairs { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; }
273
+ .pair { border-radius: 10px; padding: 12px; display: grid; gap: 4px; border: 1px solid var(--line); }
274
+ .pair b { font: 600 1rem var(--display); }
275
+ .pair span { font: 400 0.75rem var(--mono); }
276
+
277
+ /* File */
278
+ pre.json { margin: 0; padding: 12px; border-radius: 10px; background: var(--surface); border: 1px solid var(--line); font: 400 0.78rem/1.5 var(--mono); overflow: auto; max-height: 520px; }
279
+
280
+ @media (max-width: 1100px) {
281
+ .page { grid-template-columns: minmax(0, 1fr); }
282
+ .rail { display: none; }
283
+ }
284
+ @media (max-width: 860px) {
285
+ .band-body, .pal { grid-template-columns: minmax(0, 1fr); }
286
+ .status { text-align: left; }
287
+ }
288
+ @media (prefers-reduced-motion: reduce) { .track i { transition: none; } }
289
+ </style>
290
+
291
+ <header class="bar">
292
+ <div class="bar-in">
293
+ <h1>Brand builder <small id="file-name"></small></h1>
294
+ <div class="bar-tools">
295
+ <label class="toggle" title="Apply the exact values pinned in the overrides file"><input type="checkbox" id="pins" checked> <span id="pins-label">Pins</span></label>
296
+ <select id="vision-sim" aria-label="Show previews as seen with" title="Show previews as seen with a kind of color blindness">
297
+ <option value="typical">Typical vision</option>
298
+ <option value="protan">Red-blind</option>
299
+ <option value="deutan">Green-blind</option>
300
+ <option value="tritan">Blue-blind</option>
301
+ <option value="achromat">No color</option>
302
+ </select>
303
+ <select id="brand-pick" aria-label="Brand to edit"></select>
304
+ <button class="btn" id="copy" type="button">Copy JSON</button>
305
+ <button class="btn primary" id="save" type="button">Save</button>
306
+ </div>
307
+ <nav class="steps" id="steps" aria-label="Steps"></nav>
308
+ <p class="status" id="status" role="status"></p>
309
+ </div>
310
+ </header>
311
+
312
+ <svg class="sim-defs" aria-hidden="true" focusable="false"><defs>
313
+ <filter id="sim-protan"><feColorMatrix type="matrix" values="0.152286 1.052583 -0.204868 0 0 0.114503 0.786281 0.099216 0 0 -0.003882 -0.048116 1.051998 0 0 0 0 0 1 0"/></filter>
314
+ <filter id="sim-deutan"><feColorMatrix type="matrix" values="0.367322 0.860646 -0.227968 0 0 0.280085 0.672501 0.047413 0 0 -0.011820 0.042940 0.968881 0 0 0 0 0 1 0"/></filter>
315
+ <filter id="sim-tritan"><feColorMatrix type="matrix" values="1.255528 -0.076749 -0.178779 0 0 -0.078411 0.930809 0.147602 0 0 0.004733 0.691367 0.303900 0 0 0 0 0 1 0"/></filter>
316
+ <filter id="sim-achromat"><feColorMatrix type="matrix" values="0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0"/></filter>
317
+ </defs></svg>
318
+
319
+ <div class="page">
320
+ <nav class="rail" id="rail" aria-label="Sections"></nav>
321
+
322
+ <main id="main">
323
+ <p class="muted">Loading the brand…</p>
324
+ </main>
325
+ </div>
326
+
327
+ <script type="importmap">{ "imports": { "culori": "/vendor/culori/src/index.js" } }</script>
328
+ <script type="module" src="builder.js"></script>
329
+ </html>
@@ -0,0 +1,119 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <meta charset="utf-8">
4
+ <meta name="viewport" content="width=device-width, initial-scale=1">
5
+ <title>Pair Lab</title>
6
+ <link rel="icon" href="data:,">
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Barlow+Semi+Condensed:wght@400;500;600;700&family=Funnel+Display:wght@500;600;700&family=Roboto+Mono:wght@400;500&display=swap">
9
+ <style>
10
+ /* Pair lab, served by `loomiere lab`. Controls on top, a grid of live mockup cards, then the saved-pairs tray. */
11
+ :root {
12
+ --bg: #f8fbf9; --surface: #ffffff; --fg: #181e1a; --muted: #4f5852; --line: #e7ece4; --strong: #7c8980;
13
+ --accent: #883ac3; --on-accent: #ffffff;
14
+ --display: 'Funnel Display', 'Barlow Semi Condensed', system-ui, sans-serif;
15
+ --body: 'Barlow Semi Condensed', system-ui, sans-serif;
16
+ --mono: 'Roboto Mono', ui-monospace, monospace;
17
+ }
18
+ @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) {
19
+ --bg: #181e1a; --surface: #222924; --fg: #eff3ee; --muted: #a5b4aa; --line: #353d38; --strong: #7c8980;
20
+ --accent: #d4a2fc; --on-accent: #181e1a; color-scheme: dark } }
21
+ :root[data-theme="dark"] {
22
+ --bg: #181e1a; --surface: #222924; --fg: #eff3ee; --muted: #a5b4aa; --line: #353d38; --strong: #7c8980;
23
+ --accent: #d4a2fc; --on-accent: #181e1a; color-scheme: dark }
24
+ * { box-sizing: border-box; }
25
+ body { margin: 0; background: var(--bg); color: var(--fg); font: 400 16px/1.45 var(--body);
26
+ padding-left: max(16px, env(safe-area-inset-left, 0px)); padding-right: max(16px, env(safe-area-inset-right, 0px)); padding-block: 32px 64px; }
27
+ main { max-width: 1180px; margin: 0 auto; display: grid; gap: 28px; }
28
+ header { display: grid; gap: 10px; max-width: 70ch; }
29
+ h1 { font: 700 clamp(1.8rem, 1.4rem + 1.6vw, 2.6rem)/1.05 var(--display); margin: 0; letter-spacing: -0.02em; text-wrap: balance; }
30
+ h2 { font: 600 1.25rem/1.2 var(--display); margin: 0; }
31
+ p { margin: 0; color: var(--muted); }
32
+ code { font: 400 0.85em var(--mono); }
33
+ .controls { display: grid; gap: 14px; padding: 16px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface); }
34
+ .row { display: grid; grid-template-columns: 9rem 1fr; gap: 8px 14px; align-items: start; }
35
+ .row > .lbl { font: 500 0.75rem var(--mono); text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); padding-top: 7px; }
36
+ .chips { display: flex; flex-wrap: wrap; gap: 6px; min-width: 0; }
37
+ .chip { font: 500 0.9rem var(--body); padding: 4px 12px; border-radius: 999px; border: 1px solid var(--line); background: transparent; color: var(--fg); cursor: pointer; display: inline-flex; gap: 6px; align-items: center; }
38
+ .chip[aria-pressed="true"] { background: var(--fg); color: var(--bg); border-color: var(--fg); }
39
+ .chip .dot { width: 10px; height: 10px; border-radius: 50%; border: 1px solid rgba(127,127,127,.4); }
40
+ .steps { display: flex; flex-wrap: wrap; gap: 4px; }
41
+ .step { width: 34px; height: 30px; border-radius: 6px; border: 1px solid var(--line); cursor: pointer; font: 500 0.6rem var(--mono); display: grid; place-items: end center; padding-bottom: 2px; }
42
+ .step[aria-pressed="true"] { outline: 2px solid var(--fg); outline-offset: 2px; }
43
+ .step.any { width: auto; padding: 0 10px; place-items: center; background: transparent; color: var(--fg); font-size: 0.75rem; }
44
+ button:focus-visible, input:focus-visible, select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
45
+ .summary { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: baseline; }
46
+ .summary .meta { font: 400 0.85rem var(--mono); color: var(--muted); }
47
+ .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 14px; }
48
+ .card { border-radius: 14px; overflow: hidden; border: 1px solid var(--line); display: grid; background: var(--surface); }
49
+ .card.starred { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
50
+ .mock { padding: 18px 18px 16px; display: grid; gap: 10px; min-height: 172px; align-content: start; }
51
+ .mock .eyebrow { font: 600 0.7rem var(--body); letter-spacing: 0.1em; text-transform: uppercase; opacity: .85; }
52
+ .mock .head { font: 600 1.35rem/1.1 var(--display); text-wrap: balance; }
53
+ .mock .copy { font: 400 0.95rem/1.4 var(--body); }
54
+ .mock .acts { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
55
+ .mock .btn { font: 600 0.85rem var(--body); padding: 5px 14px; border-radius: 999px; border: 1.5px solid currentColor; }
56
+ .mock .badge { font: 600 0.7rem var(--mono); padding: 2px 8px; border-radius: 4px; }
57
+ .foot { display: grid; grid-template-columns: 1fr auto; gap: 4px 8px; padding: 10px 12px; border-top: 1px solid var(--line); align-items: center; }
58
+ .foot .name { font: 500 0.85rem var(--mono); color: var(--fg); overflow-wrap: anywhere; }
59
+ .foot .spec { font: 400 0.75rem var(--mono); color: var(--muted); grid-column: 1; }
60
+ .foot .tools { grid-row: 1 / span 2; grid-column: 2; display: flex; gap: 4px; }
61
+ .icon { width: 34px; height: 34px; border-radius: 8px; border: 1px solid var(--line); background: transparent; color: var(--fg); cursor: pointer; font-size: 1rem; line-height: 1; }
62
+ .icon[aria-pressed="true"] { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
63
+ .lvl { font: 600 0.7rem var(--mono); padding: 1px 6px; border-radius: 4px; border: 1px solid var(--strong); color: var(--fg); margin-left: 6px; }
64
+ .empty { padding: 28px; text-align: center; color: var(--muted); border: 1px dashed var(--line); border-radius: 12px; }
65
+ .tray { display: grid; gap: 12px; padding: 16px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface); }
66
+ .tray-list { display: grid; gap: 8px; }
67
+ .saved { display: grid; grid-template-columns: 54px minmax(0, 1fr) auto; gap: 10px; align-items: center; padding-block: 6px; border-bottom: 1px solid var(--line); }
68
+ .saved .opts { grid-column: 2 / -1; display: flex; flex-wrap: wrap; gap: 8px 16px; }
69
+ .saved label { display: inline-flex; gap: 6px; align-items: center; font: 400 0.8rem var(--mono); color: var(--muted); }
70
+ .saved select { font: 400 0.8rem var(--mono); padding: 4px 6px; border-radius: 6px; border: 1px solid var(--line); background: var(--bg); color: var(--fg); max-width: 100%; }
71
+ .saved .warn { grid-column: 2 / -1; font: 500 0.8rem var(--body); color: #b42318; }
72
+ .secondary { font: 600 0.95rem var(--body); padding: 8px 16px; border-radius: 999px; border: 1px solid var(--line); background: transparent; color: var(--fg); cursor: pointer; }
73
+ .primary:disabled { opacity: .5; cursor: not-allowed; }
74
+ .log { white-space: pre-wrap; }
75
+ .saved .sw { height: 36px; border-radius: 8px; display: grid; place-items: center; font: 600 0.9rem var(--display); border: 1px solid var(--line); }
76
+ .saved input { font: 500 0.9rem var(--mono); padding: 6px 8px; border-radius: 6px; border: 1px solid var(--line); background: var(--bg); color: var(--fg); width: 100%; min-width: 0; }
77
+ .saved .spec { font: 400 0.75rem var(--mono); color: var(--muted); }
78
+ pre { margin: 0; padding: 12px; border-radius: 8px; background: var(--bg); border: 1px solid var(--line); font: 400 0.8rem/1.5 var(--mono); overflow-x: auto; }
79
+ .tray .acts { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
80
+ .primary { font: 600 0.95rem var(--body); padding: 8px 16px; border-radius: 999px; border: 0; background: var(--accent); color: var(--on-accent); cursor: pointer; }
81
+ .status { font-size: 0.9rem; color: var(--muted); }
82
+ @media (max-width: 560px) { .row { grid-template-columns: 1fr; } .row > .lbl { padding-top: 0; } }
83
+ @media (prefers-reduced-motion: no-preference) { .card { transition: box-shadow .15s, border-color .15s; } }
84
+ </style>
85
+
86
+ <main>
87
+ <header>
88
+ <h1 id="title">Pair lab</h1>
89
+ <p>Pick a background, then look through every text color that passes. Star the pairs you'd use and hide the ones you wouldn't. Starred pairs get a name from the palette labels, which you can change. <strong>Save</strong> writes them to <code id="brand-file">the brand file</code> as <code>color.pairs</code> and rebuilds the tokens, so each one becomes a <code>.sl-pair-NAME</code> class.</p>
90
+ </header>
91
+
92
+ <section class="controls" aria-label="Filters">
93
+ <div class="row"><span class="lbl">Background</span><div class="chips" id="bg-pal"></div></div>
94
+ <div class="row"><span class="lbl">Background step</span><div class="steps" id="bg-step"></div></div>
95
+ <div class="row"><span class="lbl">Text from</span><div class="chips" id="fg-pal"></div></div>
96
+ <div class="row"><span class="lbl">At least</span><div class="chips" id="level"></div></div>
97
+ <div class="row"><span class="lbl">Show</span><div class="chips" id="show"></div></div>
98
+ </section>
99
+
100
+ <section style="display:grid;gap:14px">
101
+ <div class="summary"><h2 id="count"></h2><span class="meta" id="count-meta"></span></div>
102
+ <div class="grid" id="grid"></div>
103
+ </section>
104
+
105
+ <section class="tray" aria-label="Saved pairs">
106
+ <div class="summary"><h2>Your pairs</h2><span class="meta" id="saved-meta"></span></div>
107
+ <p>An <strong>accent</strong> colors links, buttons and focus rings inside the pair; without one they use the text color. <strong>Dark mode</strong> can switch to another starred pair, or keep the same colors.</p>
108
+ <div class="tray-list" id="saved"></div>
109
+ <pre id="json" aria-label="pairs block for the brand file"></pre>
110
+ <div class="acts">
111
+ <button class="primary" id="save" type="button">Save to brand file</button>
112
+ <button class="secondary" id="copy" type="button">Copy pairs block</button>
113
+ <span class="status log" id="status" role="status"></span>
114
+ </div>
115
+ </section>
116
+ </main>
117
+
118
+ <script src="lab.js"></script>
119
+ </html>