@oneli8/core 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 (125) 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 +130 -0
  6. package/ai-context.json +348 -0
  7. package/package.json +100 -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/icon.css +54 -0
  12. package/src/atoms/icon/icon.d.ts +19 -0
  13. package/src/atoms/icon/icon.js +81 -0
  14. package/src/atoms/icon/icons.generated.d.ts +48 -0
  15. package/src/atoms/icon/icons.generated.js +562 -0
  16. package/src/atoms/icon/index.d.ts +2 -0
  17. package/src/atoms/icon/index.js +2 -0
  18. package/src/atoms/index.d.ts +3 -0
  19. package/src/atoms/index.js +3 -0
  20. package/src/atoms/navigation-badge/index.d.ts +1 -0
  21. package/src/atoms/navigation-badge/index.js +1 -0
  22. package/src/atoms/navigation-badge/navigation-badge.css +35 -0
  23. package/src/atoms/navigation-badge/navigation-badge.d.ts +5 -0
  24. package/src/atoms/navigation-badge/navigation-badge.js +21 -0
  25. package/src/atoms/selection-indicator/index.d.ts +1 -0
  26. package/src/atoms/selection-indicator/index.js +1 -0
  27. package/src/atoms/selection-indicator/selection-indicator.css +160 -0
  28. package/src/atoms/selection-indicator/selection-indicator.d.ts +36 -0
  29. package/src/atoms/selection-indicator/selection-indicator.js +61 -0
  30. package/src/index.d.ts +3 -0
  31. package/src/index.js +3 -0
  32. package/src/materials/gem/gem.css +374 -0
  33. package/src/molecules/button/button.css +193 -0
  34. package/src/molecules/button/button.d.ts +51 -0
  35. package/src/molecules/button/button.js +134 -0
  36. package/src/molecules/button/index.d.ts +1 -0
  37. package/src/molecules/button/index.js +1 -0
  38. package/src/molecules/choice-chip/choice-chip.css +214 -0
  39. package/src/molecules/choice-chip/choice-chip.d.ts +27 -0
  40. package/src/molecules/choice-chip/choice-chip.js +170 -0
  41. package/src/molecules/choice-chip/index.d.ts +2 -0
  42. package/src/molecules/choice-chip/index.js +10 -0
  43. package/src/molecules/choice-item/choice-item.css +265 -0
  44. package/src/molecules/choice-item/choice-item.d.ts +54 -0
  45. package/src/molecules/choice-item/choice-item.js +153 -0
  46. package/src/molecules/choice-item/index.d.ts +1 -0
  47. package/src/molecules/choice-item/index.js +1 -0
  48. package/src/molecules/form-message/form-message.css +46 -0
  49. package/src/molecules/form-message/form-message.d.ts +23 -0
  50. package/src/molecules/form-message/form-message.js +73 -0
  51. package/src/molecules/form-message/index.d.ts +1 -0
  52. package/src/molecules/form-message/index.js +1 -0
  53. package/src/molecules/icon-button/icon-button.css +222 -0
  54. package/src/molecules/icon-button/icon-button.d.ts +40 -0
  55. package/src/molecules/icon-button/icon-button.js +121 -0
  56. package/src/molecules/icon-button/index.d.ts +1 -0
  57. package/src/molecules/icon-button/index.js +1 -0
  58. package/src/molecules/index.d.ts +10 -0
  59. package/src/molecules/index.js +10 -0
  60. package/src/molecules/link/index.d.ts +1 -0
  61. package/src/molecules/link/index.js +1 -0
  62. package/src/molecules/link/link.css +114 -0
  63. package/src/molecules/link/link.d.ts +27 -0
  64. package/src/molecules/link/link.js +76 -0
  65. package/src/molecules/navigation-item/index.js +1 -0
  66. package/src/molecules/navigation-item/navigation-item.css +153 -0
  67. package/src/molecules/navigation-item/navigation-item.js +95 -0
  68. package/src/molecules/select/index.js +1 -0
  69. package/src/molecules/select/select.css +31 -0
  70. package/src/molecules/select/select.js +127 -0
  71. package/src/molecules/selection-option/index.d.ts +1 -0
  72. package/src/molecules/selection-option/index.js +1 -0
  73. package/src/molecules/selection-option/selection-option.css +121 -0
  74. package/src/molecules/selection-option/selection-option.d.ts +42 -0
  75. package/src/molecules/selection-option/selection-option.js +97 -0
  76. package/src/molecules/text-field/index.d.ts +1 -0
  77. package/src/molecules/text-field/index.js +5 -0
  78. package/src/molecules/text-field/text-field.css +266 -0
  79. package/src/molecules/text-field/text-field.d.ts +69 -0
  80. package/src/molecules/text-field/text-field.js +234 -0
  81. package/src/molecules/token/index.d.ts +2 -0
  82. package/src/molecules/token/index.js +1 -0
  83. package/src/molecules/token/token.css +63 -0
  84. package/src/molecules/token/token.d.ts +35 -0
  85. package/src/molecules/token/token.js +83 -0
  86. package/src/organisms/choice-group/choice-group.css +93 -0
  87. package/src/organisms/choice-group/choice-group.d.ts +80 -0
  88. package/src/organisms/choice-group/choice-group.js +159 -0
  89. package/src/organisms/choice-group/index.d.ts +1 -0
  90. package/src/organisms/choice-group/index.js +1 -0
  91. package/src/organisms/choice-picker/choice-picker.css +98 -0
  92. package/src/organisms/choice-picker/choice-picker.d.ts +62 -0
  93. package/src/organisms/choice-picker/choice-picker.js +201 -0
  94. package/src/organisms/choice-picker/index.d.ts +1 -0
  95. package/src/organisms/choice-picker/index.js +1 -0
  96. package/src/organisms/combobox/combobox.css +40 -0
  97. package/src/organisms/combobox/combobox.js +309 -0
  98. package/src/organisms/combobox/index.js +5 -0
  99. package/src/organisms/index.d.ts +8 -0
  100. package/src/organisms/index.js +8 -0
  101. package/src/organisms/multi-select-field/index.d.ts +1 -0
  102. package/src/organisms/multi-select-field/index.js +1 -0
  103. package/src/organisms/multi-select-field/multi-select-field.css +76 -0
  104. package/src/organisms/multi-select-field/multi-select-field.d.ts +64 -0
  105. package/src/organisms/multi-select-field/multi-select-field.js +317 -0
  106. package/src/organisms/navigation-keys.js +58 -0
  107. package/src/organisms/segmented-control/index.d.ts +1 -0
  108. package/src/organisms/segmented-control/index.js +1 -0
  109. package/src/organisms/segmented-control/segmented-control.css +113 -0
  110. package/src/organisms/segmented-control/segmented-control.d.ts +43 -0
  111. package/src/organisms/segmented-control/segmented-control.js +163 -0
  112. package/src/organisms/selection-popup/index.js +1 -0
  113. package/src/organisms/selection-popup/selection-popup.css +90 -0
  114. package/src/organisms/selection-popup/selection-popup.js +100 -0
  115. package/src/organisms/tab-bar/index.d.ts +1 -0
  116. package/src/organisms/tab-bar/index.js +1 -0
  117. package/src/organisms/tab-bar/tab-bar.css +45 -0
  118. package/src/organisms/tab-bar/tab-bar.d.ts +37 -0
  119. package/src/organisms/tab-bar/tab-bar.js +104 -0
  120. package/src/organisms/tabs/index.d.ts +1 -0
  121. package/src/organisms/tabs/index.js +1 -0
  122. package/src/organisms/tabs/tabs.css +53 -0
  123. package/src/organisms/tabs/tabs.d.ts +48 -0
  124. package/src/organisms/tabs/tabs.js +152 -0
  125. package/styles.css +29 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Oneli8 · Selection Indicator (ATOM)
