@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,267 @@
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
+ /* The quiet face is clear glass — near-white at 0.18/0.27, not a dark tint —
189
+ so it cannot guarantee what sits behind it at all. Content on it therefore
190
+ takes the stable near-white gem ink rather than body text, and carries its
191
+ own halo, which is the only thing standing between the label and whatever
192
+ the environment happens to be. Unlike the gem text field, the label and
193
+ description here really are on the face: .ol8-choice__surface spans the row
194
+ beneath them. Disabled is excluded — it resolves to the opaque equivalent,
195
+ where the regular disabled ink applies and no halo is wanted. */
196
+ /* ONE GLOW, TWO SYNTAXES.
197
+ Content on Gem means ALL content — glyphs and control geometry alike. Text
198
+ takes the halo through text-shadow; an SVG stroke cannot, so the indicator
199
+ takes the identical halo through chained drop-shadow filters. Both are built
200
+ from the same two tokens and the same four repetitions, which is what makes
201
+ the label's glow and the checkbox's glow read as one field rather than two
202
+ near-misses. Change the radius or the colour and both follow. */
203
+ .ol8-choice[data-ol8-material="gem"] {
204
+ --_gem-glow: 0 0 var(--ol8-material-gem-blur-contentglow)
205
+ var(--ol8-material-gem-color-quiet-contentglow);
206
+ /* Control geometry takes one stack FEWER than text, on purpose. WCAG 1.4.11
207
+ asks 3:1 of a graphical object where 1.4.3 asks 4.5:1 of text, so the
208
+ boundary does not need the fourth repetition — and leaving it off is what
209
+ keeps the indicator blended into the glass instead of ringed. Measured at
210
+ 3px: text in four stacks is 5.06:1, the boundary in three is 3.75:1. */
211
+ --_gem-glow-filter:
212
+ drop-shadow(var(--_gem-glow)) drop-shadow(var(--_gem-glow))
213
+ drop-shadow(var(--_gem-glow));
214
+ }
215
+
216
+ /* DISABLED ON GEM IS ONE GREY, SHARED.
217
+ color.text.disabled is dark in the Light appearance, so a disabled row on
218
+ glass whose enabled content is near-white ended up with a dark label beside
219
+ a dark box — it read broken rather than unavailable. On Gem, disabled text
220
+ and disabled control geometry take the SAME whitish grey, and neither takes
221
+ the halo: the absence of the glow is part of how unavailable reads.
222
+ Disabled content is exempt from 1.4.3, which excludes inactive controls. */
223
+ .ol8-choice[data-ol8-material="gem"][data-ol8-availability="disabled"] .ol8-choice__label,
224
+ .ol8-choice[data-ol8-material="gem"][data-ol8-availability="disabled"] .ol8-choice__description {
225
+ color: var(--ol8-material-gem-color-quiet-contentdisabled);
226
+ text-shadow: none;
227
+ }
228
+ .ol8-choice[data-ol8-material="gem"][data-ol8-availability="disabled"] .ol8-selection {
229
+ --_edge: var(--ol8-material-gem-color-quiet-contentdisabled);
230
+ --_ink: var(--ol8-material-gem-color-quiet-contentdisabled);
231
+ filter: none;
232
+ }
233
+
234
+ /* The unselected control boundary is content, not chrome. It was
235
+ color.border.strong, which is dark in the Light appearance and left the
236
+ checkbox reading as a dark hole in a surface whose text had gone near-white.
237
+ On Gem it takes the same ink as the label. Selected and mixed are untouched:
238
+ those fill with the primary action family and carry their own white mark,
239
+ which already reads on glass. */
240
+ .ol8-choice[data-ol8-material="gem"]:not([data-ol8-availability="disabled"]) .ol8-selection {
241
+ --_edge: var(--ol8-material-gem-color-quiet-content);
242
+ filter: var(--_gem-glow-filter);
243
+ }
244
+
245
+ /* Any iconography sitting on the face takes the token that already exists for
246
+ exactly this and is already stable across appearances. */
247
+ .ol8-choice[data-ol8-material="gem"]:not([data-ol8-availability="disabled"]) .ol8-icon {
248
+ color: var(--ol8-color-icon-on-gem);
249
+ filter: var(--_gem-glow-filter);
250
+ }
251
+
252
+ .ol8-choice[data-ol8-material="gem"]:not([data-ol8-availability="disabled"]) .ol8-choice__label,
253
+ .ol8-choice[data-ol8-material="gem"]:not([data-ol8-availability="disabled"]) .ol8-choice__description {
254
+ color: var(--ol8-material-gem-color-quiet-content);
255
+ /* Stacked three times on purpose. A single shadow at this radius reaches
256
+ only 1.71:1 against near-white ink on a bright plate, and widening the
257
+ radius makes it WEAKER because blur spreads a fixed alpha budget over more
258
+ area. Repeating the same shadow accumulates alpha instead of spreading it:
259
+ 1 - (1 - a)^4. Measured in proof/gem-halo-contrast.html. */
260
+ text-shadow: var(--_gem-glow), var(--_gem-glow),
261
+ var(--_gem-glow), var(--_gem-glow);
262
+ }
263
+ @media (prefers-reduced-transparency: reduce) {
264
+ .ol8-choice[data-ol8-material="gem"] > .ol8-choice__surface {
265
+ background: var(--ol8-material-gem-color-quiet-opaqueequivalent);
266
+ }
267
+ }
@@ -0,0 +1,100 @@
1
+ /* GENERATED from packages/core/src/organisms/choice-picker/choice-picker.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 Picker (ORGANISM)
5
+ * Figma: Organism / Choice Picker / Soft Hexagon 906:2898 (16 variants)
6
+ *
7
+ * Three parts stacked twelve apart: a search field, a wrapping matrix of chips,
8
+ * and in Apply mode a commitment line. The search field is the canonical Text
9
+ * Field owner and the chips are the canonical Choice Chip owner, so neither is
10
+ * restyled here.
11
+ *
12
+ * Read from all four sizes in Figma: the root gap, the matrix gap and the
13
+ * commitment typography do NOT scale with the picker size. They are twelve,
14
+ * six, and 12/18 at Compact, Standard, Comfortable and Large alike. Only the
15
+ * search field and the chips take the size.
16
+ */
17
+
18
+ .ol8-picker {
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: var(--ol8-spacing-stack-related);
22
+ inline-size: 100%;
23
+ }
24
+
25
+ /* "The matrix wraps with 6px gaps and preserves source/reading order; it is not
26
+ a masonry layout." A plain wrapping flex row is exactly that. */
27
+ .ol8-picker__matrix {
28
+ display: flex;
29
+ flex-wrap: wrap;
30
+ gap: var(--ol8-component-choice-chip-gap);
31
+ }
32
+
33
+ /* Figma: HORIZONTAL, space between, 24 tall, both texts secondary at 12/18. */
34
+ .ol8-picker__commitment {
35
+ display: flex;
36
+ align-items: flex-start;
37
+ justify-content: space-between;
38
+ gap: var(--ol8-spacing-inline-standard);
39
+ min-block-size: var(--ol8-size-icon-standard);
40
+ font-size: var(--ol8-font-size-012);
41
+ line-height: var(--ol8-font-line-018);
42
+ color: var(--ol8-color-text-secondary);
43
+ }
44
+
45
+ .ol8-picker__summary {
46
+ font-family: var(--ol8-font-family-functional);
47
+ color: inherit;
48
+ }
49
+
50
+ /* Clear all and the apply hint are text weight actions rather than Button
51
+ instances. The contract asks for Clear all to be quiet, and Figma's row is
52
+ twenty four tall, which a Button's protected forty eight target would break.
53
+ They are still real buttons, so they are reachable and announced. */
54
+ .ol8-picker__apply,
55
+ .ol8-picker__clear {
56
+ padding: 0;
57
+ border: 0;
58
+ background: none;
59
+ font-family: var(--ol8-font-family-functional);
60
+ font-size: inherit;
61
+ line-height: inherit;
62
+ color: var(--ol8-color-actionquiet-contentdefault);
63
+ cursor: pointer;
64
+ }
65
+ .ol8-picker__apply:hover,
66
+ .ol8-picker__clear:hover {
67
+ color: var(--ol8-color-actionquiet-contenthover);
68
+ text-decoration: underline;
69
+ }
70
+ .ol8-picker__apply:focus-visible,
71
+ .ol8-picker__clear:focus-visible {
72
+ outline: var(--ol8-focus-ring-innerwidth) solid var(--ol8-color-focus-inner);
73
+ outline-offset: var(--ol8-focus-ring-offset);
74
+ }
75
+
76
+ .ol8-picker[data-ol8-availability="disabled"] .ol8-picker__apply,
77
+ .ol8-picker[data-ol8-availability="disabled"] .ol8-picker__clear {
78
+ color: var(--ol8-color-actionquiet-contentdisabled);
79
+ cursor: not-allowed;
80
+ }
81
+
82
+ /* The search label stays associated while the composition hides it. */
83
+ .ol8-picker__label-row--hidden {
84
+ position: absolute;
85
+ inline-size: 1px;
86
+ block-size: 1px;
87
+ overflow: hidden;
88
+ clip-path: inset(50%);
89
+ white-space: nowrap;
90
+ }
91
+
92
+ /* Retrieval and result counts are announced rather than only drawn. */
93
+ .ol8-picker__status {
94
+ position: absolute;
95
+ inline-size: 1px;
96
+ block-size: 1px;
97
+ overflow: hidden;
98
+ clip-path: inset(50%);
99
+ white-space: nowrap;
100
+ }
@@ -0,0 +1,42 @@
1
+ /* GENERATED from packages/core/src/organisms/combobox/combobox.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 · Combobox (ORGANISM)
5
+ * Figma: Organism / Combobox 883:2159 — "Reuses approved Text Field, Selection
6
+ * Popup and governed icon atoms."
7
+ *
8
+ * So the field geometry is the field's, the popup is the popup's, and this
9
+ * file adds only what belongs to the combination: where the popup sits, and
10
+ * the two marks at the trailing edge.
11
+ */
12
+
13
+ .ol8-combobox { position: relative; }
14
+
15
+ /* "Closed control to popup block gap" is a token, so the popup sits where the
16
+ design says rather than wherever absolute positioning lands it. */
17
+ .ol8-combobox__popup {
18
+ position: absolute;
19
+ z-index: 1;
20
+ inset-inline: 0;
21
+ inset-block-start: calc(100% + var(--ol8-component-selectionpopup-gap));
22
+ }
23
+
24
+ .ol8-combobox__disclosure {
25
+ flex: none;
26
+ display: inline-flex;
27
+ color: var(--ol8-color-icon-secondary);
28
+ cursor: pointer;
29
+ }
30
+
31
+ .ol8-combobox__clear {
32
+ flex: none;
33
+ display: inline-flex;
34
+ border: 0;
35
+ padding: 0;
36
+ background: none;
37
+ color: var(--ol8-color-icon-secondary);
38
+ cursor: pointer;
39
+ }
40
+ .ol8-combobox__clear:disabled { color: var(--ol8-color-icon-disabled); cursor: not-allowed; }
41
+
42
+ .ol8-combobox[data-ol8-disabled] .ol8-combobox__disclosure { color: var(--ol8-color-icon-disabled); cursor: not-allowed; }
@@ -0,0 +1,48 @@
1
+ /* GENERATED from packages/core/src/molecules/form-message/form-message.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 · Form Message (MOLECULE)
5
+ * Figma: Form Message 177:13, five tones.
6
+ *
7
+ * Anatomy read from Figma: a horizontal row, gap Component / Text Field / Gap /
8
+ * Status, items centred, one Small status icon beside twelve over eighteen
9
+ * regular text. Tone changes exactly one thing, the inline content colour,
10
+ * which both the icon stroke and the text take.
11
+ */
12
+
13
+ .ol8-form-message {
14
+ display: flex;
15
+ align-items: center;
16
+ gap: var(--ol8-component-textfield-gap-status);
17
+ color: var(--_tone);
18
+ font-family: var(--ol8-font-family-functional);
19
+ font-size: var(--ol8-font-size-012);
20
+ font-weight: var(--ol8-font-weight-regular);
21
+ line-height: var(--ol8-font-line-018);
22
+ }
23
+
24
+ .ol8-form-message__icon { flex: none; color: var(--_tone); }
25
+
26
+ /* ---- Tone ------------------------------------------------------------- */
27
+ .ol8-form-message[data-ol8-tone="critical"] { --_tone: var(--ol8-color-feedbackcritical-inlinecontent); }
28
+ .ol8-form-message[data-ol8-tone="caution"] { --_tone: var(--ol8-color-feedbackcaution-inlinecontent); }
29
+ .ol8-form-message[data-ol8-tone="positive"] { --_tone: var(--ol8-color-feedbackpositive-inlinecontent); }
30
+ .ol8-form-message[data-ol8-tone="informative"] { --_tone: var(--ol8-color-feedbackinformative-inlinecontent); }
31
+
32
+ /* Pending reads as informative text, and Figma gives its loading mark the
33
+ primary icon colour rather than a feedback colour, because nothing has been
34
+ decided yet. */
35
+ .ol8-form-message[data-ol8-tone="pending"] {
36
+ --_tone: var(--ol8-color-feedbackinformative-inlinecontent);
37
+ }
38
+ .ol8-form-message[data-ol8-tone="pending"] .ol8-form-message__icon {
39
+ color: var(--ol8-color-icon-primary);
40
+ }
41
+
42
+ /* Same treatment the Button gives its busy mark: rotation is opt in and motion
43
+ guarded, and Reduced Motion keeps the static mark. */
44
+ @media (prefers-reduced-motion: no-preference) {
45
+ .ol8-form-message[data-ol8-tone="pending"] .ol8-form-message__icon > svg {
46
+ animation: ol8-spin 900ms linear infinite;
47
+ }
48
+ }