@oneli8/react 1.0.0-beta.10

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 (61) hide show
  1. package/AGENTS.md +16 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +8 -0
  4. package/PRINCIPLES.md +48 -0
  5. package/README.md +136 -0
  6. package/ai-context.json +264 -0
  7. package/package.json +66 -0
  8. package/skills/oneli8-figma-to-code/SKILL.md +25 -0
  9. package/skills/oneli8-icons/SKILL.md +36 -0
  10. package/skills/oneli8-ui/SKILL.md +52 -0
  11. package/src/atoms/Icon.js +34 -0
  12. package/src/atoms/NavigationBadge.js +19 -0
  13. package/src/atoms/SelectionIndicator.js +43 -0
  14. package/src/foundations/geometry.js +143 -0
  15. package/src/foundations/icons.generated.d.ts +48 -0
  16. package/src/foundations/icons.generated.js +562 -0
  17. package/src/index.d.ts +381 -0
  18. package/src/index.js +23 -0
  19. package/src/molecules/Button.js +50 -0
  20. package/src/molecules/ChoiceChip.js +101 -0
  21. package/src/molecules/ChoiceItem.js +55 -0
  22. package/src/molecules/FormMessage.js +33 -0
  23. package/src/molecules/IconButton.js +33 -0
  24. package/src/molecules/Link.js +31 -0
  25. package/src/molecules/NavigationItem.js +55 -0
  26. package/src/molecules/Select.js +109 -0
  27. package/src/molecules/SelectionOption.js +44 -0
  28. package/src/molecules/TextField.js +174 -0
  29. package/src/molecules/Token.js +49 -0
  30. package/src/organisms/ChoiceGroup.js +78 -0
  31. package/src/organisms/ChoicePicker.js +114 -0
  32. package/src/organisms/Combobox.js +122 -0
  33. package/src/organisms/MultiSelectField.js +138 -0
  34. package/src/organisms/SegmentedControl.js +114 -0
  35. package/src/organisms/SelectionPopup.js +73 -0
  36. package/src/organisms/TabBar.js +68 -0
  37. package/src/organisms/Tabs.js +123 -0
  38. package/src/styles/button.css +195 -0
  39. package/src/styles/choice-chip.css +216 -0
  40. package/src/styles/choice-group.css +95 -0
  41. package/src/styles/choice-item.css +267 -0
  42. package/src/styles/choice-picker.css +100 -0
  43. package/src/styles/combobox.css +42 -0
  44. package/src/styles/form-message.css +48 -0
  45. package/src/styles/gem.css +376 -0
  46. package/src/styles/icon-button.css +224 -0
  47. package/src/styles/icon.css +56 -0
  48. package/src/styles/link.css +116 -0
  49. package/src/styles/multi-select-field.css +78 -0
  50. package/src/styles/navigation-badge.css +37 -0
  51. package/src/styles/navigation-item.css +155 -0
  52. package/src/styles/segmented-control.css +115 -0
  53. package/src/styles/select.css +33 -0
  54. package/src/styles/selection-indicator.css +162 -0
  55. package/src/styles/selection-option.css +123 -0
  56. package/src/styles/selection-popup.css +92 -0
  57. package/src/styles/tab-bar.css +47 -0
  58. package/src/styles/tabs.css +55 -0
  59. package/src/styles/text-field.css +268 -0
  60. package/src/styles/token.css +65 -0
  61. package/styles.css +29 -0
