@oneli8/react 1.0.0-beta.2 → 1.0.0-beta.5

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 (51) hide show
  1. package/AGENTS.md +12 -0
  2. package/PRINCIPLES.md +48 -0
  3. package/README.md +87 -75
  4. package/ai-context.json +146 -0
  5. package/package.json +50 -23
  6. package/skills/oneli8-figma-to-code/SKILL.md +25 -0
  7. package/skills/oneli8-icons/SKILL.md +24 -0
  8. package/skills/oneli8-ui/SKILL.md +24 -0
  9. package/src/atoms/Icon.js +31 -29
  10. package/src/atoms/SelectionIndicator.js +43 -0
  11. package/src/foundations/geometry.js +72 -0
  12. package/src/foundations/icons.generated.d.ts +38 -0
  13. package/src/foundations/icons.generated.js +406 -0
  14. package/src/index.d.ts +167 -4
  15. package/src/index.js +9 -4
  16. package/src/molecules/Button.js +47 -18
  17. package/src/molecules/ChoiceItem.js +55 -0
  18. package/src/molecules/IconButton.js +31 -16
  19. package/src/molecules/SelectionOption.js +41 -0
  20. package/src/organisms/ChoiceGroup.js +78 -0
  21. package/src/styles/button.css +195 -0
  22. package/src/styles/choice-group.css +95 -0
  23. package/src/styles/choice-item.css +192 -0
  24. package/src/styles/gem.css +173 -0
  25. package/src/styles/icon-button.css +224 -0
  26. package/src/styles/icon.css +56 -0
  27. package/src/styles/selection-indicator.css +153 -0
  28. package/src/styles/selection-option.css +123 -0
  29. package/styles.css +12 -6
  30. package/src/Button.d.ts +0 -20
  31. package/src/Button.js +0 -1
  32. package/src/Icon.d.ts +0 -4
  33. package/src/Icon.js +0 -1
  34. package/src/IconButton.d.ts +0 -20
  35. package/src/IconButton.js +0 -1
  36. package/src/Link.d.ts +0 -16
  37. package/src/Link.js +0 -1
  38. package/src/atoms/AnchorControl.js +0 -7
  39. package/src/atoms/BusyIndicator.js +0 -6
  40. package/src/atoms/ButtonControl.js +0 -14
  41. package/src/atoms/IconFrame.js +0 -19
  42. package/src/atoms/Label.js +0 -5
  43. package/src/foundations/collection.js +0 -13
  44. package/src/foundations/runtime.js +0 -44
  45. package/src/molecules/Link.js +0 -14
  46. package/styles/accessibility.css +0 -1
  47. package/styles/button.css +0 -176
  48. package/styles/gem-control-focus.css +0 -33
  49. package/styles/icon-button.css +0 -42
  50. package/styles/icon-frame.css +0 -1
  51. package/styles/link.css +0 -20
