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

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 +61 -75
  4. package/ai-context.json +146 -0
  5. package/package.json +30 -24
  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,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); }
@@ -0,0 +1,153 @@
1
+ /* GENERATED from packages/core/src/atoms/selection-indicator/selection-indicator.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 · Selection Indicator (ATOM)
5
+ * Figma: Documentation / Choice Controls (246:3)
6
+ * Icon / Selection / Checkbox 365:2417 (12 variants)
7
+ * Icon / Selection / Radio 366:2371 (8)
8
+ * Icon / Selection / Switch 824:1304 (8)
9
+ *
10
+ * Figma names the tier itself — "Documentation / Atom / Selection Indicator" —
11
+ * and points at packages/react/src/atoms/SelectionIndicator.js (kind=switch),
12
+ * so all three kinds are one atom parameterised by kind, not three components.
13
+ *
14
+ * It composes no other component: the marks are drawn from native elements and
15
+ * one inline path, which is what keeps this an atom rather than a molecule.
16
+ *
17
+ * PURELY PRESENTATIONAL. It carries no role and no state of its own — the
18
+ * native <input> inside Choice Item owns semantics. Always aria-hidden.
19
+ *
20
+ * Axes are Figma's: Selection and Availability.
21
+ */
22
+
23
+ .ol8-selection {
24
+ --_box: var(--ol8-size-icon-small); /* 18px compact */
25
+ --_radius: var(--ol8-component-checkbox-radius-compact);
26
+ --_dot: var(--ol8-component-radio-dot-compact);
27
+ --_track-w: var(--ol8-component-switch-trackwidth-compact);
28
+ --_track-h: var(--ol8-component-switch-trackheight-compact);
29
+ --_thumb: var(--ol8-component-switch-thumb-compact);
30
+
31
+ --_surface: transparent;
32
+ --_edge: var(--ol8-color-border-strong);
33
+ --_ink: var(--ol8-color-selection-content);
34
+
35
+ box-sizing: border-box;
36
+ display: inline-flex;
37
+ flex: none;
38
+ align-items: center;
39
+ justify-content: center;
40
+ position: relative;
41
+ inline-size: var(--_box);
42
+ block-size: var(--_box);
43
+ background: var(--_surface);
44
+ color: var(--_ink);
45
+ }
46
+
47
+ .ol8-selection[data-ol8-size="comfortable"] {
48
+ --_box: var(--ol8-size-icon-standard); /* 24px */
49
+ --_radius: var(--ol8-component-checkbox-radius-comfortable);
50
+ --_dot: var(--ol8-component-radio-dot-comfortable);
51
+ --_track-w: var(--ol8-component-switch-trackwidth-comfortable);
52
+ --_track-h: var(--ol8-component-switch-trackheight-comfortable);
53
+ --_thumb: var(--ol8-component-switch-thumb-comfortable);
54
+ }
55
+
56
+ /* ---- Checkbox -------------------------------------------------------- */
57
+ .ol8-selection--checkbox {
58
+ border: var(--ol8-component-checkbox-boundary) solid var(--_edge);
59
+ border-radius: var(--_radius);
60
+ }
61
+ /* Filled states drop the boundary entirely so the mark occupies the whole
62
+ box, exactly as Figma draws it (glyph at 0,0 at full box size). */
63
+ .ol8-selection--checkbox[data-ol8-selection="checked"],
64
+ .ol8-selection--checkbox[data-ol8-selection="mixed"] {
65
+ border-width: 0;
66
+ --_surface: var(--ol8-color-actionprimary-default);
67
+ --_ink: var(--ol8-color-actionprimary-content);
68
+ }
69
+ .ol8-selection--checkbox[data-ol8-availability="disabled"] { --_edge: var(--ol8-color-border-disabled); }
70
+ .ol8-selection--checkbox[data-ol8-availability="disabled"][data-ol8-selection="checked"],
71
+ .ol8-selection--checkbox[data-ol8-availability="disabled"][data-ol8-selection="mixed"] {
72
+ --_surface: var(--ol8-color-surface-disabled);
73
+ --_ink: var(--ol8-color-icon-disabled);
74
+ }
75
+
76
+ /* The check glyph scales geometrically with the box (18px master is an exact
77
+ 0.75x of the 24px one), so a single 24-unit path serves both sizes. */
78
+ .ol8-selection--checkbox > .ol8-selection__mark > svg {
79
+ display: block;
80
+ inline-size: var(--_box);
81
+ block-size: var(--_box);
82
+ }
83
+
84
+ /* The Mixed bar does NOT scale. Both the 18px and 24px masters carry the same
85
+ 9 x 1.8px bar — an approved optical exception — so it is a fixed-size rule
86
+ here rather than a scaled glyph. */
87
+ .ol8-selection--checkbox[data-ol8-selection="mixed"] > .ol8-selection__mark {
88
+ inline-size: var(--ol8-component-checkbox-mark-mixedwidth);
89
+ block-size: var(--ol8-component-checkbox-mark-stroke);
90
+ border-radius: calc(var(--ol8-component-checkbox-mark-stroke) / 2);
91
+ background: currentColor;
92
+ }
93
+
94
+ /* ---- Radio ----------------------------------------------------------- */
95
+ .ol8-selection--radio {
96
+ border: var(--ol8-component-radio-boundary) solid var(--_edge);
97
+ border-radius: var(--ol8-shape-radius-full);
98
+ }
99
+ /* A selected radio is a checked checkbox in a circle, so it reads the same way:
100
+ the boundary drops and the whole indicator fills with the primary action
101
+ colour, carrying its own content role as the mark. Figma painted both the ring
102
+ and the dot in Selection Surface, which left the radio outlined where the
103
+ checkbox is filled and put the dot at 1.16:1 on the canvas against a 3:1
104
+ minimum. The fill is 8.81:1 and the mark 8.81:1 on the fill. */
105
+ .ol8-selection--radio[data-ol8-selection="selected"] {
106
+ border-width: 0;
107
+ --_surface: var(--ol8-color-actionprimary-default);
108
+ --_ink: var(--ol8-color-actionprimary-content);
109
+ }
110
+ .ol8-selection--radio[data-ol8-availability="disabled"] { --_edge: var(--ol8-color-border-disabled); }
111
+ .ol8-selection--radio[data-ol8-availability="disabled"][data-ol8-selection="selected"] {
112
+ border-width: 0;
113
+ --_surface: var(--ol8-color-surface-disabled);
114
+ --_ink: var(--ol8-color-icon-disabled);
115
+ }
116
+ .ol8-selection--radio > .ol8-selection__mark {
117
+ flex: none; /* never let the flex row shrink it oval */
118
+ inline-size: var(--_dot);
119
+ block-size: var(--_dot);
120
+ border-radius: var(--ol8-shape-radius-full);
121
+ background: currentColor;
122
+ }
123
+
124
+ /* ---- Switch ---------------------------------------------------------- */
125
+ .ol8-selection--switch {
126
+ inline-size: var(--_track-w);
127
+ block-size: var(--_track-h);
128
+ border-radius: var(--ol8-shape-radius-full);
129
+ --_surface: var(--ol8-color-surface-sunken);
130
+ justify-content: flex-start;
131
+ }
132
+ .ol8-selection--switch[data-ol8-selection="on"] { --_surface: var(--ol8-color-selection-surface); }
133
+ .ol8-selection--switch[data-ol8-availability="disabled"] { --_surface: var(--ol8-color-surface-disabled); }
134
+
135
+ .ol8-selection--switch > .ol8-selection__mark {
136
+ position: absolute;
137
+ inset-block-start: var(--ol8-component-switch-inset);
138
+ inset-inline-start: var(--ol8-component-switch-inset);
139
+ inline-size: var(--_thumb);
140
+ block-size: var(--_thumb);
141
+ border-radius: var(--ol8-shape-radius-full);
142
+ background: var(--ol8-color-control-thumb);
143
+ display: inline-flex;
144
+ align-items: center;
145
+ justify-content: center;
146
+ overflow: hidden;
147
+ }
148
+ .ol8-selection--switch[data-ol8-selection="on"] > .ol8-selection__mark {
149
+ inset-inline-start: calc(var(--_track-w) - var(--_thumb) - var(--ol8-component-switch-inset));
150
+ }
151
+ /* Optional artwork inside the thumb: 9px compact, 12px comfortable. */
152
+ .ol8-selection--switch .ol8-icon { --_ol8-icon-size: var(--ol8-size-icon-micro); }
153
+ .ol8-selection--switch[data-ol8-size="comfortable"] .ol8-icon { --_ol8-icon-size: var(--ol8-size-icon-compact); }
@@ -0,0 +1,123 @@
1
+ /* GENERATED from packages/core/src/molecules/selection-option/selection-option.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 · Selection Option (MOLECULE)
5
+ * Figma: Select and Combobox -> 01 · Atomic Selection Building Blocks
6
+ * Building Blocks / Selection Option 570:122 (8 variants)
7
+ * Size Standard|Large x Selection x Availability
8
+ *
9
+ * From the Figma description (570:122):
10
+ * "Description is associated through aria-describedby and REMAINS OUTSIDE
11
+ * the option accessible name. Selection Popup composes this owner;
12
+ * KEYBOARD FOCUS REMAINS ON THE COMBOBOX INPUT through
13
+ * aria-activedescendant."
14
+ *
15
+ * Two consequences that shape everything here:
16
+ * - the option is never focusable and carries no tabindex
17
+ * - "active" (the aria-activedescendant highlight) is a DIFFERENT thing from
18
+ * :hover and from selected, so it gets its own attribute
19
+ *
20
+ * Row geometry: 48px (Standard) / 60px (Large) minimum, but the content box
21
+ * stays 24px tall in both — the padding absorbs the difference.
22
+ * padding-block = (minimum - 24) / 2 -> 12px and 18px
23
+ */
24
+
25
+ .ol8-option {
26
+ --_min: var(--ol8-component-selectionoption-minimum-standard);
27
+ --_inset: var(--ol8-component-selectionoption-inset-standard);
28
+
29
+ box-sizing: border-box;
30
+ display: flex;
31
+ align-items: center;
32
+ min-block-size: var(--_min);
33
+ font-family: var(--ol8-font-family-functional);
34
+ font-variation-settings: "ARRR" var(--ol8-font-axis-arrr-baseline);
35
+ list-style: none;
36
+ cursor: pointer;
37
+ }
38
+ .ol8-option[data-ol8-size="large"] {
39
+ --_min: var(--ol8-component-selectionoption-minimum-large);
40
+ --_inset: var(--ol8-component-selectionoption-inset-large);
41
+ }
42
+ .ol8-option[aria-disabled="true"] { cursor: not-allowed; }
43
+
44
+ /* The surface carries every wash and the focus ring; the row itself stays
45
+ clean so a popup can lay options out without fighting them. */
46
+ .ol8-option__surface {
47
+ box-sizing: border-box;
48
+ position: relative;
49
+ display: flex;
50
+ align-items: center;
51
+ gap: var(--ol8-component-choice-gap-standard);
52
+ flex: 1 0 0;
53
+ min-inline-size: 0;
54
+ block-size: 100%;
55
+ padding-inline: var(--_inset);
56
+ padding-block: calc((var(--_min) - var(--ol8-font-line-024)) / 2);
57
+ border-radius: var(--ol8-shape-radius-medium);
58
+ overflow: clip;
59
+ }
60
+
61
+ /* Selected, hover and active all paint the same wash. Selected is a state of
62
+ the data; active is the aria-activedescendant cursor; hover is the pointer.
63
+ They coincide visually by design. */
64
+ .ol8-option[aria-selected="true"] > .ol8-option__surface,
65
+ .ol8-option[data-ol8-active="true"] > .ol8-option__surface,
66
+ .ol8-option:hover:not([aria-disabled="true"]) > .ol8-option__surface {
67
+ background: var(--ol8-color-selection-surface);
68
+ }
69
+ .ol8-option:active:not([aria-disabled="true"]) > .ol8-option__surface {
70
+ background: var(--ol8-color-selection-surface);
71
+ opacity: 0.84;
72
+ }
73
+
74
+ /* Focus lives on the combobox input, never here — but the option still shows
75
+ the active ring when a popup marks it, so the two-tone ring is reproduced. */
76
+ .ol8-option[data-ol8-focus-ring="true"] > .ol8-option__surface {
77
+ border: var(--ol8-focus-ring-outerwidth) solid var(--ol8-color-focus-outer);
78
+ }
79
+ .ol8-option[data-ol8-focus-ring="true"] > .ol8-option__surface::after {
80
+ content: "";
81
+ position: absolute;
82
+ inset: 2px;
83
+ border: var(--ol8-focus-ring-innerwidth) solid var(--ol8-color-focus-inner);
84
+ border-radius: 7px;
85
+ pointer-events: none;
86
+ }
87
+
88
+ .ol8-option__text {
89
+ display: flex;
90
+ flex-direction: column;
91
+ flex: 1 0 0;
92
+ min-inline-size: 0;
93
+ }
94
+ .ol8-option__label {
95
+ font-size: var(--ol8-font-size-016);
96
+ line-height: var(--ol8-font-line-024);
97
+ font-weight: var(--ol8-font-weight-regular);
98
+ color: var(--ol8-color-text-primary);
99
+ overflow-wrap: break-word;
100
+ }
101
+ .ol8-option__description {
102
+ font-size: var(--ol8-font-size-012);
103
+ line-height: var(--ol8-font-line-018);
104
+ letter-spacing: 0.12px;
105
+ color: var(--ol8-color-text-secondary);
106
+ overflow-wrap: break-word;
107
+ }
108
+ .ol8-option[aria-disabled="true"] .ol8-option__label,
109
+ .ol8-option[aria-disabled="true"] .ol8-option__description {
110
+ color: var(--ol8-color-text-disabled);
111
+ }
112
+
113
+ /* The trailing slot is ALWAYS reserved, selected or not, so the label column
114
+ never shifts when selection changes. Figma keeps an empty 18px node there. */
115
+ .ol8-option__check {
116
+ flex: none;
117
+ inline-size: var(--ol8-size-icon-small);
118
+ block-size: var(--ol8-size-icon-small);
119
+ color: var(--ol8-color-selection-content);
120
+ }
121
+ .ol8-option__check > svg { display: block; inline-size: 100%; block-size: 100%; }
122
+
123
+ .ol8-option .ol8-icon { --_ol8-icon-size: var(--ol8-size-icon-small); }
package/styles.css CHANGED
@@ -1,7 +1,13 @@
1
+ /* OneLi8 · complete stylesheet for @oneli8/react.
2
+ Load order matters: tokens define the custom properties every rule reads,
3
+ atoms before the molecules that compose them, and the Gem material last
4
+ because it adapts surfaces the component rules have already painted. */
1
5
  @import "@oneli8/tokens/css";