@@ -0,0 +1,376 @@
1
+ /* GENERATED from packages/core/src/materials/gem/gem.css by packages/sync/src/build-styles.mjs — do not edit.
2
+ Edit the source in packages/core/src and re-run `npm run sync:styles`. */
3
+ /**
4
+ * Oneli8 · Gem material adapter
5
+ * Figma: section "Gem Button" (1034:9969) — 144 components across
6
+ * Button/Primary/Gem 100:3, Button/Secondary/Gem 100:208,
7
+ * Icon Button ±Circle Primary/Secondary Gem 102:78, 102:151, 119:202, 119:299.
8
+ *
9
+ * From the Figma description (100:3):
10
+ * "Material adapter — NOT a fifth semantic variant. Geometry, meaning,
11
+ * accessibility, protected target, Size, State, label/icon content and
12
+ * native behavior are inherited from the regular molecule. Material changes
13
+ * only the token-bound surface and inner focus edge. Reduced Transparency
14
+ * resolves to the regular opaque equivalent. Quiet and Destructive use the
15
+ * regular material treatment until a distinct Gem treatment is explicitly
16
+ * approved."
17
+ *
18
+ * So Gem is an ATTRIBUTE on an existing button, never a new class:
19
+ * <button class="ol8-btn ol8-btn--primary" data-ol8-material="gem">
20
+ *
21
+ * It adds exactly two painted layers and touches nothing else:
22
+ * ::before the three-stop body + backdrop blur
23
+ * ::after the rim (specular top edge + integrated light rim)
24
+ *
25
+ * THE GRADIENT ANGLE
26
+ * Figma bakes a per-instance angle: 153.1595deg at Standard (83x42),
27
+ * 151.6609 at Comfortable (89x48), 149.2872 at Large (101x60). Those are not
28
+ * three different designs — every one satisfies
29
+ * theta = 90deg + atan(W / H)
30
+ * to four decimal places, which is exactly CSS's `to bottom right` corner
31
+ * keyword (gradient line perpendicular to the other diagonal). Using the
32
+ * keyword reproduces Figma at every size AND at any label width, where a baked
33
+ * degree value would only be correct for the one instance it was measured on.
34
+ */
35
+
36
+ .ol8-btn[data-ol8-material="gem"],
37
+ .ol8-icon-btn[data-ol8-material="gem"] > .ol8-icon-btn__surface,
38
+ .ol8-field[data-ol8-material="gem"] > .ol8-field__control-stack > .ol8-field__control {
39
+ position: relative;
40
+ isolation: isolate;
41
+ --_rim: var(--ol8-material-control-gem--rim-default);
42
+ /* Defined on every Gem surface, not just one, because the halo is read by
43
+ descendants: text through text-shadow and control geometry through chained
44
+ drop-shadow filters. Both are built from the same two tokens and repeated
45
+ the same four times, which is what makes a label's glow and a control's
46
+ glow one field instead of two near-misses. */
47
+ --_gem-glow: 0 0 var(--ol8-material-gem-blur-contentglow)
48
+ var(--ol8-material-gem-color-quiet-contentglow);
49
+ /* Control geometry takes one stack FEWER than text, on purpose. WCAG 1.4.11
50
+ asks 3:1 of a graphical object where 1.4.3 asks 4.5:1 of text, so the
51
+ boundary does not need the fourth repetition — and leaving it off is what
52
+ keeps the indicator blended into the glass instead of ringed. Measured at
53
+ 3px: text in four stacks is 5.06:1, the boundary in three is 3.75:1. */
54
+ --_gem-glow-filter:
55
+ drop-shadow(var(--_gem-glow)) drop-shadow(var(--_gem-glow))
56
+ drop-shadow(var(--_gem-glow));
57
+ }
58
+
59
+ /* The regular opaque background stays underneath on purpose — it is what
60
+ shows through when the gem layer is suppressed for Reduced Transparency. */
61
+ .ol8-btn[data-ol8-material="gem"]::before,
62
+ .ol8-icon-btn[data-ol8-material="gem"] > .ol8-icon-btn__surface::before,
63
+ .ol8-field[data-ol8-material="gem"] .ol8-field__control::before {
64
+ content: "";
65
+ position: absolute;
66
+ inset: 0;
67
+ z-index: -1;
68
+ border-radius: inherit;
69
+ pointer-events: none;
70
+ backdrop-filter: blur(var(--ol8-material-control-gem--blur));
71
+ -webkit-backdrop-filter: blur(var(--ol8-material-control-gem--blur));
72
+ background-image: linear-gradient(
73
+ to bottom right,
74
+ var(--_g1) var(--ol8-material-control-gem--gradient-start),
75
+ var(--_g2) var(--ol8-material-control-gem--gradient-mid),
76
+ var(--_g3) var(--ol8-material-control-gem--gradient-end));
77
+ }
78
+
79
+ .ol8-btn[data-ol8-material="gem"]::after,
80
+ .ol8-icon-btn[data-ol8-material="gem"] > .ol8-icon-btn__surface::after,
81
+ .ol8-field[data-ol8-material="gem"] .ol8-field__control::after {
82
+ content: "";
83
+ position: absolute;
84
+ inset: 0;
85
+ border-radius: inherit;
86
+ pointer-events: none;
87
+ /* Figma's two inner shadows on the Control frame, in order:
88
+ INNER_SHADOW offset 0,2 radius 1 spread 0 -> Rim / Specular
89
+ INNER_SHADOW offset 0,0 radius 0 spread 1 -> Rim / Default
90
+ The second one was written with spread 0, which draws nothing, so the
91
+ integrated light rim never painted even once the variable names were
92
+ right. It is the 1px edge that reads as the bezel. */
93
+ box-shadow:
94
+ inset 0 2px 1px 0 var(--ol8-material-control-gem--rim-specular),
95
+ inset 0 0 0 1px var(--_rim);
96
+ }
97
+
98
+ /* ---- Primary · three-stop same-hue body ------------------------------ */
99
+ .ol8-btn--primary[data-ol8-material="gem"],
100
+ .ol8-icon-btn--primary[data-ol8-material="gem"] > .ol8-icon-btn__surface {
101
+ --_g1: var(--ol8-material-control-gem--primary-start);
102
+ --_g2: var(--ol8-material-control-gem--primary-mid);
103
+ --_g3: var(--ol8-material-control-gem--primary-end);
104
+ --_fg: var(--ol8-material-control-gem--primary-content);
105
+ --_ink: var(--ol8-material-control-gem--primary-content);
106
+ }
107
+ .ol8-btn--primary[data-ol8-material="gem"]:hover:not(:disabled):not([data-ol8-loading]),
108
+ .ol8-icon-btn--primary[data-ol8-material="gem"]:hover:not(:disabled):not([data-ol8-loading]) > .ol8-icon-btn__surface {
109
+ --_g1: var(--ol8-material-control-gem--primary-hover-start);
110
+ --_g2: var(--ol8-material-control-gem--primary-hover-mid);
111
+ --_g3: var(--ol8-material-control-gem--primary-hover-end);
112
+ --_rim: var(--ol8-material-control-gem--rim-hover);
113
+ }
114
+ /* Pressed reuses the HOVER stops and adds the cut-edge drop shadow. */
115
+ .ol8-btn--primary[data-ol8-material="gem"]:active:not(:disabled),
116
+ .ol8-btn--primary[data-ol8-material="gem"][aria-pressed="true"]:not(:disabled),
117
+ .ol8-icon-btn--primary[data-ol8-material="gem"]:active:not(:disabled) > .ol8-icon-btn__surface,
118
+ .ol8-icon-btn--primary[data-ol8-material="gem"][aria-pressed="true"]:not(:disabled) > .ol8-icon-btn__surface {
119
+ --_g1: var(--ol8-material-control-gem--primary-hover-start);
120
+ --_g2: var(--ol8-material-control-gem--primary-hover-mid);
121
+ --_g3: var(--ol8-material-control-gem--primary-hover-end);
122
+ --_rim: var(--ol8-material-control-gem--rim-hover);
123
+ }
124
+ .ol8-btn--primary[data-ol8-material="gem"]:active:not(:disabled),
125
+ .ol8-btn--primary[data-ol8-material="gem"][aria-pressed="true"]:not(:disabled) {
126
+ filter: drop-shadow(0 9px 12px var(--ol8-material-gem-color-cutedge-shadow));
127
+ }
128
+ .ol8-btn--primary[data-ol8-material="gem"][data-ol8-loading],
129
+ .ol8-icon-btn--primary[data-ol8-material="gem"][data-ol8-loading] > .ol8-icon-btn__surface {
130
+ --_g1: var(--ol8-material-control-gem--primary-loading-start);
131
+ --_g2: var(--ol8-material-control-gem--primary-loading-mid);
132
+ --_g3: var(--ol8-material-control-gem--primary-loading-end);
133
+ }
134
+
135
+ /* ---- Secondary · transparent body with an integrated light rim -------
136
+ The regular Secondary's 2px boundary is dropped in Gem; the rim replaces
137
+ it. Reduced Transparency restores the boundary along with the opaque body. */
138
+ .ol8-btn--secondary[data-ol8-material="gem"] { --_bw: 0px; }
139
+ .ol8-btn--secondary[data-ol8-material="gem"],
140
+ .ol8-icon-btn--secondary[data-ol8-material="gem"] > .ol8-icon-btn__surface {
141
+ --_g1: var(--ol8-material-control-gem--secondary-high);
142
+ --_g2: var(--ol8-material-control-gem--secondary-mid);
143
+ --_g3: var(--ol8-material-control-gem--secondary-low);
144
+ --_fg: var(--ol8-material-control-gem--secondary-content);
145
+ --_ink: var(--ol8-material-control-gem--secondary-content);
146
+ --_bd: transparent;
147
+ }
148
+ .ol8-btn--secondary[data-ol8-material="gem"]:hover:not(:disabled):not([data-ol8-loading]),
149
+ .ol8-icon-btn--secondary[data-ol8-material="gem"]:hover:not(:disabled):not([data-ol8-loading]) > .ol8-icon-btn__surface {
150
+ --_g1: var(--ol8-material-control-gem--secondary-hover-high);
151
+ --_g2: var(--ol8-material-control-gem--secondary-hover-mid);
152
+ --_g3: var(--ol8-material-control-gem--secondary-hover-low);
153
+ --_rim: var(--ol8-material-control-gem--rim-hover);
154
+ --_fg: var(--ol8-material-control-gem--secondary-content);
155
+ }
156
+ .ol8-btn--secondary[data-ol8-material="gem"]:active:not(:disabled),
157
+ .ol8-btn--secondary[data-ol8-material="gem"][aria-pressed="true"]:not(:disabled),
158
+ .ol8-icon-btn--secondary[data-ol8-material="gem"]:active:not(:disabled) > .ol8-icon-btn__surface {
159
+ --_g1: var(--ol8-material-control-gem--secondary-hover-high);
160
+ --_g2: var(--ol8-material-control-gem--secondary-hover-mid);
161
+ --_g3: var(--ol8-material-control-gem--secondary-hover-low);
162
+ --_rim: var(--ol8-material-control-gem--rim-hover);
163
+ --_fg: var(--ol8-material-control-gem--secondary-content);
164
+ }
165
+ .ol8-btn--secondary[data-ol8-material="gem"][data-ol8-loading],
166
+ .ol8-icon-btn--secondary[data-ol8-material="gem"][data-ol8-loading] > .ol8-icon-btn__surface {
167
+ --_g1: var(--ol8-material-control-gem--secondary-loading-high);
168
+ --_g2: var(--ol8-material-control-gem--secondary-loading-mid);
169
+ --_g3: var(--ol8-material-control-gem--secondary-loading-low);
170
+ }
171
+
172
+ /* ---- Text Field · the same Secondary body, per Figma 211:36 and 215:226 ---
173
+ Outline follows the Cut Edge, Filled sits on the Stabilized Face and follows
174
+ the Inner Thin Cut. Hover moves Filled onto the Cut Edge. Invalid keeps the
175
+ critical boundary, because a material must never hide a validation result. */
176
+ .ol8-field[data-ol8-material="gem"] .ol8-field__control {
177
+ --_g1: var(--ol8-material-control-gem--secondary-high);
178
+ --_g2: var(--ol8-material-control-gem--secondary-mid);
179
+ --_g3: var(--ol8-material-control-gem--secondary-low);
180
+ --_surface: transparent;
181
+ --_border: var(--ol8-material-gem-color-cutedge-high);
182
+ }
183
+ .ol8-field[data-ol8-material="gem"]:hover:not([data-ol8-disabled]) .ol8-field__control {
184
+ --_g1: var(--ol8-material-control-gem--secondary-hover-high);
185
+ --_g2: var(--ol8-material-control-gem--secondary-hover-mid);
186
+ --_g3: var(--ol8-material-control-gem--secondary-hover-low);
187
+ --_rim: var(--ol8-material-control-gem--rim-hover);
188
+ --_border: var(--ol8-material-gem-color-cutedge-high);
189
+ }
190
+ .ol8-field[data-ol8-material="gem"][data-ol8-appearance="filled"] .ol8-field__control {
191
+ --_surface: var(--ol8-material-gem-color-quiet-stabilizedface);
192
+ --_border: var(--ol8-material-gem-color-innerthincut-high);
193
+ }
194
+ /* Filled is the only gem field appearance that sits ON the quiet face; outline
195
+ leaves --_surface transparent and keeps whatever is behind it. So the
196
+ content colour moves here and nowhere else.
197
+
198
+ WHICH NODES, EXACTLY: only what is physically inside .ol8-field__control,
199
+ because that box is the gem face. The label is NOT — .ol8-field__label lives
200
+ in .ol8-field__label-row, a SIBLING of .ol8-field__control-stack, so it sits
201
+ on the page background above the field. Figma agrees: "Label Row / Label" is
202
+ a sibling of "Control Stack" in Text Field / Filled / Gem (215:226). Giving
203
+ the label gem ink put near-white text on a light page — 1.26:1, invisible.
204
+ The affixes and icons DO sit inside the control and so do belong here. */
205
+ .ol8-field[data-ol8-material="gem"][data-ol8-appearance="filled"]:not([data-ol8-disabled]) .ol8-field__input,
206
+ .ol8-field[data-ol8-material="gem"][data-ol8-appearance="filled"]:not([data-ol8-disabled]) .ol8-field__affix {
207
+ color: var(--ol8-material-gem-color-quiet-content);
208
+ /* Stacked three times on purpose. A single shadow at this radius reaches
209
+ only 1.71:1 against near-white ink on a bright plate, and widening the
210
+ radius makes it WEAKER because blur spreads a fixed alpha budget over more
211
+ area. Repeating the same shadow accumulates alpha instead of spreading it:
212
+ 1 - (1 - a)^4. Measured in proof/gem-halo-contrast.html. */
213
+ text-shadow: var(--_gem-glow), var(--_gem-glow),
214
+ var(--_gem-glow), var(--_gem-glow);
215
+ }
216
+ /* Icons on the face take the token that already exists for exactly this and
217
+ is already stable across appearances. */
218
+ .ol8-field[data-ol8-material="gem"][data-ol8-appearance="filled"]:not([data-ol8-disabled]) .ol8-field__leading-icon,
219
+ .ol8-field[data-ol8-material="gem"][data-ol8-appearance="filled"]:not([data-ol8-disabled]) .ol8-field__trailing-action {
220
+ color: var(--ol8-color-icon-on-gem);
221
+ }
222
+ .ol8-field[data-ol8-material="gem"][data-ol8-readonly][data-ol8-appearance="filled"] .ol8-field__control {
223
+ --_border: var(--ol8-material-gem-color-innerthincut-high);
224
+ }
225
+ .ol8-field[data-ol8-material="gem"]:has(.ol8-field__input[aria-invalid="true"]) .ol8-field__control,
226
+ .ol8-field[data-ol8-material="gem"][data-ol8-status="invalid"] .ol8-field__control {
227
+ --_border: var(--ol8-color-feedbackcritical-inlinecontent);
228
+ }
229
+ /* Disabled resolves to the opaque equivalent, which is the same answer Reduced
230
+ Transparency gives. */
231
+ .ol8-field[data-ol8-material="gem"][data-ol8-disabled] .ol8-field__control {
232
+ --_surface: var(--ol8-material-gem-color-quiet-opaqueequivalent);
233
+ --_border: var(--ol8-color-border-disabled);
234
+ }
235
+ .ol8-field[data-ol8-material="gem"][data-ol8-disabled] .ol8-field__control::before,
236
+ .ol8-field[data-ol8-material="gem"][data-ol8-disabled] .ol8-field__control::after { display: none; }
237
+
238
+ /* Content must sit above the ::before body. */
239
+ .ol8-btn[data-ol8-material="gem"] > *,
240
+ .ol8-icon-btn[data-ol8-material="gem"] .ol8-icon,
241
+ .ol8-field[data-ol8-material="gem"] .ol8-field__control > * { position: relative; z-index: 0; }
242
+
243
+ /* ---- Disabled inherits the regular opaque treatment ------------------ */
244
+ .ol8-btn[data-ol8-material="gem"]:disabled::before,
245
+ .ol8-btn[data-ol8-material="gem"]:disabled::after,
246
+ .ol8-icon-btn[data-ol8-material="gem"]:disabled > .ol8-icon-btn__surface::before,
247
+ .ol8-icon-btn[data-ol8-material="gem"]:disabled > .ol8-icon-btn__surface::after { display: none; }
248
+
249
+ /* ---- Reduced Transparency -> the regular opaque equivalent -----------
250
+ Explicitly required by the Figma description. Hiding the gem layers
251
+ reveals the regular molecule's own background, which was never removed. */
252
+ @media (prefers-reduced-transparency: reduce) {
253
+ .ol8-btn[data-ol8-material="gem"]::before,
254
+ .ol8-btn[data-ol8-material="gem"]::after,
255
+ .ol8-icon-btn[data-ol8-material="gem"] > .ol8-icon-btn__surface::before,
256
+ .ol8-icon-btn[data-ol8-material="gem"] > .ol8-icon-btn__surface::after,
257
+ .ol8-field[data-ol8-material="gem"] .ol8-field__control::before,
258
+ .ol8-field[data-ol8-material="gem"] .ol8-field__control::after { display: none; }
259
+ .ol8-field[data-ol8-material="gem"] .ol8-field__control {
260
+ --_surface: var(--ol8-material-gem-color-quiet-opaqueequivalent);
261
+ }
262
+ .ol8-btn--secondary[data-ol8-material="gem"] {
263
+ --_bw: var(--ol8-component-button-secondaryboundary);
264
+ --_bd: var(--ol8-color-actionsecondary-contentdefault);
265
+ --_fg: var(--ol8-color-actionsecondary-contentdefault);
266
+ }
267
+ .ol8-btn--primary[data-ol8-material="gem"] { --_fg: var(--ol8-color-actionprimary-content); }
268
+ .ol8-btn[data-ol8-material="gem"]:active { filter: none; }
269
+ }
270
+
271
+ /* ---- Generic gem surface ---------------------------------------------
272
+ Until now Gem was reachable only through ol8-btn, ol8-icon-btn and
273
+ ol8-field. Anything else needing the material — a panel, a widget, a
274
+ sheet — had nowhere to go, which is exactly how hand-rolled gem starts.
275
+ This is the primitive: the quiet gem face plus the cut edge, and nothing
276
+ about what the surface contains.
277
+
278
+ The face axis is Figma's own vocabulary, not an invention:
279
+ stabilizedface neutral-030 @ 27% at rest
280
+ environmentalface neutral-030 @ 18% when the environment matters
281
+ Both are clear glass on the same near-white tint; the pair differs only in
282
+ how present the face is, never in hue.
283
+ Motion study 01 (labs/gem-in-motion) drives data-ol8-face along it. */
284
+ .ol8-gem {
285
+ --_gem-face: var(--ol8-material-gem-color-quiet-stabilizedface);
286
+ /* material.gem.blur.* existed in the tokens and nothing consumed it —
287
+ every gem surface was using the CONTROL blur instead, which is a flat
288
+ 12px. These are the surface pair, and they move with the face:
289
+ stabilized 12px, environmental 18px. That step is the dynamic blur. */
290
+ --_gem-blur: var(--ol8-material-gem-blur-stabilized);
291
+
292
+ box-sizing: border-box;
293
+ position: relative;
294
+ background: var(--_gem-face);
295
+ backdrop-filter: blur(var(--_gem-blur)) saturate(1.06);
296
+ -webkit-backdrop-filter: blur(var(--_gem-blur)) saturate(1.06);
297
+ transition: background .4s linear, backdrop-filter .4s linear;
298
+ border-radius: var(--ol8-shape-radius-standard);
299
+ /* Gem carries LIGHT content. color.icon.onGem and both control gem content
300
+ tokens were already neutral-030; the quiet face just never had a content
301
+ token of its own, so surfaces using it fell back to dark body text.
302
+ A transparent face cannot guarantee its own backdrop, so the content
303
+ carries a halo rather than the surface being thickened to provide one. */
304
+ --_gem-glow: 0 0 var(--ol8-material-gem-blur-contentglow)
305
+ var(--ol8-material-gem-color-quiet-contentglow);
306
+ /* Control geometry takes one stack FEWER than text, on purpose. WCAG 1.4.11
307
+ asks 3:1 of a graphical object where 1.4.3 asks 4.5:1 of text, so the
308
+ boundary does not need the fourth repetition — and leaving it off is what
309
+ keeps the indicator blended into the glass instead of ringed. Measured at
310
+ 3px: text in four stacks is 5.06:1, the boundary in three is 3.75:1. */
311
+ --_gem-glow-filter:
312
+ drop-shadow(var(--_gem-glow)) drop-shadow(var(--_gem-glow))
313
+ drop-shadow(var(--_gem-glow));
314
+ color: var(--ol8-material-gem-color-quiet-content);
315
+ /* Stacked three times on purpose. A single shadow at this radius reaches
316
+ only 1.71:1 against near-white ink on a bright plate, and widening the
317
+ radius makes it WEAKER because blur spreads a fixed alpha budget over more
318
+ area. Repeating the same shadow accumulates alpha instead of spreading it:
319
+ 1 - (1 - a)^4. Measured in proof/gem-halo-contrast.html. */
320
+ text-shadow: var(--_gem-glow), var(--_gem-glow),
321
+ var(--_gem-glow), var(--_gem-glow);
322
+ /* The cut edge. Invariant — it is what keeps a gem recognisable as gem. */
323
+ box-shadow:
324
+ inset 0 2px 1px 0 var(--ol8-material-gem-color-cutedge-high),
325
+ inset 0 0 0 1px var(--ol8-material-gem-color-cutedge-mid),
326
+ 0 9px 12px var(--ol8-material-gem-color-cutedge-shadow);
327
+ }
328
+
329
+ /* Disabled on a generic Gem surface, same single grey, same absent halo. */
330
+ .ol8-gem [data-ol8-availability="disabled"] .ol8-selection,
331
+ .ol8-gem [disabled] .ol8-selection {
332
+ --_edge: var(--ol8-material-gem-color-quiet-contentdisabled);
333
+ --_ink: var(--ol8-material-gem-color-quiet-contentdisabled);
334
+ filter: none;
335
+ }
336
+
337
+ /* Content on a generic Gem surface, same rule as the Choice Item carries:
338
+ glyphs and control geometry both take the gem ink and share one halo
339
+ definition, so a label's glow and a control's glow are the same field. Text
340
+ uses text-shadow; an SVG stroke cannot, so it uses chained drop-shadow
341
+ filters built from the identical two tokens. */
342
+ .ol8-gem .ol8-selection {
343
+ --_edge: var(--ol8-material-gem-color-quiet-content);
344
+ filter: var(--_gem-glow-filter);
345
+ }
346
+ .ol8-gem .ol8-icon {
347
+ color: var(--ol8-color-icon-on-gem);
348
+ filter: var(--_gem-glow-filter);
349
+ }
350
+
351
+ .ol8-gem[data-ol8-face="environmental"] {
352
+ --_gem-face: var(--ol8-material-gem-color-quiet-environmentalface);
353
+ --_gem-blur: var(--ol8-material-gem-blur-environmental);
354
+ }
355
+
356
+ /* REDUCED TRANSPARENCY, THE WORN-DISPLAY READING OF IT
357
+ Gem is worn, so the background is the real world and an opaque panel would
358
+ occlude the street for the person who needs help reading the interface most.
359
+ The reduced mode is therefore denser, not solid: legibilityFace at 0.72
360
+ leaves 28% of the scene visible and still measures 7.70:1 against near-white
361
+ ink in the DARKEST-to-BRIGHTEST worst region of a real daylight plate, with
362
+ no credit taken for the halo. That is AAA while remaining see-through.
363
+ Opt in per surface, since the platform flag cannot tell a phone from a lens. */
364
+ .ol8-gem[data-ol8-transparency="reduced"] {
365
+ --_gem-face: var(--ol8-material-gem-color-quiet-legibilityface);
366
+ }
367
+
368
+ /* Same contract as every other gem consumer: transparency is the variable,
369
+ the opaque equivalent is the floor. */
370
+ @media (prefers-reduced-transparency: reduce) {
371
+ .ol8-gem {
372
+ background: var(--ol8-material-gem-color-quiet-opaqueequivalent);
373
+ backdrop-filter: none;
374
+ -webkit-backdrop-filter: none;
375
+ }
376
+ }
@@ -0,0 +1,224 @@
1
+ /* GENERATED from packages/core/src/molecules/icon-button/icon-button.css by packages/sync/src/build-styles.mjs — do not edit.
2
+ Edit the source in packages/core/src and re-run `npm run sync:styles`. */
3
+ /**
4
+ * Oneli8 · Icon Button
5
+ * Figma: section "Icon Button" (1034:10546) — Primary 86:2, Secondary 87:26,
6
+ * Quiet 88:50, Destructive 89:74, plus the four Circle sets.
7
+ * 4 families × 2 shapes × 4 sizes × 6 states = 192 components.
8
+ *
9
+ * Documentation (85:9):
10
+ * "Visible boxes: 36/42/48/60px · protected targets: 48/48/48/60px
11
+ * Artwork: 18/18/24/36px
12
+ * Hover changes the container only; the symbol never translates, rotates
13
+ * or scales. Accessible name is mandatory. Rounded Square is the canonical
14
+ * base; Circle is a governed presentation extension."
15
+ *
16
+ * The component is TWO boxes, and that is the whole point:
17
+ * .ol8-icon-btn — the protected TARGET. Transparent, no paint.
18
+ * Never smaller than 48px even when the visible
19
+ * box is 36px, so the hit area stays compliant.
20
+ * .ol8-icon-btn__surface — the VISIBLE box. Carries bg, 2px border, radius.
21
+ *
22
+ * Collapsing these into one element would silently shrink the touch target
23
+ * to 36px at Compact. Don't.
24
+ */
25
+
26
+ .ol8-icon-btn {
27
+ --_target: var(--ol8-component-iconbutton-target-standard);
28
+ --_box: var(--ol8-component-iconbutton-box-standard);
29
+ --_radius: var(--ol8-component-iconbutton-radius-standard);
30
+ --_artwork: var(--ol8-component-iconbutton-artwork-standard);
31
+
32
+ --_bg: transparent;
33
+ --_bd: transparent;
34
+ --_ink: var(--ol8-color-icon-primary);
35
+
36
+ box-sizing: border-box;
37
+ display: inline-flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+ inline-size: var(--_target);
41
+ block-size: var(--_target);
42
+ padding: 0;
43
+ border: 0;
44
+ background: none; /* the target paints nothing */
45
+ cursor: pointer;
46
+ -webkit-appearance: none;
47
+ appearance: none;
48
+ }
49
+
50
+ .ol8-icon-btn__surface {
51
+ box-sizing: border-box;
52
+ display: inline-flex;
53
+ align-items: center;
54
+ justify-content: center;
55
+ inline-size: var(--_box);
56
+ block-size: var(--_box);
57
+ border: var(--ol8-border-width-standard) solid var(--_bd);
58
+ border-radius: var(--_radius);
59
+ background: var(--_bg);
60
+ color: var(--_ink);
61
+ }
62
+
63
+ /* The surface governs artwork size — never set a size on the glyph. */
64
+ .ol8-icon-btn .ol8-icon { --_ol8-icon-size: var(--_artwork); }
65
+
66
+ /* "the symbol never translates, rotates, or scales" — no transform on the
67
+ glyph in any state. Hover changes the container only. */
68
+
69
+ /* ---- Size ------------------------------------------------------------ */
70
+ .ol8-icon-btn[data-ol8-size="compact"] {
71
+ --_target: var(--ol8-component-iconbutton-target-compact);
72
+ --_box: var(--ol8-component-iconbutton-box-compact);
73
+ --_radius: var(--ol8-component-iconbutton-radius-compact);
74
+ --_artwork: var(--ol8-component-iconbutton-artwork-compact);
75
+ }
76
+ .ol8-icon-btn[data-ol8-size="standard"] {
77
+ --_target: var(--ol8-component-iconbutton-target-standard);
78
+ --_box: var(--ol8-component-iconbutton-box-standard);
79
+ --_radius: var(--ol8-component-iconbutton-radius-standard);
80
+ --_artwork: var(--ol8-component-iconbutton-artwork-standard);
81
+ }
82
+ .ol8-icon-btn[data-ol8-size="comfortable"] {
83
+ --_target: var(--ol8-component-iconbutton-target-comfortable);
84
+ --_box: var(--ol8-component-iconbutton-box-comfortable);
85
+ --_radius: var(--ol8-component-iconbutton-radius-comfortable);
86
+ --_artwork: var(--ol8-component-iconbutton-artwork-comfortable);
87
+ }
88
+ .ol8-icon-btn[data-ol8-size="large"] {
89
+ --_target: var(--ol8-component-iconbutton-target-large);
90
+ --_box: var(--ol8-component-iconbutton-box-large);
91
+ --_radius: var(--ol8-component-iconbutton-radius-large);
92
+ --_artwork: var(--ol8-component-iconbutton-artwork-large);
93
+ }
94
+
95
+ /* ---- Shape: Circle is a governed presentation extension -------------- */
96
+ .ol8-icon-btn[data-ol8-shape="circle"] { --_radius: var(--ol8-shape-radius-full); }
97
+
98
+ /* ---- Family: Primary ------------------------------------------------- */
99
+ /* The glyph binds to the action's CONTENT colour, not the neutral ink role.
100
+ Figma binds --ol8-color-icon-primary (#151817) here, which lands a near-black
101
+ glyph on a #2c438b surface: 1.94:1, against the 3:1 WCAG 1.4.11 minimum for
102
+ graphical objects. --ol8-color-actionprimary-content measures 8.81:1. */
103
+ .ol8-icon-btn--primary {
104
+ --_bg: var(--ol8-color-actionprimary-default);
105
+ --_bd: var(--ol8-color-actionprimary-default);
106
+ --_ink: var(--ol8-color-actionprimary-content);
107
+ }
108
+ .ol8-icon-btn--primary:hover:not(:disabled):not([data-ol8-loading]) > .ol8-icon-btn__surface,
109
+ .ol8-icon-btn--primary[data-ol8-loading] > .ol8-icon-btn__surface {
110
+ --_bg: var(--ol8-color-actionprimary-hover); --_bd: var(--ol8-color-actionprimary-hover);
111
+ }
112
+ .ol8-icon-btn--primary:active:not(:disabled) > .ol8-icon-btn__surface,
113
+ .ol8-icon-btn--primary[aria-pressed="true"]:not(:disabled) > .ol8-icon-btn__surface {
114
+ --_bg: var(--ol8-color-actionprimary-pressed); --_bd: var(--ol8-color-actionprimary-pressed);
115
+ }
116
+ .ol8-icon-btn--primary:disabled > .ol8-icon-btn__surface {
117
+ --_bg: var(--ol8-color-actionprimary-disabled);
118
+ --_bd: var(--ol8-color-border-disabled);
119
+ /* Disabled flips to a LIGHT surface, so the glyph flips back to dark ink. */
120
+ --_ink: var(--ol8-color-actionprimary-disabledcontent);
121
+ }
122
+
123
+ /* ---- Family: Destructive --------------------------------------------- */
124
+ /* Same defect as Primary: #151817 on #922824 measures 2.17:1. */
125
+ .ol8-icon-btn--destructive {
126
+ --_bg: var(--ol8-color-actiondestructive-default);
127
+ --_bd: var(--ol8-color-actiondestructive-default);
128
+ --_ink: var(--ol8-color-actiondestructive-content);
129
+ }
130
+ .ol8-icon-btn--destructive:hover:not(:disabled):not([data-ol8-loading]) > .ol8-icon-btn__surface,
131
+ .ol8-icon-btn--destructive[data-ol8-loading] > .ol8-icon-btn__surface {
132
+ --_bg: var(--ol8-color-actiondestructive-hover); --_bd: var(--ol8-color-actiondestructive-hover);
133
+ }
134
+ .ol8-icon-btn--destructive:active:not(:disabled) > .ol8-icon-btn__surface,
135
+ .ol8-icon-btn--destructive[aria-pressed="true"]:not(:disabled) > .ol8-icon-btn__surface {
136
+ --_bg: var(--ol8-color-actiondestructive-pressed); --_bd: var(--ol8-color-actiondestructive-pressed);
137
+ }
138
+ .ol8-icon-btn--destructive:disabled > .ol8-icon-btn__surface {
139
+ --_bg: var(--ol8-color-actiondestructive-disabled);
140
+ --_bd: var(--ol8-color-border-disabled);
141
+ --_ink: var(--ol8-color-actiondestructive-disabledcontent);
142
+ }
143
+
144
+ /* ---- Family: Secondary (bordered, transparent) ----------------------- */
145
+ .ol8-icon-btn--secondary {
146
+ --_bg: var(--ol8-color-actionsecondary-surfacedefault);
147
+ --_bd: var(--ol8-color-actionsecondary-contentdefault);
148
+ --_ink: var(--ol8-color-actionsecondary-contentdefault);
149
+ }
150
+ .ol8-icon-btn--secondary:hover:not(:disabled):not([data-ol8-loading]) > .ol8-icon-btn__surface,
151
+ .ol8-icon-btn--secondary[data-ol8-loading] > .ol8-icon-btn__surface {
152
+ --_bg: var(--ol8-color-actionsecondary-surfacehover);
153
+ --_bd: var(--ol8-color-actionsecondary-contenthover);
154
+ --_ink: var(--ol8-color-actionsecondary-contenthover);
155
+ }
156
+ .ol8-icon-btn--secondary:active:not(:disabled) > .ol8-icon-btn__surface,
157
+ .ol8-icon-btn--secondary[aria-pressed="true"]:not(:disabled) > .ol8-icon-btn__surface {
158
+ --_bg: var(--ol8-color-actionsecondary-surfacepressed);
159
+ --_bd: var(--ol8-color-actionsecondary-contentpressed);
160
+ --_ink: var(--ol8-color-actionsecondary-contentpressed);
161
+ }
162
+ .ol8-icon-btn--secondary:disabled > .ol8-icon-btn__surface {
163
+ --_bg: var(--ol8-color-actionsecondary-surfacedisabled);
164
+ --_bd: var(--ol8-color-border-disabled);
165
+ --_ink: var(--ol8-color-actionsecondary-contentdisabled);
166
+ }
167
+
168
+ /* ---- Family: Quiet (no boundary) ------------------------------------- */
169
+ .ol8-icon-btn--quiet {
170
+ --_bg: var(--ol8-color-actionquiet-surfacedefault);
171
+ --_bd: transparent;
172
+ --_ink: var(--ol8-color-actionquiet-contentdefault);
173
+ }
174
+ .ol8-icon-btn--quiet:hover:not(:disabled):not([data-ol8-loading]) > .ol8-icon-btn__surface,
175
+ .ol8-icon-btn--quiet[data-ol8-loading] > .ol8-icon-btn__surface {
176
+ --_bg: var(--ol8-color-actionquiet-surfacehover); --_ink: var(--ol8-color-actionquiet-contenthover);
177
+ }
178
+ .ol8-icon-btn--quiet:active:not(:disabled) > .ol8-icon-btn__surface,
179
+ .ol8-icon-btn--quiet[aria-pressed="true"]:not(:disabled) > .ol8-icon-btn__surface {
180
+ --_bg: var(--ol8-color-actionquiet-surfacepressed); --_ink: var(--ol8-color-actionquiet-contentpressed);
181
+ }
182
+ .ol8-icon-btn--quiet:disabled > .ol8-icon-btn__surface {
183
+ --_bg: var(--ol8-color-actionquiet-surfacedisabled); --_ink: var(--ol8-color-actionquiet-contentdisabled);
184
+ }
185
+
186
+ /* ---- Pressed scrim ---------------------------------------------------
187
+ Figma layers a 3px inset scrim over the pressed surface, in addition to
188
+ the darker fill. Reproduced as an inset shadow rather than an extra node. */
189
+ .ol8-icon-btn:active:not(:disabled) > .ol8-icon-btn__surface,
190
+ .ol8-icon-btn[aria-pressed="true"]:not(:disabled) > .ol8-icon-btn__surface {
191
+ box-shadow: inset 0 0 0 3px var(--ol8-component-iconbutton-pressedscrim);
192
+ }
193
+
194
+ /* ---- Focus -----------------------------------------------------------
195
+ OUTSIDE two-tone ring, unlike the labelled Button's inside stroke.
196
+
197
+ Figma declares the 6px outer shadow FIRST, and box-shadow paints the first
198
+ shadow on top — so the 6px dark spread completely covers the 3px light one
199
+ and only a flat dark band renders. Sampling the Figma export confirms it:
200
+ uniform #101b3a at every perimeter position and at both 2px and 4px depth.
201
+
202
+ The inner band is declared here first so both are visible: light from the
203
+ edge out to 3px, dark from 3px to 6px. That is the ring the two tokens
204
+ describe. To restore the Figma-exact single-band ring, swap these two
205
+ lines back.
206
+
207
+ Both focus-ring tokens are BAND THICKNESSES (3px each). box-shadow spread is
208
+ cumulative from the edge, so the outer band's spread is the sum of the two —
209
+ 3px light from the edge, then dark from 3px out to 6px. Do not pass
210
+ --ol8-focus-ring-outerwidth here on its own: that would paint the dark band
211
+ at the same 3px radius as the light one and hide it completely. */
212
+ .ol8-icon-btn:focus-visible { outline: none; }
213
+ .ol8-icon-btn:focus-visible > .ol8-icon-btn__surface {
214
+ box-shadow:
215
+ 0 0 0 var(--ol8-focus-ring-innerwidth) var(--ol8-color-focus-inner),
216
+ 0 0 0 calc(var(--ol8-focus-ring-innerwidth) + var(--ol8-focus-ring-outerwidth)) var(--ol8-color-focus-outer);
217
+ }
218
+
219
+ /* ---- Disabled / Loading ---------------------------------------------- */
220
+ .ol8-icon-btn:disabled { cursor: not-allowed; }
221
+ .ol8-icon-btn[data-ol8-loading] { cursor: progress; }
222
+ @media (prefers-reduced-motion: no-preference) {
223
+ .ol8-icon-btn[data-ol8-loading] .ol8-icon > svg { animation: ol8-spin 900ms linear infinite; }
224
+ }
@@ -0,0 +1,56 @@
1
+ /* GENERATED from packages/core/src/atoms/icon/icon.css by packages/sync/src/build-styles.mjs — do not edit.
2
+ Edit the source in packages/core/src and re-run `npm run sync:styles`. */
3
+ /**
4
+ * Oneli8 · Icon atom
5
+ * Figma: page "Iconography" — masters "Icon Library / Masters" (426:55),
6
+ * sizing from "Icon Frame" (418:51).
7
+ *
8
+ * Contract, straight from the Figma component descriptions:
9
+ * - one canonical 24px source per glyph
10
+ * - the Icon Frame *geometrically* scales the complete source instance, so
11
+ * the 1.8-unit stroke scales with the artwork (48px icon => 3.6px stroke).
12
+ * This is why the SVG keeps viewBox="0 0 24 24" and we size the box only —
13
+ * never `vector-effect: non-scaling-stroke`.
14
+ * - the source is neutral; the consumer supplies semantic color, which is
15
+ * why every stroke/fill is `currentColor`.
16
+ */
17
+
18
+ .ol8-icon {
19
+ /* The outer asset box. Defaults to the canonical 24px source size. */
20
+ --_ol8-icon-size: var(--ol8-size-icon-standard);
21
+
22
+ display: inline-flex;
23
+ flex: none;
24
+ align-items: center;
25
+ justify-content: center;
26
+ inline-size: var(--_ol8-icon-size);
27
+ block-size: var(--_ol8-icon-size);
28
+ /* Inherit ink from the consuming component (semantic color lives there). */
29
+ color: inherit;
30
+ }
31
+
32
+ /* The inner leaf. Both dimensions are explicit — never `auto`. */
33
+ .ol8-icon > svg {
34
+ display: block;
35
+ inline-size: var(--_ol8-icon-size);
36
+ block-size: var(--_ol8-icon-size);
37
+ overflow: visible; /* matches the Figma export; round caps may kiss the edge */
38
+ }
39
+
40
+ /* ---- Icon Frame · universal sizes (Figma 418:51) ---------------------- */
41
+ .ol8-icon[data-ol8-icon-size="9"] { --_ol8-icon-size: var(--ol8-size-icon-micro); }
42
+ .ol8-icon[data-ol8-icon-size="12"] { --_ol8-icon-size: var(--ol8-size-icon-compact); }
43
+ .ol8-icon[data-ol8-icon-size="18"] { --_ol8-icon-size: var(--ol8-size-icon-small); }
44
+ .ol8-icon[data-ol8-icon-size="24"] { --_ol8-icon-size: var(--ol8-size-icon-standard); }
45
+ .ol8-icon[data-ol8-icon-size="30"] { --_ol8-icon-size: var(--ol8-size-icon-medium); }
46
+ .ol8-icon[data-ol8-icon-size="36"] { --_ol8-icon-size: var(--ol8-size-icon-large); }
47
+ .ol8-icon[data-ol8-icon-size="48"] { --_ol8-icon-size: var(--ol8-size-icon-xlarge); }
48
+ .ol8-icon[data-ol8-icon-size="60"] { --_ol8-icon-size: var(--ol8-size-icon-spacious); }
49
+ .ol8-icon[data-ol8-icon-size="72"] { --_ol8-icon-size: var(--ol8-size-icon-display); }
50
+ .ol8-icon[data-ol8-icon-size="96"] { --_ol8-icon-size: var(--ol8-size-icon-hero); }
51
+ .ol8-icon[data-ol8-icon-size="108"] { --_ol8-icon-size: var(--ol8-size-icon-landmark); }
52
+
53
+ /* ---- Direction ------------------------------------------------------- */
54
+ /* "Direction: semantic registry controls RTL mirroring" (Iconography doc 84:13).
55
+ The registry sets data-ol8-icon-mirror; CSS only obeys it. */
56
+ [dir="rtl"] .ol8-icon[data-ol8-icon-mirror="true"] > svg { transform: scaleX(-1); }