@@ -0,0 +1,195 @@
1
+ /* GENERATED from packages/core/src/molecules/button/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 · Button
5
+ * Figma: section "Button" (1034:9968) — Primary 15:823, Secondary 15:824,
6
+ * Quiet 15:825, Destructive 15:826. 4 families × 4 sizes × 6 states.
7
+ *
8
+ * The governing rule comes from the Figma component description itself:
9
+ *
10
+ * "State → deterministic visual evidence … Runtime-only native button
11
+ * semantics, disabled, loading, pressed, fullWidth and iconMotion remain
12
+ * implemented and tested in code; they must not become fake visual axes."
13
+ *
14
+ * So the six Figma States are NOT six markup variants:
15
+ * Default → the base rule
16
+ * Hover → :hover
17
+ * Pressed → :active (and [aria-pressed="true"] for toggles)
18
+ * Focus → :focus-visible
19
+ * Disabled → [disabled] (native, not a class)
20
+ * Loading → [data-ol8-loading] (which also implies disabled + aria-busy)
21
+ *
22
+ * There is no .ol8-btn--hover. Emitting one is a bug.
23
+ */
24
+
25
+ .ol8-btn {
26
+ /* Per-variant channels. Families set these; the base rule never hardcodes. */
27
+ --_bg: transparent;
28
+ --_fg: currentColor;
29
+ --_bd: transparent;
30
+ --_bw: 0px;
31
+
32
+ /* Size channels, defaulted to Standard. */
33
+ --_h: var(--ol8-component-button-height-standard);
34
+ --_px: var(--ol8-component-button-paddinginline-standard);
35
+ --_r: var(--ol8-component-button-radius-standard);
36
+ --_gap: var(--ol8-component-button-gap-compact);
37
+ --_icon: var(--ol8-size-icon-small);
38
+
39
+ box-sizing: border-box; /* Figma strokes are inside the frame */
40
+ display: inline-flex;
41
+ align-items: center;
42
+ justify-content: center;
43
+ gap: var(--_gap);
44
+ block-size: var(--_h);
45
+ padding-inline: var(--_px);
46
+ padding-block: 0;
47
+ border: var(--_bw) solid var(--_bd);
48
+ border-radius: var(--_r);
49
+ background: var(--_bg);
50
+ color: var(--_fg);
51
+
52
+ font-family: var(--ol8-font-family-functional);
53
+ font-size: var(--ol8-font-size-016);
54
+ line-height: var(--ol8-font-line-024);
55
+ font-weight: var(--ol8-font-weight-regular);
56
+ letter-spacing: 0;
57
+ /* AR One Sans variable axis. Figma sets this on every functional text node;
58
+ omitting it shifts glyph advances and the button measures narrow. */
59
+ font-variation-settings: "ARRR" var(--ol8-font-axis-arrr-baseline);
60
+
61
+ cursor: pointer;
62
+ user-select: none;
63
+ white-space: nowrap;
64
+ text-decoration: none; /* when rendered as <a class="ol8-btn"> */
65
+ -webkit-appearance: none;
66
+ appearance: none;
67
+ }
68
+
69
+ .ol8-btn__label { white-space: nowrap; }
70
+
71
+ /* Button size governs icon size — the Figma Icon Frame is driven by the
72
+ button, never set on the glyph. Higher specificity than .ol8-icon's own
73
+ [data-ol8-icon-size] rule, deliberately. */
74
+ .ol8-btn .ol8-icon { --_ol8-icon-size: var(--_icon); }
75
+
76
+ /* ---- Size (Figma: Size variant axis) --------------------------------- */
77
+ .ol8-btn[data-ol8-size="compact"] {
78
+ --_h: var(--ol8-component-button-height-compact);
79
+ --_px: var(--ol8-component-button-paddinginline-compact);
80
+ --_r: var(--ol8-component-button-radius-compact);
81
+ --_gap: var(--ol8-component-button-gap-compact);
82
+ --_icon: var(--ol8-size-icon-small);
83
+ }
84
+ .ol8-btn[data-ol8-size="standard"] {
85
+ --_h: var(--ol8-component-button-height-standard);
86
+ --_px: var(--ol8-component-button-paddinginline-standard);
87
+ --_r: var(--ol8-component-button-radius-standard);
88
+ --_gap: var(--ol8-component-button-gap-compact); /* shared, by design */
89
+ --_icon: var(--ol8-size-icon-small);
90
+ }
91
+ .ol8-btn[data-ol8-size="comfortable"] {
92
+ --_h: var(--ol8-component-button-height-comfortable);
93
+ --_px: var(--ol8-component-button-paddinginline-comfortable);
94
+ --_r: var(--ol8-component-button-radius-comfortable);
95
+ --_gap: var(--ol8-component-button-gap-comfortable);
96
+ --_icon: var(--ol8-size-icon-standard);
97
+ }
98
+ .ol8-btn[data-ol8-size="large"] {
99
+ --_h: var(--ol8-component-button-height-large);
100
+ --_px: var(--ol8-component-button-paddinginline-large);
101
+ --_r: var(--ol8-component-button-radius-large);
102
+ --_gap: var(--ol8-component-button-gap-comfortable); /* shared, by design */
103
+ --_icon: var(--ol8-size-icon-standard);
104
+ }
105
+
106
+ /* ---- Family: Primary ------------------------------------------------- */
107
+ .ol8-btn--primary { --_bg: var(--ol8-color-actionprimary-default); --_fg: var(--ol8-color-actionprimary-content); }
108
+ .ol8-btn--primary:hover:not(:disabled):not([data-ol8-loading]) { --_bg: var(--ol8-color-actionprimary-hover); }
109
+ .ol8-btn--primary:active:not(:disabled):not([data-ol8-loading]),
110
+ .ol8-btn--primary[aria-pressed="true"]:not(:disabled) { --_bg: var(--ol8-color-actionprimary-pressed); }
111
+ .ol8-btn--primary:disabled, .ol8-btn--primary[data-ol8-loading] {
112
+ --_bg: var(--ol8-color-actionprimary-disabled);
113
+ --_fg: var(--ol8-color-actionprimary-disabledcontent);
114
+ }
115
+
116
+ /* ---- Family: Destructive (same shape as Primary) --------------------- */
117
+ .ol8-btn--destructive { --_bg: var(--ol8-color-actiondestructive-default); --_fg: var(--ol8-color-actiondestructive-content); }
118
+ .ol8-btn--destructive:hover:not(:disabled):not([data-ol8-loading]) { --_bg: var(--ol8-color-actiondestructive-hover); }
119
+ .ol8-btn--destructive:active:not(:disabled):not([data-ol8-loading]),
120
+ .ol8-btn--destructive[aria-pressed="true"]:not(:disabled) { --_bg: var(--ol8-color-actiondestructive-pressed); }
121
+ .ol8-btn--destructive:disabled, .ol8-btn--destructive[data-ol8-loading] {
122
+ --_bg: var(--ol8-color-actiondestructive-disabled);
123
+ --_fg: var(--ol8-color-actiondestructive-disabledcontent);
124
+ }
125
+
126
+ /* ---- Family: Secondary ----------------------------------------------
127
+ Secondary and Quiet carry byte-identical color roles in Figma. The ONLY
128
+ differentiator is this boundary: Secondary has a 2px stroke whose color
129
+ tracks the content color; Quiet has none. Verified — Quiet's node binds no
130
+ --ol8-component-button-secondaryboundary at all. */
131
+ .ol8-btn--secondary {
132
+ --_bw: var(--ol8-component-button-secondaryboundary);
133
+ --_bg: var(--ol8-color-actionsecondary-surfacedefault);
134
+ --_fg: var(--ol8-color-actionsecondary-contentdefault);
135
+ --_bd: var(--ol8-color-actionsecondary-contentdefault);
136
+ }
137
+ .ol8-btn--secondary:hover:not(:disabled):not([data-ol8-loading]) {
138
+ --_bg: var(--ol8-color-actionsecondary-surfacehover);
139
+ --_fg: var(--ol8-color-actionsecondary-contenthover);
140
+ --_bd: var(--ol8-color-actionsecondary-contenthover);
141
+ }
142
+ .ol8-btn--secondary:active:not(:disabled):not([data-ol8-loading]),
143
+ .ol8-btn--secondary[aria-pressed="true"]:not(:disabled) {
144
+ --_bg: var(--ol8-color-actionsecondary-surfacepressed);
145
+ --_fg: var(--ol8-color-actionsecondary-contentpressed);
146
+ --_bd: var(--ol8-color-actionsecondary-contentpressed);
147
+ }
148
+ .ol8-btn--secondary:disabled, .ol8-btn--secondary[data-ol8-loading] {
149
+ --_bg: var(--ol8-color-actionsecondary-surfacedisabled);
150
+ --_fg: var(--ol8-color-actionsecondary-contentdisabled);
151
+ --_bd: var(--ol8-color-actionsecondary-contentdisabled);
152
+ }
153
+
154
+ /* ---- Family: Quiet (identical colors, no boundary) ------------------- */
155
+ .ol8-btn--quiet {
156
+ --_bg: var(--ol8-color-actionquiet-surfacedefault);
157
+ --_fg: var(--ol8-color-actionquiet-contentdefault);
158
+ }
159
+ .ol8-btn--quiet:hover:not(:disabled):not([data-ol8-loading]) {
160
+ --_bg: var(--ol8-color-actionquiet-surfacehover);
161
+ --_fg: var(--ol8-color-actionquiet-contenthover);
162
+ }
163
+ .ol8-btn--quiet:active:not(:disabled):not([data-ol8-loading]),
164
+ .ol8-btn--quiet[aria-pressed="true"]:not(:disabled) {
165
+ --_bg: var(--ol8-color-actionquiet-surfacepressed);
166
+ --_fg: var(--ol8-color-actionquiet-contentpressed);
167
+ }
168
+ .ol8-btn--quiet:disabled, .ol8-btn--quiet[data-ol8-loading] {
169
+ --_bg: var(--ol8-color-actionquiet-surfacedisabled);
170
+ --_fg: var(--ol8-color-actionquiet-contentdisabled);
171
+ }
172
+
173
+ /* ---- Focus ----------------------------------------------------------
174
+ Figma draws the focus ring as an INSIDE stroke, so the focused variant is
175
+ exactly the same size as the unfocused one (83×42 either way, Standard).
176
+ An inset box-shadow reproduces that without disturbing layout or fighting
177
+ Secondary's own 2px boundary — `outline` would sit outside and grow it. */
178
+ .ol8-btn:focus-visible {
179
+ outline: none;
180
+ box-shadow: inset 0 0 0 var(--ol8-focus-ring-innerwidth) var(--ol8-color-focus-outer);
181
+ }
182
+
183
+ /* ---- Disabled / Loading --------------------------------------------- */
184
+ .ol8-btn:disabled, .ol8-btn[data-ol8-loading] { cursor: not-allowed; }
185
+ .ol8-btn[data-ol8-loading] { cursor: progress; }
186
+
187
+ /* "Platform motion adapters may rotate it; Reduced Motion keeps the static
188
+ busy mark." — Figma 379:7. Rotation is opt-in and motion-guarded. */
189
+ @media (prefers-reduced-motion: no-preference) {
190
+ .ol8-btn[data-ol8-loading] .ol8-btn__spinner > svg { animation: ol8-spin 900ms linear infinite; }
191
+ }
192
+ @keyframes ol8-spin { to { transform: rotate(360deg); } }
193
+
194
+ /* ---- fullWidth (runtime-only, per the Figma description) ------------- */
195
+ .ol8-btn[data-ol8-fullwidth] { display: flex; inline-size: 100%; }
@@ -0,0 +1,95 @@
1
+ /* GENERATED from packages/core/src/organisms/choice-group/choice-group.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 · Choice Group (ORGANISM)
5
+ * Figma: Documentation / Organism / Choice Group
6
+ * Checkbox Hierarchy 273:603 Aggregate = None | Some | All
7
+ * Radio Group 859:5180 Behavior = Safe Default | No Default |
8
+ * Horizontal | Invalid
9
+ *
10
+ * First organism in the system, and the first tier allowed to hold logic:
11
+ * the checkbox group computes its parent from its children, and the radio
12
+ * group owns group-level validation.
13
+ *
14
+ * From Figma (273:603): "Aggregate None/Some/All is computed from nested
15
+ * Checkbox molecules; MIXED IS NEVER A THIRD PREFERENCE. Runtime recursion,
16
+ * parent-child synchronization, group naming, required behavior, reset, and
17
+ * announcements live in code."
18
+ *
19
+ * So Aggregate is an OUTPUT, never an input. Nothing may author it.
20
+ *
21
+ * The two shells differ, deliberately:
22
+ * checkbox bordered, radius-container (24px), legend Title/Small
23
+ * radio unbordered, radius-standard (12px), legend Body/Medium
24
+ */
25
+
26
+ .ol8-choice-group {
27
+ box-sizing: border-box;
28
+ display: flex;
29
+ flex-direction: column;
30
+ gap: var(--ol8-spacing-stack-related);
31
+ padding: var(--ol8-spacing-inset-standard);
32
+ background: var(--ol8-color-surface-raised);
33
+ border: 0;
34
+ border-radius: var(--ol8-shape-radius-standard);
35
+ font-family: var(--ol8-font-family-functional);
36
+ font-variation-settings: "ARRR" var(--ol8-font-axis-arrr-baseline);
37
+ min-inline-size: 0; /* fieldset defaults to min-content; this lets it shrink */
38
+ }
39
+
40
+ .ol8-choice-group--checkbox {
41
+ border: 1px solid var(--ol8-color-border-subtle);
42
+ border-radius: var(--ol8-shape-radius-container);
43
+ }
44
+
45
+ /* <legend> is taken out of flow by default, so it is laid out as a normal
46
+ flex child instead — the group label is a real legend for assistive tech. */
47
+ .ol8-choice-group__legend {
48
+ display: block;
49
+ float: none;
50
+ padding: 0;
51
+ inline-size: 100%;
52
+ color: var(--ol8-color-text-primary);
53
+ }
54
+ .ol8-choice-group--checkbox > .ol8-choice-group__legend {
55
+ font-size: var(--ol8-font-size-021);
56
+ line-height: var(--ol8-font-line-030);
57
+ font-weight: var(--ol8-font-weight-semibold);
58
+ }
59
+ .ol8-choice-group--radio > .ol8-choice-group__legend {
60
+ font-size: var(--ol8-font-size-016);
61
+ line-height: var(--ol8-font-line-024);
62
+ font-weight: var(--ol8-font-weight-regular);
63
+ }
64
+
65
+ .ol8-choice-group__instruction {
66
+ font-size: var(--ol8-font-size-012);
67
+ line-height: var(--ol8-font-line-018);
68
+ letter-spacing: 0.12px;
69
+ color: var(--ol8-color-text-secondary);
70
+ }
71
+
72
+ .ol8-choice-group__options { display: flex; flex-direction: column; }
73
+ .ol8-choice-group[data-ol8-orientation="horizontal"] > .ol8-choice-group__options {
74
+ flex-direction: row;
75
+ gap: var(--ol8-spacing-stack-related);
76
+ flex-wrap: wrap;
77
+ }
78
+
79
+ /* Children of a hierarchy sit under their parent, indented one 24px step. */
80
+ .ol8-choice-group__children {
81
+ display: flex;
82
+ flex-direction: column;
83
+ padding-inline-start: var(--ol8-dimension-scale-024);
84
+ }
85
+
86
+ /* Group-level validation message. */
87
+ .ol8-choice-group__message {
88
+ font-size: var(--ol8-font-size-012);
89
+ line-height: var(--ol8-font-line-018);
90
+ letter-spacing: 0.12px;
91
+ color: var(--ol8-color-text-secondary);
92
+ }
93
+ .ol8-choice-group[data-ol8-invalid="true"] > .ol8-choice-group__message {
94
+ color: var(--ol8-color-feedbackcritical-inlinecontent);
95
+ }
@@ -0,0 +1,192 @@
1
+ /* GENERATED from packages/core/src/molecules/choice-item/choice-item.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 · Choice Item (MOLECULE)
5
+ * Figma: Documentation / Molecule / Choice Item
6
+ * Checkbox 252:301 (24 variants) · Radio 331:1309 (16) · Switch 825:1591 (16)
7
+ *
8
+ * From the Figma description (252:301):
9
+ * "Selection maps Unchecked/Checked/Mixed to checked/indeterminate;
10
+ * Availability maps enabled/disabled ... The indicator is a shared
11
+ * selection atom, not local artwork. Native input, name/value, required,
12
+ * reset, form submission, and onCheckedChange live in code."
13
+ *
14
+ * So the molecule is: a real <input>, the selection-indicator atom, a label,
15
+ * an optional description — and NOTHING that duplicates native behaviour.
16
+ *
17
+ * ANATOMY (Figma 266:257 / 252:74 / 252:77)
18
+ * <label class=ol8-choice> the whole row is the target
19
+ * input visually hidden, still focusable
20
+ * __surface hover / pressed / gem wash, bleeds 9px inline
21
+ * __indicator slot: indicator wide, label-line tall
22
+ * __content label + optional description
23
+ *
24
+ * ROW GEOMETRY — every size resolves to the same 48px minimum row:
25
+ * size gap indicator slot h label padding-block
26
+ * compact 6 18 18 Body/Small 15
27
+ * standard 9 18 24 Body/Medium 12
28
+ * comfortable 9 24 24 Body/Medium 12
29
+ * large 9 24 30 Body/Large 9
30
+ * padding-block is derived: (48 - slot height) / 2.
31
+ */
32
+
33
+ .ol8-choice {
34
+ --_gap: var(--ol8-component-choice-gap-standard);
35
+ --_slot-h: var(--ol8-font-line-024);
36
+ --_pad: calc((var(--ol8-component-choice-rowminimum) - var(--_slot-h)) / 2);
37
+ --_label-size: var(--ol8-font-size-016);
38
+ --_label-line: var(--ol8-font-line-024);
39
+ --_indicator: var(--ol8-size-icon-small);
40
+ /* The hover / pressed / focus wash bleeds outside the text column. */
41
+ --_bleed: var(--ol8-dimension-scale-009);
42
+
43
+ box-sizing: border-box;
44
+ position: relative;
45
+ display: flex;
46
+ align-items: flex-start;
47
+ gap: var(--_gap);
48
+ min-block-size: var(--ol8-component-choice-rowminimum);
49
+ padding-block: var(--_pad);
50
+ font-family: var(--ol8-font-family-functional);
51
+ font-variation-settings: "ARRR" var(--ol8-font-axis-arrr-baseline);
52
+ cursor: pointer;
53
+ }
54
+ .ol8-choice[data-ol8-availability="disabled"] { cursor: not-allowed; }
55
+
56
+ /* ---- Size ------------------------------------------------------------ */
57
+ .ol8-choice[data-ol8-size="compact"] {
58
+ --_gap: var(--ol8-component-choice-gap-compact);
59
+ --_slot-h: var(--ol8-font-line-018);
60
+ --_label-size: var(--ol8-font-size-012);
61
+ --_label-line: var(--ol8-font-line-018);
62
+ --_indicator: var(--ol8-size-icon-small);
63
+ }
64
+ .ol8-choice[data-ol8-size="standard"] {
65
+ --_slot-h: var(--ol8-font-line-024);
66
+ --_label-size: var(--ol8-font-size-016);
67
+ --_label-line: var(--ol8-font-line-024);
68
+ --_indicator: var(--ol8-size-icon-small);
69
+ }
70
+ .ol8-choice[data-ol8-size="comfortable"] {
71
+ --_slot-h: var(--ol8-font-line-024);
72
+ --_label-size: var(--ol8-font-size-016);
73
+ --_label-line: var(--ol8-font-line-024);
74
+ --_indicator: var(--ol8-size-icon-standard);
75
+ }
76
+ .ol8-choice[data-ol8-size="large"] {
77
+ --_slot-h: var(--ol8-font-line-030);
78
+ --_label-size: var(--ol8-font-size-021);
79
+ --_label-line: var(--ol8-font-line-030);
80
+ --_indicator: var(--ol8-size-icon-standard);
81
+ }
82
+ /* Compact's label carries the Body/Small tracking. */
83
+ .ol8-choice[data-ol8-size="compact"] .ol8-choice__label { letter-spacing: 1px; }
84
+
85
+ /* ---- The native input owns everything semantic ----------------------- */
86
+ .ol8-choice__input {
87
+ position: absolute;
88
+ inline-size: 1px;
89
+ block-size: 1px;
90
+ opacity: 0;
91
+ pointer-events: none;
92
+ /* No margin reset needed — it is absolutely positioned, so margin cannot
93
+ affect layout. The structure lint would reject one regardless. */
94
+ }
95
+
96
+ /* ---- Wash: hover, pressed, focus, gem -------------------------------- */
97
+ .ol8-choice__surface {
98
+ position: absolute;
99
+ inset-block: 0;
100
+ inset-inline: calc(-1 * var(--_bleed));
101
+ border-radius: var(--ol8-shape-radius-medium);
102
+ pointer-events: none;
103
+ }
104
+ .ol8-choice:hover:not([data-ol8-availability="disabled"]) > .ol8-choice__surface,
105
+ .ol8-choice:active:not([data-ol8-availability="disabled"]) > .ol8-choice__surface {
106
+ background: var(--ol8-color-selection-surface);
107
+ }
108
+
109
+ /* Two nested rings, both visible — outer 3px, inner 3px at a 2px inset.
110
+ This is the same two-tone ring the Icon Button focus tokens describe. */
111
+ .ol8-choice:has(> .ol8-choice__input:focus-visible) > .ol8-choice__surface {
112
+ border: var(--ol8-focus-ring-outerwidth) solid var(--ol8-color-focus-outer);
113
+ }
114
+ .ol8-choice:has(> .ol8-choice__input:focus-visible) > .ol8-choice__surface::after {
115
+ content: "";
116
+ position: absolute;
117
+ inset: 2px;
118
+ border: var(--ol8-focus-ring-innerwidth) solid var(--ol8-color-focus-inner);
119
+ border-radius: 7px;
120
+ }
121
+
122
+ /* ---- Indicator slot -------------------------------------------------- */
123
+ .ol8-choice__indicator {
124
+ display: flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ position: relative;
128
+ flex: none;
129
+ inline-size: var(--_indicator);
130
+ block-size: var(--_slot-h);
131
+ }
132
+
133
+ /* Invalid draws an extra boundary over the indicator, same shape. */
134
+ .ol8-choice[data-ol8-invalid="true"] .ol8-choice__indicator::after {
135
+ content: "";
136
+ position: absolute;
137
+ inline-size: var(--_indicator);
138
+ block-size: var(--_indicator);
139
+ border: var(--ol8-component-checkbox-boundary) solid var(--ol8-color-feedbackcritical-inlinecontent);
140
+ border-radius: var(--ol8-component-checkbox-radius-compact);
141
+ pointer-events: none;
142
+ }
143
+ .ol8-choice[data-ol8-size="comfortable"][data-ol8-invalid="true"] .ol8-choice__indicator::after,
144
+ .ol8-choice[data-ol8-size="large"][data-ol8-invalid="true"] .ol8-choice__indicator::after {
145
+ border-radius: var(--ol8-component-checkbox-radius-comfortable);
146
+ }
147
+ .ol8-choice--radio[data-ol8-invalid="true"] .ol8-choice__indicator::after {
148
+ border-radius: var(--ol8-shape-radius-full);
149
+ }
150
+
151
+ /* ---- Content --------------------------------------------------------- */
152
+ .ol8-choice__content {
153
+ display: flex;
154
+ flex-direction: column;
155
+ gap: var(--ol8-dimension-scale-003);
156
+ flex: 1 0 0;
157
+ min-inline-size: 0;
158
+ position: relative;
159
+ }
160
+ .ol8-choice__label {
161
+ font-size: var(--_label-size);
162
+ line-height: var(--_label-line);
163
+ font-weight: var(--ol8-font-weight-regular);
164
+ color: var(--ol8-color-text-primary);
165
+ overflow-wrap: break-word;
166
+ }
167
+ .ol8-choice__description {
168
+ font-size: var(--ol8-font-size-012);
169
+ line-height: var(--ol8-font-line-018);
170
+ letter-spacing: 0.12px;
171
+ color: var(--ol8-color-text-secondary);
172
+ overflow-wrap: break-word;
173
+ }
174
+ .ol8-choice[data-ol8-availability="disabled"] .ol8-choice__label,
175
+ .ol8-choice[data-ol8-availability="disabled"] .ol8-choice__description {
176
+ color: var(--ol8-color-text-disabled);
177
+ }
178
+
179
+ /* ---- Gem material ----------------------------------------------------
180
+ "Gem is one Boolean material adapter over the same anatomy; Regular
181
+ remains the default." A wash on the row, not a different component. */
182
+ .ol8-choice[data-ol8-material="gem"] > .ol8-choice__surface {
183
+ background: var(--ol8-material-gem-color-quiet-environmentalface);
184
+ border: 1px solid var(--ol8-material-gem-color-cutedge-high);
185
+ border-radius: var(--ol8-shape-radius-standard);
186
+ inset-inline: 0;
187
+ }
188
+ @media (prefers-reduced-transparency: reduce) {
189
+ .ol8-choice[data-ol8-material="gem"] > .ol8-choice__surface {
190
+ background: var(--ol8-material-gem-color-quiet-opaqueequivalent);
191
+ }
192
+ }
@@ -0,0 +1,173 @@
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
+ position: relative;
39
+ isolation: isolate;
40
+ --_rim: var(--ol8-material-control-gem-rim-default);
41
+ }
42
+
43
+ /* The regular opaque background stays underneath on purpose — it is what
44
+ shows through when the gem layer is suppressed for Reduced Transparency. */
45
+ .ol8-btn[data-ol8-material="gem"]::before,
46
+ .ol8-icon-btn[data-ol8-material="gem"] > .ol8-icon-btn__surface::before {
47
+ content: "";
48
+ position: absolute;
49
+ inset: 0;
50
+ z-index: -1;
51
+ border-radius: inherit;
52
+ pointer-events: none;
53
+ backdrop-filter: blur(var(--ol8-material-control-gem-blur));
54
+ -webkit-backdrop-filter: blur(var(--ol8-material-control-gem-blur));
55
+ background-image: linear-gradient(
56
+ to bottom right,
57
+ var(--_g1) var(--ol8-material-control-gem-gradient-start),
58
+ var(--_g2) var(--ol8-material-control-gem-gradient-mid),
59
+ var(--_g3) var(--ol8-material-control-gem-gradient-end));
60
+ }
61
+
62
+ .ol8-btn[data-ol8-material="gem"]::after,
63
+ .ol8-icon-btn[data-ol8-material="gem"] > .ol8-icon-btn__surface::after {
64
+ content: "";
65
+ position: absolute;
66
+ inset: 0;
67
+ border-radius: inherit;
68
+ pointer-events: none;
69
+ box-shadow:
70
+ inset 0 2px 1px 0 var(--ol8-material-control-gem-rim-specular),
71
+ inset 0 0 0 0 var(--_rim);
72
+ }
73
+
74
+ /* ---- Primary · three-stop same-hue body ------------------------------ */
75
+ .ol8-btn--primary[data-ol8-material="gem"],
76
+ .ol8-icon-btn--primary[data-ol8-material="gem"] > .ol8-icon-btn__surface {
77
+ --_g1: var(--ol8-material-control-gem-primary-start);
78
+ --_g2: var(--ol8-material-control-gem-primary-mid);
79
+ --_g3: var(--ol8-material-control-gem-primary-end);
80
+ --_fg: var(--ol8-material-control-gem-primary-content);
81
+ --_ink: var(--ol8-material-control-gem-primary-content);
82
+ }
83
+ .ol8-btn--primary[data-ol8-material="gem"]:hover:not(:disabled):not([data-ol8-loading]),
84
+ .ol8-icon-btn--primary[data-ol8-material="gem"]:hover:not(:disabled):not([data-ol8-loading]) > .ol8-icon-btn__surface {
85
+ --_g1: var(--ol8-material-control-gem-primary-hoverstart);
86
+ --_g2: var(--ol8-material-control-gem-primary-hovermid);
87
+ --_g3: var(--ol8-material-control-gem-primary-hoverend);
88
+ --_rim: var(--ol8-material-control-gem-rim-hover);
89
+ }
90
+ /* Pressed reuses the HOVER stops and adds the cut-edge drop shadow. */
91
+ .ol8-btn--primary[data-ol8-material="gem"]:active:not(:disabled),
92
+ .ol8-btn--primary[data-ol8-material="gem"][aria-pressed="true"]:not(:disabled),
93
+ .ol8-icon-btn--primary[data-ol8-material="gem"]:active:not(:disabled) > .ol8-icon-btn__surface,
94
+ .ol8-icon-btn--primary[data-ol8-material="gem"][aria-pressed="true"]:not(:disabled) > .ol8-icon-btn__surface {
95
+ --_g1: var(--ol8-material-control-gem-primary-hoverstart);
96
+ --_g2: var(--ol8-material-control-gem-primary-hovermid);
97
+ --_g3: var(--ol8-material-control-gem-primary-hoverend);
98
+ --_rim: var(--ol8-material-control-gem-rim-hover);
99
+ }
100
+ .ol8-btn--primary[data-ol8-material="gem"]:active:not(:disabled),
101
+ .ol8-btn--primary[data-ol8-material="gem"][aria-pressed="true"]:not(:disabled) {
102
+ filter: drop-shadow(0 9px 12px var(--ol8-material-gem-color-cutedge-shadow));
103
+ }
104
+ .ol8-btn--primary[data-ol8-material="gem"][data-ol8-loading],
105
+ .ol8-icon-btn--primary[data-ol8-material="gem"][data-ol8-loading] > .ol8-icon-btn__surface {
106
+ --_g1: var(--ol8-material-control-gem-primary-loadingstart);
107
+ --_g2: var(--ol8-material-control-gem-primary-loadingmid);
108
+ --_g3: var(--ol8-material-control-gem-primary-loadingend);
109
+ }
110
+
111
+ /* ---- Secondary · transparent body with an integrated light rim -------
112
+ The regular Secondary's 2px boundary is dropped in Gem; the rim replaces
113
+ it. Reduced Transparency restores the boundary along with the opaque body. */
114
+ .ol8-btn--secondary[data-ol8-material="gem"] { --_bw: 0px; }
115
+ .ol8-btn--secondary[data-ol8-material="gem"],
116
+ .ol8-icon-btn--secondary[data-ol8-material="gem"] > .ol8-icon-btn__surface {
117
+ --_g1: var(--ol8-material-control-gem-secondary-high);
118
+ --_g2: var(--ol8-material-control-gem-secondary-mid);
119
+ --_g3: var(--ol8-material-control-gem-secondary-low);
120
+ --_fg: var(--ol8-material-control-gem-secondary-content);
121
+ --_ink: var(--ol8-material-control-gem-secondary-content);
122
+ --_bd: transparent;
123
+ }
124
+ .ol8-btn--secondary[data-ol8-material="gem"]:hover:not(:disabled):not([data-ol8-loading]),
125
+ .ol8-icon-btn--secondary[data-ol8-material="gem"]:hover:not(:disabled):not([data-ol8-loading]) > .ol8-icon-btn__surface {
126
+ --_g1: var(--ol8-material-control-gem-secondary-hoverhigh);
127
+ --_g2: var(--ol8-material-control-gem-secondary-hovermid);
128
+ --_g3: var(--ol8-material-control-gem-secondary-hoverlow);
129
+ --_rim: var(--ol8-material-control-gem-rim-hover);
130
+ --_fg: var(--ol8-material-control-gem-secondary-content);
131
+ }
132
+ .ol8-btn--secondary[data-ol8-material="gem"]:active:not(:disabled),
133
+ .ol8-btn--secondary[data-ol8-material="gem"][aria-pressed="true"]:not(:disabled),
134
+ .ol8-icon-btn--secondary[data-ol8-material="gem"]:active:not(:disabled) > .ol8-icon-btn__surface {
135
+ --_g1: var(--ol8-material-control-gem-secondary-hoverhigh);
136
+ --_g2: var(--ol8-material-control-gem-secondary-hovermid);
137
+ --_g3: var(--ol8-material-control-gem-secondary-hoverlow);
138
+ --_rim: var(--ol8-material-control-gem-rim-hover);
139
+ --_fg: var(--ol8-material-control-gem-secondary-content);
140
+ }
141
+ .ol8-btn--secondary[data-ol8-material="gem"][data-ol8-loading],
142
+ .ol8-icon-btn--secondary[data-ol8-material="gem"][data-ol8-loading] > .ol8-icon-btn__surface {
143
+ --_g1: var(--ol8-material-control-gem-secondary-loadinghigh);
144
+ --_g2: var(--ol8-material-control-gem-secondary-loadingmid);
145
+ --_g3: var(--ol8-material-control-gem-secondary-loadinglow);
146
+ }
147
+
148
+ /* Content must sit above the ::before body. */
149
+ .ol8-btn[data-ol8-material="gem"] > *,
150
+ .ol8-icon-btn[data-ol8-material="gem"] .ol8-icon { position: relative; z-index: 0; }
151
+
152
+ /* ---- Disabled inherits the regular opaque treatment ------------------ */
153
+ .ol8-btn[data-ol8-material="gem"]:disabled::before,
154
+ .ol8-btn[data-ol8-material="gem"]:disabled::after,
155
+ .ol8-icon-btn[data-ol8-material="gem"]:disabled > .ol8-icon-btn__surface::before,
156
+ .ol8-icon-btn[data-ol8-material="gem"]:disabled > .ol8-icon-btn__surface::after { display: none; }
157
+
158
+ /* ---- Reduced Transparency -> the regular opaque equivalent -----------
159
+ Explicitly required by the Figma description. Hiding the gem layers
160
+ reveals the regular molecule's own background, which was never removed. */
161
+ @media (prefers-reduced-transparency: reduce) {
162
+ .ol8-btn[data-ol8-material="gem"]::before,
163
+ .ol8-btn[data-ol8-material="gem"]::after,
164
+ .ol8-icon-btn[data-ol8-material="gem"] > .ol8-icon-btn__surface::before,
165
+ .ol8-icon-btn[data-ol8-material="gem"] > .ol8-icon-btn__surface::after { display: none; }
166
+ .ol8-btn--secondary[data-ol8-material="gem"] {
167
+ --_bw: var(--ol8-component-button-secondaryboundary);
168
+ --_bd: var(--ol8-color-actionsecondary-contentdefault);
169
+ --_fg: var(--ol8-color-actionsecondary-contentdefault);
170
+ }
171
+ .ol8-btn--primary[data-ol8-material="gem"] { --_fg: var(--ol8-color-actionprimary-content); }
172
+ .ol8-btn[data-ol8-material="gem"]:active { filter: none; }
173
+ }