2
- @import "./styles/accessibility.css";
3
- @import "./styles/icon-frame.css";
4
- @import "./styles/button.css";
5
- @import "./styles/icon-button.css";
6
- @import "./styles/gem-control-focus.css";
7
- @import "./styles/link.css";
6
+ @import "./src/styles/icon.css";
7
+ @import "./src/styles/selection-indicator.css";
8
+ @import "./src/styles/button.css";
9
+ @import "./src/styles/icon-button.css";
10
+ @import "./src/styles/choice-item.css";
11
+ @import "./src/styles/selection-option.css";
12
+ @import "./src/styles/choice-group.css";
13
+ @import "./src/styles/gem.css";
package/src/Button.d.ts DELETED
@@ -1,20 +0,0 @@
1
- import type {ButtonHTMLAttributes, ReactNode} from "react";
2
-
3
- export type ButtonVariant="primary"|"secondary"|"quiet"|"destructive";
4
- export type ButtonSize="compact"|"standard"|"comfortable"|"large";
5
- export type ButtonIconMotion="none"|"rotate"|"forward"|"backward";
6
- export type ButtonMaterial="regular"|"gem";
7
-
8
- export interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>,"size">{
9
- variant?:ButtonVariant;
10
- size?:ButtonSize;
11
- material?:ButtonMaterial;
12
- loading?:boolean;
13
- pressed?:boolean;
14
- leadingIcon?:ReactNode;
15
- trailingIcon?:ReactNode;
16
- iconMotion?:ButtonIconMotion;
17
- fullWidth?:boolean;
18
- }
19
-
20
- export declare const Button: import("react").ForwardRefExoticComponent<ButtonProps&import("react").RefAttributes<HTMLButtonElement>>;
package/src/Button.js DELETED
@@ -1 +0,0 @@
1
- export {Button} from "./molecules/Button.js";
package/src/Icon.d.ts DELETED
@@ -1,4 +0,0 @@
1
- export type IconName="add"|"forward"|"critical"|"visibility-open"|"loading"|"caution"|"positive"|"informative"|"visibility-closed"|"chevron-down"|"chevron-up"|"search"|"clear"|"close"|"selection-check"|"selection-mixed"|"selection-dot";
2
- export interface IconProps{name:IconName;className?:string}
3
- export declare const iconNames:readonly IconName[];
4
- export declare function Icon(props:IconProps):import("react").ReactNode;
package/src/Icon.js DELETED
@@ -1 +0,0 @@
1
- export {Icon,iconNames} from "./atoms/Icon.js";
@@ -1,20 +0,0 @@
1
- import type {ButtonHTMLAttributes,ReactNode} from "react";
2
- import type {ButtonSize,ButtonVariant} from "./Button.js";
3
-
4
- export type IconButtonShape="rounded"|"circle";
5
- export type IconButtonMaterial="regular"|"gem";
6
- export type IconButtonMotion="system"|"none";
7
-
8
- export interface IconButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>,"children"|"size">{
9
- icon:ReactNode;
10
- accessibleName:string;
11
- variant?:ButtonVariant;
12
- size?:ButtonSize;
13
- shape?:IconButtonShape;
14
- material?:IconButtonMaterial;
15
- loading?:boolean;
16
- pressed?:boolean;
17
- motion?:IconButtonMotion;
18
- }
19
-
20
- export declare const IconButton:import("react").ForwardRefExoticComponent<IconButtonProps&import("react").RefAttributes<HTMLButtonElement>>;
package/src/IconButton.js DELETED
@@ -1 +0,0 @@
1
- export {IconButton} from "./molecules/IconButton.js";
package/src/Link.d.ts DELETED
@@ -1,16 +0,0 @@
1
- import type {AnchorHTMLAttributes,HTMLAttributes,ReactNode} from "react";
2
-
3
- export type LinkForm="inline"|"standalone"|"navigation";
4
- export type LinkSize="small"|"standard"|"large"|"inherit";
5
- export type LinkCurrent="page"|"step"|"location"|"date"|"time"|"true"|boolean;
6
- export type LinkMotion="system"|"none";
7
-
8
- export interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>,"href">{
9
- href:string;
10
- form?:LinkForm;
11
- size?:LinkSize;
12
- current?:LinkCurrent;
13
- motion?:LinkMotion;
14
- }
15
-
16
- export declare const Link:import("react").ForwardRefExoticComponent<LinkProps&import("react").RefAttributes<HTMLAnchorElement>>;
package/src/Link.js DELETED
@@ -1 +0,0 @@
1
- export {Link} from "./molecules/Link.js";
@@ -1,7 +0,0 @@
1
- import {createElement,forwardRef} from "react";
2
- import {cx} from "../foundations/runtime.js";
3
-
4
- export const AnchorControl=forwardRef(function AnchorControl({href,current,className="",children,...rest},ref){
5
- if(typeof href!=="string"||!href.trim())throw new TypeError("Link requires a non-empty href.");
6
- return createElement("a",{...rest,ref,href,"aria-current":current||undefined,className:cx("ol8-AnchorControl",className)},children);
7
- });