3
+ * Figma: Documentation / Choice Controls (246:3)
4
+ * Icon / Selection / Checkbox 365:2417 (12 variants)
5
+ * Icon / Selection / Radio 366:2371 (8)
6
+ * Icon / Selection / Switch 824:1304 (8)
7
+ *
8
+ * Figma names the tier itself — "Documentation / Atom / Selection Indicator" —
9
+ * and points at packages/react/src/atoms/SelectionIndicator.js (kind=switch),
10
+ * so all three kinds are one atom parameterised by kind, not three components.
11
+ *
12
+ * It composes no other component: the marks are drawn from native elements and
13
+ * one inline path, which is what keeps this an atom rather than a molecule.
14
+ *
15
+ * PURELY PRESENTATIONAL. It carries no role and no state of its own — the
16
+ * native <input> inside Choice Item owns semantics. Always aria-hidden.
17
+ *
18
+ * Axes are Figma's: Selection and Availability.
19
+ */
20
+
21
+ .ol8-selection {
22
+ --_box: var(--ol8-size-icon-small); /* 18px compact */
23
+ --_radius: var(--ol8-component-checkbox-radius-compact);
24
+ --_dot: var(--ol8-component-radio-dot-compact);
25
+ --_track-w: var(--ol8-component-switch-trackwidth-compact);
26
+ --_track-h: var(--ol8-component-switch-trackheight-compact);
27
+ --_thumb: var(--ol8-component-switch-thumb-compact);
28
+
29
+ --_surface: transparent;
30
+ --_edge: var(--ol8-color-border-strong);
31
+ --_ink: var(--ol8-color-selection-content);
32
+
33
+ box-sizing: border-box;
34
+ display: inline-flex;
35
+ flex: none;
36
+ align-items: center;
37
+ justify-content: center;
38
+ position: relative;
39
+ inline-size: var(--_box);
40
+ block-size: var(--_box);
41
+ background: var(--_surface);
42
+ color: var(--_ink);
43
+ }
44
+
45
+ .ol8-selection[data-ol8-size="comfortable"] {
46
+ --_box: var(--ol8-size-icon-standard); /* 24px */
47
+ --_radius: var(--ol8-component-checkbox-radius-comfortable);
48
+ --_dot: var(--ol8-component-radio-dot-comfortable);
49
+ --_track-w: var(--ol8-component-switch-trackwidth-comfortable);
50
+ --_track-h: var(--ol8-component-switch-trackheight-comfortable);
51
+ --_thumb: var(--ol8-component-switch-thumb-comfortable);
52
+ }
53
+
54
+ /* ---- Checkbox -------------------------------------------------------- */
55
+ .ol8-selection--checkbox {
56
+ border: var(--ol8-component-checkbox-boundary) solid var(--_edge);
57
+ border-radius: var(--_radius);
58
+ }
59
+ /* Filled states drop the boundary entirely so the mark occupies the whole
60
+ box, exactly as Figma draws it (glyph at 0,0 at full box size). */
61
+ .ol8-selection--checkbox[data-ol8-selection="checked"],
62
+ .ol8-selection--checkbox[data-ol8-selection="mixed"] {
63
+ border-width: 0;
64
+ --_surface: var(--ol8-color-actionprimary-default);
65
+ --_ink: var(--ol8-color-actionprimary-content);
66
+ }
67
+ .ol8-selection--checkbox[data-ol8-availability="disabled"] { --_edge: var(--ol8-color-border-disabled); }
68
+ .ol8-selection--checkbox[data-ol8-availability="disabled"][data-ol8-selection="checked"],
69
+ .ol8-selection--checkbox[data-ol8-availability="disabled"][data-ol8-selection="mixed"] {
70
+ --_surface: var(--ol8-color-surface-disabled);
71
+ --_ink: var(--ol8-color-icon-disabled);
72
+ }
73
+
74
+ /* The check glyph does NOT scale with the box, for the same reason the Mixed
75
+ bar below does not. Figma's `Icon / Selection / Check` is a governed two size
76
+ master rather than an Icon Frame instance: Size=Small (18) and Size=Standard
77
+ (24) both carry the same 10.2 x 6.9 artwork at the same 1.8 stroke, centred
78
+ in their box. So the source keeps its 24 unit box at both sizes and the
79
+ smaller box simply lets the empty margin overflow. The artwork is well
80
+ inside 18, so nothing is clipped.
81
+
82
+ This previously scaled with the box, which drew the compact check a quarter
83
+ smaller than Figma at a 1.35 stroke, off the 1.8 system. */
84
+ .ol8-selection--checkbox > .ol8-selection__mark > svg {
85
+ display: block;
86
+ inline-size: var(--ol8-size-icon-standard);
87
+ block-size: var(--ol8-size-icon-standard);
88
+ overflow: visible;
89
+ }
90
+
91
+ /* The Mixed bar does NOT scale. Both the 18px and 24px masters carry the same
92
+ 9 x 1.8px bar — an approved optical exception — so it is a fixed-size rule
93
+ here rather than a scaled glyph. */
94
+ .ol8-selection--checkbox[data-ol8-selection="mixed"] > .ol8-selection__mark {
95
+ inline-size: var(--ol8-component-checkbox-mark-mixedwidth);
96
+ block-size: var(--ol8-component-checkbox-mark-stroke);
97
+ border-radius: calc(var(--ol8-component-checkbox-mark-stroke) / 2);
98
+ background: currentColor;
99
+ }
100
+
101
+ /* ---- Radio ----------------------------------------------------------- */
102
+ .ol8-selection--radio {
103
+ border: var(--ol8-component-radio-boundary) solid var(--_edge);
104
+ border-radius: var(--ol8-shape-radius-full);
105
+ }
106
+ /* A selected radio is a checked checkbox in a circle, so it reads the same way:
107
+ the boundary drops and the whole indicator fills with the primary action
108
+ colour, carrying its own content role as the mark. Figma painted both the ring
109
+ and the dot in Selection Surface, which left the radio outlined where the
110
+ checkbox is filled and put the dot at 1.16:1 on the canvas against a 3:1
111
+ minimum. The fill is 8.81:1 and the mark 8.81:1 on the fill. */
112
+ .ol8-selection--radio[data-ol8-selection="selected"] {
113
+ border-width: 0;
114
+ --_surface: var(--ol8-color-actionprimary-default);
115
+ --_ink: var(--ol8-color-actionprimary-content);
116
+ }
117
+ .ol8-selection--radio[data-ol8-availability="disabled"] { --_edge: var(--ol8-color-border-disabled); }
118
+ .ol8-selection--radio[data-ol8-availability="disabled"][data-ol8-selection="selected"] {
119
+ border-width: 0;
120
+ --_surface: var(--ol8-color-surface-disabled);
121
+ --_ink: var(--ol8-color-icon-disabled);
122
+ }
123
+ .ol8-selection--radio > .ol8-selection__mark {
124
+ flex: none; /* never let the flex row shrink it oval */
125
+ inline-size: var(--_dot);
126
+ block-size: var(--_dot);
127
+ border-radius: var(--ol8-shape-radius-full);
128
+ background: currentColor;
129
+ }
130
+
131
+ /* ---- Switch ---------------------------------------------------------- */
132
+ .ol8-selection--switch {
133
+ inline-size: var(--_track-w);
134
+ block-size: var(--_track-h);
135
+ border-radius: var(--ol8-shape-radius-full);
136
+ --_surface: var(--ol8-color-surface-sunken);
137
+ justify-content: flex-start;
138
+ }
139
+ .ol8-selection--switch[data-ol8-selection="on"] { --_surface: var(--ol8-color-selection-surface); }
140
+ .ol8-selection--switch[data-ol8-availability="disabled"] { --_surface: var(--ol8-color-surface-disabled); }
141
+
142
+ .ol8-selection--switch > .ol8-selection__mark {
143
+ position: absolute;
144
+ inset-block-start: var(--ol8-component-switch-inset);
145
+ inset-inline-start: var(--ol8-component-switch-inset);
146
+ inline-size: var(--_thumb);
147
+ block-size: var(--_thumb);
148
+ border-radius: var(--ol8-shape-radius-full);
149
+ background: var(--ol8-color-control-thumb);
150
+ display: inline-flex;
151
+ align-items: center;
152
+ justify-content: center;
153
+ overflow: hidden;
154
+ }
155
+ .ol8-selection--switch[data-ol8-selection="on"] > .ol8-selection__mark {
156
+ inset-inline-start: calc(var(--_track-w) - var(--_thumb) - var(--ol8-component-switch-inset));
157
+ }
158
+ /* Optional artwork inside the thumb: 9px compact, 12px comfortable. */
159
+ .ol8-selection--switch .ol8-icon { --_ol8-icon-size: var(--ol8-size-icon-micro); }
160
+ .ol8-selection--switch[data-ol8-size="comfortable"] .ol8-icon { --_ol8-icon-size: var(--ol8-size-icon-compact); }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Selection Indicator (ATOM) — presentational only. It renders the mark a
3
+ * checkbox, radio or switch shows; it never owns state, role or focus.
4
+ */
5
+ export type Ol8SelectionKind = 'checkbox' | 'radio' | 'switch';
6
+ export type Ol8SelectionSize = 'compact' | 'comfortable';
7
+
8
+ export type Ol8CheckboxSelection = 'unchecked' | 'checked' | 'mixed';
9
+ export type Ol8RadioSelection = 'unselected' | 'selected';
10
+ export type Ol8SwitchSelection = 'off' | 'on';
11
+ export type Ol8Selection = Ol8CheckboxSelection | Ol8RadioSelection | Ol8SwitchSelection;
12
+
13
+ export declare const OL8_SELECTION_KINDS: readonly Ol8SelectionKind[];
14
+ export declare const OL8_SELECTION_SIZES: readonly Ol8SelectionSize[];
15
+
16
+ /** Figma's Selection axis, per kind. */
17
+ export declare const OL8_SELECTION_STATES: {
18
+ readonly checkbox: readonly Ol8CheckboxSelection[];
19
+ readonly radio: readonly Ol8RadioSelection[];
20
+ readonly switch: readonly Ol8SwitchSelection[];
21
+ };
22
+
23
+ export interface Ol8SelectionIndicatorOptions {
24
+ /** Defaults to the kind's first state (unchecked / unselected / off). */
25
+ selection?: Ol8Selection;
26
+ size?: Ol8SelectionSize;
27
+ disabled?: boolean;
28
+ /** Override the glyph. Defaults to the kind's canonical mark. */
29
+ icon?: string;
30
+ className?: string;
31
+ }
32
+
33
+ export declare function renderSelectionIndicator(
34
+ kind: Ol8SelectionKind,
35
+ options?: Ol8SelectionIndicatorOptions,
36
+ ): string;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Oneli8 · Selection Indicator (ATOM) — headless.
3
+ *
4
+ * Presentational only. It renders the mark a checkbox, radio or switch shows;
5
+ * it never owns state, role or focus. The native <input> in Choice Item does.
6
+ *
7
+ * Kept an atom by the project's rule that atoms compose no other component:
8
+ * the check glyph is inlined as a path here rather than mounting the icon
9
+ * component. renderIconSvg returns a markup string, not a component instance.
10
+ */
11
+ import { renderIconSvg } from '../icon/icon.js';
12
+
13
+ export const OL8_SELECTION_KINDS = ['checkbox', 'radio', 'switch'];
14
+ export const OL8_SELECTION_SIZES = ['compact', 'comfortable'];
15
+
16
+ /** Figma's Selection axis, per kind. */
17
+ export const OL8_SELECTION_STATES = {
18
+ checkbox: ['unchecked', 'checked', 'mixed'],
19
+ radio: ['unselected', 'selected'],
20
+ switch: ['off', 'on'],
21
+ };
22
+
23
+ /**
24
+ * @param {'checkbox'|'radio'|'switch'} kind
25
+ * @param {{selection?:string,size?:string,disabled?:boolean,icon?:string,className?:string}} [options]
26
+ */
27
+ export function renderSelectionIndicator(kind, options = {}) {
28
+ if (!OL8_SELECTION_KINDS.includes(kind)) throw new Error(`[ol8] unknown selection kind "${kind}"`);
29
+ const states = OL8_SELECTION_STATES[kind];
30
+ const { selection = states[0], size = 'compact', disabled = false, icon, className } = options;
31
+
32
+ if (!states.includes(selection)) {
33
+ throw new Error(`[ol8] "${selection}" is not a ${kind} selection state (${states.join(', ')})`);
34
+ }
35
+ if (!OL8_SELECTION_SIZES.includes(size)) throw new Error(`[ol8] unknown selection size "${size}"`);
36
+ if (icon && kind !== 'switch') throw new Error('[ol8] only the switch thumb carries optional artwork');
37
+
38
+ const attrs = [
39
+ `class="ol8-selection ol8-selection--${kind}${className ? ` ${className}` : ''}"`,
40
+ `data-ol8-size="${size}"`,
41
+ `data-ol8-selection="${selection}"`,
42
+ `data-ol8-availability="${disabled ? 'disabled' : 'enabled'}"`,
43
+ 'aria-hidden="true"',
44
+ ].join(' ');
45
+
46
+ return `<span ${attrs}>${renderMark(kind, selection, icon)}</span>`;
47
+ }
48
+
49
+ function renderMark(kind, selection, icon) {
50
+ if (kind === 'checkbox') {
51
+ if (selection === 'checked') return `<span class="ol8-selection__mark">${renderIconSvg('check')}</span>`;
52
+ if (selection === 'mixed') return '<span class="ol8-selection__mark"></span>';
53
+ return '';
54
+ }
55
+ if (kind === 'radio') {
56
+ return selection === 'selected' ? '<span class="ol8-selection__mark"></span>' : '';
57
+ }
58
+ // switch: the thumb is always present; artwork inside it is optional
59
+ const art = icon ? `<span class="ol8-icon">${renderIconSvg(icon)}</span>` : '';
60
+ return `<span class="ol8-selection__mark">${art}</span>`;
61
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './atoms/index.js';
2
+ export * from './molecules/index.js';
3
+ export * from './organisms/index.js';
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './atoms/index.js';
2
+ export * from './molecules/index.js';
3
+ export * from './organisms/index.js';
@@ -0,0 +1,374 @@
1
+ /**
2
+ * Oneli8 · Gem material adapter
3
+ * Figma: section "Gem Button" (1034:9969) — 144 components across
4
+ * Button/Primary/Gem 100:3, Button/Secondary/Gem 100:208,
5
+ * Icon Button ±Circle Primary/Secondary Gem 102:78, 102:151, 119:202, 119:299.
6
+ *
7
+ * From the Figma description (100:3):
8
+ * "Material adapter — NOT a fifth semantic variant. Geometry, meaning,
9
+ * accessibility, protected target, Size, State, label/icon content and
10
+ * native behavior are inherited from the regular molecule. Material changes
11
+ * only the token-bound surface and inner focus edge. Reduced Transparency
12
+ * resolves to the regular opaque equivalent. Quiet and Destructive use the
13
+ * regular material treatment until a distinct Gem treatment is explicitly
14
+ * approved."
15
+ *
16
+ * So Gem is an ATTRIBUTE on an existing button, never a new class:
17
+ * <button class="ol8-btn ol8-btn--primary" data-ol8-material="gem">
18
+ *
19
+ * It adds exactly two painted layers and touches nothing else:
20
+ * ::before the three-stop body + backdrop blur
21
+ * ::after the rim (specular top edge + integrated light rim)
22
+ *
23
+ * THE GRADIENT ANGLE
24
+ * Figma bakes a per-instance angle: 153.1595deg at Standard (83x42),
25
+ * 151.6609 at Comfortable (89x48), 149.2872 at Large (101x60). Those are not
26
+ * three different designs — every one satisfies
27
+ * theta = 90deg + atan(W / H)
28
+ * to four decimal places, which is exactly CSS's `to bottom right` corner
29
+ * keyword (gradient line perpendicular to the other diagonal). Using the
30
+ * keyword reproduces Figma at every size AND at any label width, where a baked
31
+ * degree value would only be correct for the one instance it was measured on.
32
+ */
33
+
34
+ .ol8-btn[data-ol8-material="gem"],
35
+ .ol8-icon-btn[data-ol8-material="gem"] > .ol8-icon-btn__surface,
36
+ .ol8-field[data-ol8-material="gem"] > .ol8-field__control-stack > .ol8-field__control {
37
+ position: relative;
38
+ isolation: isolate;
39
+ --_rim: var(--ol8-material-control-gem--rim-default);
40
+ /* Defined on every Gem surface, not just one, because the halo is read by
41
+ descendants: text through text-shadow and control geometry through chained
42
+ drop-shadow filters. Both are built from the same two tokens and repeated
43
+ the same four times, which is what makes a label's glow and a control's
44
+ glow one field instead of two near-misses. */
45
+ --_gem-glow: 0 0 var(--ol8-material-gem-blur-contentglow)
46
+ var(--ol8-material-gem-color-quiet-contentglow);
47
+ /* Control geometry takes one stack FEWER than text, on purpose. WCAG 1.4.11
48
+ asks 3:1 of a graphical object where 1.4.3 asks 4.5:1 of text, so the
49
+ boundary does not need the fourth repetition — and leaving it off is what
50
+ keeps the indicator blended into the glass instead of ringed. Measured at
51
+ 3px: text in four stacks is 5.06:1, the boundary in three is 3.75:1. */
52
+ --_gem-glow-filter:
53
+ drop-shadow(var(--_gem-glow)) drop-shadow(var(--_gem-glow))
54
+ drop-shadow(var(--_gem-glow));
55
+ }
56
+
57
+ /* The regular opaque background stays underneath on purpose — it is what
58
+ shows through when the gem layer is suppressed for Reduced Transparency. */
59
+ .ol8-btn[data-ol8-material="gem"]::before,
60
+ .ol8-icon-btn[data-ol8-material="gem"] > .ol8-icon-btn__surface::before,
61
+ .ol8-field[data-ol8-material="gem"] .ol8-field__control::before {
62
+ content: "";
63
+ position: absolute;
64
+ inset: 0;
65
+ z-index: -1;
66
+ border-radius: inherit;
67
+ pointer-events: none;
68
+ backdrop-filter: blur(var(--ol8-material-control-gem--blur));
69
+ -webkit-backdrop-filter: blur(var(--ol8-material-control-gem--blur));
70
+ background-image: linear-gradient(
71
+ to bottom right,
72
+ var(--_g1) var(--ol8-material-control-gem--gradient-start),
73
+ var(--_g2) var(--ol8-material-control-gem--gradient-mid),
74
+ var(--_g3) var(--ol8-material-control-gem--gradient-end));
75
+ }
76
+
77
+ .ol8-btn[data-ol8-material="gem"]::after,
78
+ .ol8-icon-btn[data-ol8-material="gem"] > .ol8-icon-btn__surface::after,
79
+ .ol8-field[data-ol8-material="gem"] .ol8-field__control::after {
80
+ content: "";
81
+ position: absolute;
82
+ inset: 0;
83
+ border-radius: inherit;
84
+ pointer-events: none;
85
+ /* Figma's two inner shadows on the Control frame, in order:
86
+ INNER_SHADOW offset 0,2 radius 1 spread 0 -> Rim / Specular
87
+ INNER_SHADOW offset 0,0 radius 0 spread 1 -> Rim / Default
88
+ The second one was written with spread 0, which draws nothing, so the
89
+ integrated light rim never painted even once the variable names were
90
+ right. It is the 1px edge that reads as the bezel. */
91
+ box-shadow:
92
+ inset 0 2px 1px 0 var(--ol8-material-control-gem--rim-specular),
93
+ inset 0 0 0 1px var(--_rim);
94
+ }
95
+
96
+ /* ---- Primary · three-stop same-hue body ------------------------------ */
97
+ .ol8-btn--primary[data-ol8-material="gem"],
98
+ .ol8-icon-btn--primary[data-ol8-material="gem"] > .ol8-icon-btn__surface {
99
+ --_g1: var(--ol8-material-control-gem--primary-start);
100
+ --_g2: var(--ol8-material-control-gem--primary-mid);
101
+ --_g3: var(--ol8-material-control-gem--primary-end);
102
+ --_fg: var(--ol8-material-control-gem--primary-content);
103
+ --_ink: var(--ol8-material-control-gem--primary-content);
104
+ }
105
+ .ol8-btn--primary[data-ol8-material="gem"]:hover:not(:disabled):not([data-ol8-loading]),
106
+ .ol8-icon-btn--primary[data-ol8-material="gem"]:hover:not(:disabled):not([data-ol8-loading]) > .ol8-icon-btn__surface {
107
+ --_g1: var(--ol8-material-control-gem--primary-hover-start);
108
+ --_g2: var(--ol8-material-control-gem--primary-hover-mid);
109
+ --_g3: var(--ol8-material-control-gem--primary-hover-end);
110
+ --_rim: var(--ol8-material-control-gem--rim-hover);
111
+ }
112
+ /* Pressed reuses the HOVER stops and adds the cut-edge drop shadow. */
113
+ .ol8-btn--primary[data-ol8-material="gem"]:active:not(:disabled),
114
+ .ol8-btn--primary[data-ol8-material="gem"][aria-pressed="true"]:not(:disabled),
115
+ .ol8-icon-btn--primary[data-ol8-material="gem"]:active:not(:disabled) > .ol8-icon-btn__surface,
116
+ .ol8-icon-btn--primary[data-ol8-material="gem"][aria-pressed="true"]:not(:disabled) > .ol8-icon-btn__surface {
117
+ --_g1: var(--ol8-material-control-gem--primary-hover-start);
118
+ --_g2: var(--ol8-material-control-gem--primary-hover-mid);
119
+ --_g3: var(--ol8-material-control-gem--primary-hover-end);
120
+ --_rim: var(--ol8-material-control-gem--rim-hover);
121
+ }
122
+ .ol8-btn--primary[data-ol8-material="gem"]:active:not(:disabled),
123
+ .ol8-btn--primary[data-ol8-material="gem"][aria-pressed="true"]:not(:disabled) {
124
+ filter: drop-shadow(0 9px 12px var(--ol8-material-gem-color-cutedge-shadow));
125
+ }
126
+ .ol8-btn--primary[data-ol8-material="gem"][data-ol8-loading],
127
+ .ol8-icon-btn--primary[data-ol8-material="gem"][data-ol8-loading] > .ol8-icon-btn__surface {
128
+ --_g1: var(--ol8-material-control-gem--primary-loading-start);
129
+ --_g2: var(--ol8-material-control-gem--primary-loading-mid);
130
+ --_g3: var(--ol8-material-control-gem--primary-loading-end);
131
+ }
132
+
133
+ /* ---- Secondary · transparent body with an integrated light rim -------
134
+ The regular Secondary's 2px boundary is dropped in Gem; the rim replaces
135
+ it. Reduced Transparency restores the boundary along with the opaque body. */
136
+ .ol8-btn--secondary[data-ol8-material="gem"] { --_bw: 0px; }
137
+ .ol8-btn--secondary[data-ol8-material="gem"],
138
+ .ol8-icon-btn--secondary[data-ol8-material="gem"] > .ol8-icon-btn__surface {
139
+ --_g1: var(--ol8-material-control-gem--secondary-high);
140
+ --_g2: var(--ol8-material-control-gem--secondary-mid);
141
+ --_g3: var(--ol8-material-control-gem--secondary-low);
142
+ --_fg: var(--ol8-material-control-gem--secondary-content);
143
+ --_ink: var(--ol8-material-control-gem--secondary-content);
144
+ --_bd: transparent;
145
+ }
146
+ .ol8-btn--secondary[data-ol8-material="gem"]:hover:not(:disabled):not([data-ol8-loading]),
147
+ .ol8-icon-btn--secondary[data-ol8-material="gem"]:hover:not(:disabled):not([data-ol8-loading]) > .ol8-icon-btn__surface {
148
+ --_g1: var(--ol8-material-control-gem--secondary-hover-high);
149
+ --_g2: var(--ol8-material-control-gem--secondary-hover-mid);
150
+ --_g3: var(--ol8-material-control-gem--secondary-hover-low);
151
+ --_rim: var(--ol8-material-control-gem--rim-hover);
152
+ --_fg: var(--ol8-material-control-gem--secondary-content);
153
+ }
154
+ .ol8-btn--secondary[data-ol8-material="gem"]:active:not(:disabled),
155
+ .ol8-btn--secondary[data-ol8-material="gem"][aria-pressed="true"]:not(:disabled),
156
+ .ol8-icon-btn--secondary[data-ol8-material="gem"]:active:not(:disabled) > .ol8-icon-btn__surface {
157
+ --_g1: var(--ol8-material-control-gem--secondary-hover-high);
158
+ --_g2: var(--ol8-material-control-gem--secondary-hover-mid);
159
+ --_g3: var(--ol8-material-control-gem--secondary-hover-low);
160
+ --_rim: var(--ol8-material-control-gem--rim-hover);
161
+ --_fg: var(--ol8-material-control-gem--secondary-content);
162
+ }
163
+ .ol8-btn--secondary[data-ol8-material="gem"][data-ol8-loading],
164
+ .ol8-icon-btn--secondary[data-ol8-material="gem"][data-ol8-loading] > .ol8-icon-btn__surface {
165
+ --_g1: var(--ol8-material-control-gem--secondary-loading-high);
166
+ --_g2: var(--ol8-material-control-gem--secondary-loading-mid);
167
+ --_g3: var(--ol8-material-control-gem--secondary-loading-low);
168
+ }
169
+
170
+ /* ---- Text Field · the same Secondary body, per Figma 211:36 and 215:226 ---
171
+ Outline follows the Cut Edge, Filled sits on the Stabilized Face and follows
172
+ the Inner Thin Cut. Hover moves Filled onto the Cut Edge. Invalid keeps the
173
+ critical boundary, because a material must never hide a validation result. */
174
+ .ol8-field[data-ol8-material="gem"] .ol8-field__control {
175
+ --_g1: var(--ol8-material-control-gem--secondary-high);
176
+ --_g2: var(--ol8-material-control-gem--secondary-mid);
177
+ --_g3: var(--ol8-material-control-gem--secondary-low);
178
+ --_surface: transparent;
179
+ --_border: var(--ol8-material-gem-color-cutedge-high);
180
+ }
181
+ .ol8-field[data-ol8-material="gem"]:hover:not([data-ol8-disabled]) .ol8-field__control {
182
+ --_g1: var(--ol8-material-control-gem--secondary-hover-high);
183
+ --_g2: var(--ol8-material-control-gem--secondary-hover-mid);
184
+ --_g3: var(--ol8-material-control-gem--secondary-hover-low);
185
+ --_rim: var(--ol8-material-control-gem--rim-hover);
186
+ --_border: var(--ol8-material-gem-color-cutedge-high);
187
+ }
188
+ .ol8-field[data-ol8-material="gem"][data-ol8-appearance="filled"] .ol8-field__control {
189
+ --_surface: var(--ol8-material-gem-color-quiet-stabilizedface);
190
+ --_border: var(--ol8-material-gem-color-innerthincut-high);
191
+ }
192
+ /* Filled is the only gem field appearance that sits ON the quiet face; outline
193
+ leaves --_surface transparent and keeps whatever is behind it. So the
194
+ content colour moves here and nowhere else.
195
+
196
+ WHICH NODES, EXACTLY: only what is physically inside .ol8-field__control,
197
+ because that box is the gem face. The label is NOT — .ol8-field__label lives
198
+ in .ol8-field__label-row, a SIBLING of .ol8-field__control-stack, so it sits
199
+ on the page background above the field. Figma agrees: "Label Row / Label" is
200
+ a sibling of "Control Stack" in Text Field / Filled / Gem (215:226). Giving
201
+ the label gem ink put near-white text on a light page — 1.26:1, invisible.
202
+ The affixes and icons DO sit inside the control and so do belong here. */
203
+ .ol8-field[data-ol8-material="gem"][data-ol8-appearance="filled"]:not([data-ol8-disabled]) .ol8-field__input,
204
+ .ol8-field[data-ol8-material="gem"][data-ol8-appearance="filled"]:not([data-ol8-disabled]) .ol8-field__affix {
205
+ color: var(--ol8-material-gem-color-quiet-content);
206
+ /* Stacked three times on purpose. A single shadow at this radius reaches
207
+ only 1.71:1 against near-white ink on a bright plate, and widening the
208
+ radius makes it WEAKER because blur spreads a fixed alpha budget over more
209
+ area. Repeating the same shadow accumulates alpha instead of spreading it:
210
+ 1 - (1 - a)^4. Measured in proof/gem-halo-contrast.html. */
211
+ text-shadow: var(--_gem-glow), var(--_gem-glow),
212
+ var(--_gem-glow), var(--_gem-glow);
213
+ }
214
+ /* Icons on the face take the token that already exists for exactly this and
215
+ is already stable across appearances. */
216
+ .ol8-field[data-ol8-material="gem"][data-ol8-appearance="filled"]:not([data-ol8-disabled]) .ol8-field__leading-icon,
217
+ .ol8-field[data-ol8-material="gem"][data-ol8-appearance="filled"]:not([data-ol8-disabled]) .ol8-field__trailing-action {
218
+ color: var(--ol8-color-icon-on-gem);
219
+ }
220
+ .ol8-field[data-ol8-material="gem"][data-ol8-readonly][data-ol8-appearance="filled"] .ol8-field__control {
221
+ --_border: var(--ol8-material-gem-color-innerthincut-high);
222
+ }
223
+ .ol8-field[data-ol8-material="gem"]:has(.ol8-field__input[aria-invalid="true"]) .ol8-field__control,
224
+ .ol8-field[data-ol8-material="gem"][data-ol8-status="invalid"] .ol8-field__control {
225
+ --_border: var(--ol8-color-feedbackcritical-inlinecontent);
226
+ }
227
+ /* Disabled resolves to the opaque equivalent, which is the same answer Reduced
228
+ Transparency gives. */
229
+ .ol8-field[data-ol8-material="gem"][data-ol8-disabled] .ol8-field__control {
230
+ --_surface: var(--ol8-material-gem-color-quiet-opaqueequivalent);
231
+ --_border: var(--ol8-color-border-disabled);
232
+ }
233
+ .ol8-field[data-ol8-material="gem"][data-ol8-disabled] .ol8-field__control::before,
234
+ .ol8-field[data-ol8-material="gem"][data-ol8-disabled] .ol8-field__control::after { display: none; }
235
+
236
+ /* Content must sit above the ::before body. */
237
+ .ol8-btn[data-ol8-material="gem"] > *,
238
+ .ol8-icon-btn[data-ol8-material="gem"] .ol8-icon,
239
+ .ol8-field[data-ol8-material="gem"] .ol8-field__control > * { position: relative; z-index: 0; }
240
+
241
+ /* ---- Disabled inherits the regular opaque treatment ------------------ */
242
+ .ol8-btn[data-ol8-material="gem"]:disabled::before,
243
+ .ol8-btn[data-ol8-material="gem"]:disabled::after,
244
+ .ol8-icon-btn[data-ol8-material="gem"]:disabled > .ol8-icon-btn__surface::before,
245
+ .ol8-icon-btn[data-ol8-material="gem"]:disabled > .ol8-icon-btn__surface::after { display: none; }
246
+
247
+ /* ---- Reduced Transparency -> the regular opaque equivalent -----------
248
+ Explicitly required by the Figma description. Hiding the gem layers
249
+ reveals the regular molecule's own background, which was never removed. */
250
+ @media (prefers-reduced-transparency: reduce) {
251
+ .ol8-btn[data-ol8-material="gem"]::before,
252
+ .ol8-btn[data-ol8-material="gem"]::after,
253
+ .ol8-icon-btn[data-ol8-material="gem"] > .ol8-icon-btn__surface::before,
254
+ .ol8-icon-btn[data-ol8-material="gem"] > .ol8-icon-btn__surface::after,
255
+ .ol8-field[data-ol8-material="gem"] .ol8-field__control::before,
256
+ .ol8-field[data-ol8-material="gem"] .ol8-field__control::after { display: none; }
257
+ .ol8-field[data-ol8-material="gem"] .ol8-field__control {
258
+ --_surface: var(--ol8-material-gem-color-quiet-opaqueequivalent);
259
+ }
260
+ .ol8-btn--secondary[data-ol8-material="gem"] {
261
+ --_bw: var(--ol8-component-button-secondaryboundary);
262
+ --_bd: var(--ol8-color-actionsecondary-contentdefault);
263
+ --_fg: var(--ol8-color-actionsecondary-contentdefault);
264
+ }
265
+ .ol8-btn--primary[data-ol8-material="gem"] { --_fg: var(--ol8-color-actionprimary-content); }
266
+ .ol8-btn[data-ol8-material="gem"]:active { filter: none; }
267
+ }
268
+
269
+ /* ---- Generic gem surface ---------------------------------------------
270
+ Until now Gem was reachable only through ol8-btn, ol8-icon-btn and
271
+ ol8-field. Anything else needing the material — a panel, a widget, a
272
+ sheet — had nowhere to go, which is exactly how hand-rolled gem starts.
273
+ This is the primitive: the quiet gem face plus the cut edge, and nothing
274
+ about what the surface contains.
275
+
276
+ The face axis is Figma's own vocabulary, not an invention:
277
+ stabilizedface neutral-030 @ 27% at rest
278
+ environmentalface neutral-030 @ 18% when the environment matters
279
+ Both are clear glass on the same near-white tint; the pair differs only in
280
+ how present the face is, never in hue.
281
+ Motion study 01 (labs/gem-in-motion) drives data-ol8-face along it. */
282
+ .ol8-gem {
283
+ --_gem-face: var(--ol8-material-gem-color-quiet-stabilizedface);
284
+ /* material.gem.blur.* existed in the tokens and nothing consumed it —
285
+ every gem surface was using the CONTROL blur instead, which is a flat
286
+ 12px. These are the surface pair, and they move with the face:
287
+ stabilized 12px, environmental 18px. That step is the dynamic blur. */
288
+ --_gem-blur: var(--ol8-material-gem-blur-stabilized);
289
+
290
+ box-sizing: border-box;
291
+ position: relative;
292
+ background: var(--_gem-face);
293
+ backdrop-filter: blur(var(--_gem-blur)) saturate(1.06);
294
+ -webkit-backdrop-filter: blur(var(--_gem-blur)) saturate(1.06);
295
+ transition: background .4s linear, backdrop-filter .4s linear;
296
+ border-radius: var(--ol8-shape-radius-standard);
297
+ /* Gem carries LIGHT content. color.icon.onGem and both control gem content
298
+ tokens were already neutral-030; the quiet face just never had a content
299
+ token of its own, so surfaces using it fell back to dark body text.
300
+ A transparent face cannot guarantee its own backdrop, so the content
301
+ carries a halo rather than the surface being thickened to provide one. */
302
+ --_gem-glow: 0 0 var(--ol8-material-gem-blur-contentglow)
303
+ var(--ol8-material-gem-color-quiet-contentglow);
304
+ /* Control geometry takes one stack FEWER than text, on purpose. WCAG 1.4.11
305
+ asks 3:1 of a graphical object where 1.4.3 asks 4.5:1 of text, so the
306
+ boundary does not need the fourth repetition — and leaving it off is what
307
+ keeps the indicator blended into the glass instead of ringed. Measured at
308
+ 3px: text in four stacks is 5.06:1, the boundary in three is 3.75:1. */
309
+ --_gem-glow-filter:
310
+ drop-shadow(var(--_gem-glow)) drop-shadow(var(--_gem-glow))
311
+ drop-shadow(var(--_gem-glow));
312
+ color: var(--ol8-material-gem-color-quiet-content);
313
+ /* Stacked three times on purpose. A single shadow at this radius reaches
314
+ only 1.71:1 against near-white ink on a bright plate, and widening the
315
+ radius makes it WEAKER because blur spreads a fixed alpha budget over more
316
+ area. Repeating the same shadow accumulates alpha instead of spreading it:
317
+ 1 - (1 - a)^4. Measured in proof/gem-halo-contrast.html. */
318
+ text-shadow: var(--_gem-glow), var(--_gem-glow),
319
+ var(--_gem-glow), var(--_gem-glow);
320
+ /* The cut edge. Invariant — it is what keeps a gem recognisable as gem. */
321
+ box-shadow:
322
+ inset 0 2px 1px 0 var(--ol8-material-gem-color-cutedge-high),
323
+ inset 0 0 0 1px var(--ol8-material-gem-color-cutedge-mid),
324
+ 0 9px 12px var(--ol8-material-gem-color-cutedge-shadow);
325
+ }
326
+
327
+ /* Disabled on a generic Gem surface, same single grey, same absent halo. */
328
+ .ol8-gem [data-ol8-availability="disabled"] .ol8-selection,
329
+ .ol8-gem [disabled] .ol8-selection {
330
+ --_edge: var(--ol8-material-gem-color-quiet-contentdisabled);
331
+ --_ink: var(--ol8-material-gem-color-quiet-contentdisabled);
332
+ filter: none;
333
+ }
334
+
335
+ /* Content on a generic Gem surface, same rule as the Choice Item carries:
336
+ glyphs and control geometry both take the gem ink and share one halo
337
+ definition, so a label's glow and a control's glow are the same field. Text
338
+ uses text-shadow; an SVG stroke cannot, so it uses chained drop-shadow
339
+ filters built from the identical two tokens. */
340
+ .ol8-gem .ol8-selection {
341
+ --_edge: var(--ol8-material-gem-color-quiet-content);
342
+ filter: var(--_gem-glow-filter);
343
+ }
344
+ .ol8-gem .ol8-icon {
345
+ color: var(--ol8-color-icon-on-gem);
346
+ filter: var(--_gem-glow-filter);
347
+ }
348
+
349
+ .ol8-gem[data-ol8-face="environmental"] {
350
+ --_gem-face: var(--ol8-material-gem-color-quiet-environmentalface);
351
+ --_gem-blur: var(--ol8-material-gem-blur-environmental);
352
+ }
353
+
354
+ /* REDUCED TRANSPARENCY, THE WORN-DISPLAY READING OF IT
355
+ Gem is worn, so the background is the real world and an opaque panel would
356
+ occlude the street for the person who needs help reading the interface most.
357
+ The reduced mode is therefore denser, not solid: legibilityFace at 0.72
358
+ leaves 28% of the scene visible and still measures 7.70:1 against near-white
359
+ ink in the DARKEST-to-BRIGHTEST worst region of a real daylight plate, with
360
+ no credit taken for the halo. That is AAA while remaining see-through.
361
+ Opt in per surface, since the platform flag cannot tell a phone from a lens. */
362
+ .ol8-gem[data-ol8-transparency="reduced"] {
363
+ --_gem-face: var(--ol8-material-gem-color-quiet-legibilityface);
364
+ }
365
+
366
+ /* Same contract as every other gem consumer: transparency is the variable,
367
+ the opaque equivalent is the floor. */
368
+ @media (prefers-reduced-transparency: reduce) {
369
+ .ol8-gem {
370
+ background: var(--ol8-material-gem-color-quiet-opaqueequivalent);
371
+ backdrop-filter: none;
372
+ -webkit-backdrop-filter: none;
373
+ }
374
+ }