@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.
- package/AGENTS.md +16 -0
- package/LICENSE +202 -0
- package/NOTICE +8 -0
- package/PRINCIPLES.md +48 -0
- package/README.md +136 -0
- package/ai-context.json +264 -0
- package/package.json +66 -0
- package/skills/oneli8-figma-to-code/SKILL.md +25 -0
- package/skills/oneli8-icons/SKILL.md +36 -0
- package/skills/oneli8-ui/SKILL.md +52 -0
- package/src/atoms/Icon.js +34 -0
- package/src/atoms/NavigationBadge.js +19 -0
- package/src/atoms/SelectionIndicator.js +43 -0
- package/src/foundations/geometry.js +143 -0
- package/src/foundations/icons.generated.d.ts +48 -0
- package/src/foundations/icons.generated.js +562 -0
- package/src/index.d.ts +381 -0
- package/src/index.js +23 -0
- package/src/molecules/Button.js +50 -0
- package/src/molecules/ChoiceChip.js +101 -0
- package/src/molecules/ChoiceItem.js +55 -0
- package/src/molecules/FormMessage.js +33 -0
- package/src/molecules/IconButton.js +33 -0
- package/src/molecules/Link.js +31 -0
- package/src/molecules/NavigationItem.js +55 -0
- package/src/molecules/Select.js +109 -0
- package/src/molecules/SelectionOption.js +44 -0
- package/src/molecules/TextField.js +174 -0
- package/src/molecules/Token.js +49 -0
- package/src/organisms/ChoiceGroup.js +78 -0
- package/src/organisms/ChoicePicker.js +114 -0
- package/src/organisms/Combobox.js +122 -0
- package/src/organisms/MultiSelectField.js +138 -0
- package/src/organisms/SegmentedControl.js +114 -0
- package/src/organisms/SelectionPopup.js +73 -0
- package/src/organisms/TabBar.js +68 -0
- package/src/organisms/Tabs.js +123 -0
- package/src/styles/button.css +195 -0
- package/src/styles/choice-chip.css +216 -0
- package/src/styles/choice-group.css +95 -0
- package/src/styles/choice-item.css +267 -0
- package/src/styles/choice-picker.css +100 -0
- package/src/styles/combobox.css +42 -0
- package/src/styles/form-message.css +48 -0
- package/src/styles/gem.css +376 -0
- package/src/styles/icon-button.css +224 -0
- package/src/styles/icon.css +56 -0
- package/src/styles/link.css +116 -0
- package/src/styles/multi-select-field.css +78 -0
- package/src/styles/navigation-badge.css +37 -0
- package/src/styles/navigation-item.css +155 -0
- package/src/styles/segmented-control.css +115 -0
- package/src/styles/select.css +33 -0
- package/src/styles/selection-indicator.css +162 -0
- package/src/styles/selection-option.css +123 -0
- package/src/styles/selection-popup.css +92 -0
- package/src/styles/tab-bar.css +47 -0
- package/src/styles/tabs.css +55 -0
- package/src/styles/text-field.css +268 -0
- package/src/styles/token.css +65 -0
- package/styles.css +29 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createElement, forwardRef, useCallback, useId, useRef, useState } from 'react';
|
|
2
|
+
import { OL8_TABS_HIERARCHIES, OL8_TABS_ORIENTATIONS, OL8_TABS_ACTIVATIONS, OL8_NAVIGATION_SIZES } from '../foundations/geometry.js';
|
|
3
|
+
import { NavigationItem } from '../molecules/NavigationItem.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ORGANISM. Figma 693:171.
|
|
7
|
+
*
|
|
8
|
+
* "Production emits tablist, tab and tabpanel, requires a non empty accessible
|
|
9
|
+
* region name, retains one enabled Tab stop, mirrors horizontal Arrow keys in
|
|
10
|
+
* RTL, and supports horizontal/vertical orientation plus automatic/manual
|
|
11
|
+
* activation." Every clause is enforced here rather than described.
|
|
12
|
+
*
|
|
13
|
+
* Tabs owns the group. Consumers pass items and panels; they never assemble a
|
|
14
|
+
* tablist by hand, which is what keeps selection and focus in one place.
|
|
15
|
+
*/
|
|
16
|
+
export const Tabs = forwardRef(function Tabs({
|
|
17
|
+
label, items = [], value, defaultValue, onChange,
|
|
18
|
+
hierarchy = 'primary', size = 'standard', material = 'regular',
|
|
19
|
+
orientation = 'horizontal', activation = 'automatic',
|
|
20
|
+
renderPanel, className = '', ...rest
|
|
21
|
+
}, ref) {
|
|
22
|
+
if (typeof label !== 'string' || label === '') {
|
|
23
|
+
throw new Error('[ol8] a tab list requires an accessible name. A region nobody can name is a region nobody can reach.');
|
|
24
|
+
}
|
|
25
|
+
if (!Array.isArray(items) || items.length === 0) throw new Error('[ol8] a tab list needs at least one tab.');
|
|
26
|
+
if (!OL8_TABS_HIERARCHIES.includes(hierarchy)) throw new Error(`[ol8] unknown tabs hierarchy "${hierarchy}"`);
|
|
27
|
+
if (!OL8_NAVIGATION_SIZES.includes(size)) throw new Error(`[ol8] unknown tabs size "${size}"`);
|
|
28
|
+
if (!OL8_TABS_ORIENTATIONS.includes(orientation)) throw new Error(`[ol8] unknown tabs orientation "${orientation}"`);
|
|
29
|
+
if (!OL8_TABS_ACTIVATIONS.includes(activation)) throw new Error(`[ol8] unknown tabs activation "${activation}"`);
|
|
30
|
+
if (items.every(i => i.disabled)) throw new Error('[ol8] a tab list must keep one enabled tab stop.');
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
for (const item of items) {
|
|
33
|
+
if (!item.id) throw new Error('[ol8] every tab needs an id, because exactly one panel is paired with it.');
|
|
34
|
+
if (seen.has(item.id)) throw new Error(`[ol8] two tabs share the id "${item.id}"`);
|
|
35
|
+
seen.add(item.id);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const prefix = useId();
|
|
39
|
+
const firstEnabled = items.find(i => !i.disabled);
|
|
40
|
+
const [internal, setInternal] = useState(defaultValue ?? firstEnabled?.id);
|
|
41
|
+
const selected = value ?? internal;
|
|
42
|
+
const listRef = useRef(null);
|
|
43
|
+
|
|
44
|
+
const select = useCallback((id) => {
|
|
45
|
+
if (value === undefined) setInternal(id);
|
|
46
|
+
onChange?.(id);
|
|
47
|
+
}, [value, onChange]);
|
|
48
|
+
|
|
49
|
+
const onKeyDown = useCallback((event) => {
|
|
50
|
+
const buttons = [...(listRef.current?.querySelectorAll('.ol8-nav-item--tab') ?? [])];
|
|
51
|
+
const at = buttons.indexOf(document.activeElement);
|
|
52
|
+
if (at === -1) return;
|
|
53
|
+
|
|
54
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
55
|
+
if (activation === 'manual') { event.preventDefault(); select(items[at].id); }
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// "Mirrors horizontal Arrow keys in RTL": the browser's own direction
|
|
60
|
+
// decides which physical key means forward.
|
|
61
|
+
const rtl = getComputedStyle(listRef.current).direction === 'rtl';
|
|
62
|
+
const forward = orientation === 'vertical' ? 'ArrowDown' : (rtl ? 'ArrowLeft' : 'ArrowRight');
|
|
63
|
+
const back = orientation === 'vertical' ? 'ArrowUp' : (rtl ? 'ArrowRight' : 'ArrowLeft');
|
|
64
|
+
|
|
65
|
+
let next = -1;
|
|
66
|
+
if (event.key === 'Home') next = items.findIndex(i => !i.disabled);
|
|
67
|
+
else if (event.key === 'End') next = items.map(i => !i.disabled).lastIndexOf(true);
|
|
68
|
+
else if (event.key === forward || event.key === back) {
|
|
69
|
+
const step = event.key === forward ? 1 : -1;
|
|
70
|
+
// "Disabled choices are skipped by roving focus."
|
|
71
|
+
for (let hop = 1; hop <= items.length; hop += 1) {
|
|
72
|
+
const i = (at + step * hop + items.length * hop) % items.length;
|
|
73
|
+
if (!items[i].disabled) { next = i; break; }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (next === -1 || next === at) return;
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
buttons[next].focus();
|
|
79
|
+
if (activation !== 'manual') select(items[next].id);
|
|
80
|
+
}, [items, orientation, activation, select]);
|
|
81
|
+
|
|
82
|
+
const list = createElement('div', {
|
|
83
|
+
...rest, ref: (node) => {
|
|
84
|
+
listRef.current = node;
|
|
85
|
+
if (typeof ref === 'function') ref(node); else if (ref) ref.current = node;
|
|
86
|
+
},
|
|
87
|
+
className: `ol8-tabs${className ? ` ${className}` : ''}`,
|
|
88
|
+
role: 'tablist',
|
|
89
|
+
'aria-label': label,
|
|
90
|
+
'aria-orientation': orientation,
|
|
91
|
+
'data-ol8-hierarchy': hierarchy,
|
|
92
|
+
'data-ol8-size': size,
|
|
93
|
+
'data-ol8-orientation': orientation,
|
|
94
|
+
...(material === 'gem' ? { 'data-ol8-material': 'gem' } : {}),
|
|
95
|
+
onKeyDown,
|
|
96
|
+
}, items.map(item => createElement(NavigationItem, {
|
|
97
|
+
key: item.id, kind: 'tab', size,
|
|
98
|
+
label: item.label, icon: item.icon, badge: item.badge,
|
|
99
|
+
ariaLabel: item.ariaLabel, disabled: item.disabled,
|
|
100
|
+
selected: item.id === selected,
|
|
101
|
+
id: `${prefix}-tab-${item.id}`,
|
|
102
|
+
'aria-controls': `${prefix}-panel-${item.id}`,
|
|
103
|
+
// One enabled tab stop; arrows move focus inside the list.
|
|
104
|
+
tabIndex: item.id === selected ? 0 : -1,
|
|
105
|
+
onClick: () => select(item.id),
|
|
106
|
+
})));
|
|
107
|
+
|
|
108
|
+
// "Every consuming composition must pair each item with exactly one owned panel."
|
|
109
|
+
if (!renderPanel) return list;
|
|
110
|
+
|
|
111
|
+
return createElement('div', { className: 'ol8-tabs__group' },
|
|
112
|
+
list,
|
|
113
|
+
items.map(item => createElement('div', {
|
|
114
|
+
key: item.id,
|
|
115
|
+
className: 'ol8-tabs__panel',
|
|
116
|
+
role: 'tabpanel',
|
|
117
|
+
id: `${prefix}-panel-${item.id}`,
|
|
118
|
+
'aria-labelledby': `${prefix}-tab-${item.id}`,
|
|
119
|
+
tabIndex: 0,
|
|
120
|
+
hidden: item.id !== selected,
|
|
121
|
+
}, renderPanel(item))),
|
|
122
|
+
);
|
|
123
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/* GENERATED from packages/core/src/molecules/button/button.css by packages/sync/src/build-styles.mjs — do not edit.
|
|
2
|
+
Edit the source in packages/core/src and re-run `npm run sync:styles`. */
|
|
3
|
+
/**
|
|
4
|
+
* Oneli8 · Button
|
|
5
|
+
* Figma: section "Button" (1034:9968) — Primary 15:823, Secondary 15:824,
|
|
6
|
+
* Quiet 15:825, Destructive 15:826. 4 families × 4 sizes × 6 states.
|
|
7
|
+
*
|
|
8
|
+
* The governing rule comes from the Figma component description itself:
|
|
9
|
+
*
|
|
10
|
+
* "State → deterministic visual evidence … Runtime-only native button
|
|
11
|
+
* semantics, disabled, loading, pressed, fullWidth and iconMotion remain
|
|
12
|
+
* implemented and tested in code; they must not become fake visual axes."
|
|
13
|
+
*
|
|
14
|
+
* So the six Figma States are NOT six markup variants:
|
|
15
|
+
* Default → the base rule
|
|
16
|
+
* Hover → :hover
|
|
17
|
+
* Pressed → :active (and [aria-pressed="true"] for toggles)
|
|
18
|
+
* Focus → :focus-visible
|
|
19
|
+
* Disabled → [disabled] (native, not a class)
|
|
20
|
+
* Loading → [data-ol8-loading] (which also implies disabled + aria-busy)
|
|
21
|
+
*
|
|
22
|
+
* There is no .ol8-btn--hover. Emitting one is a bug.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
.ol8-btn {
|
|
26
|
+
/* Per-variant channels. Families set these; the base rule never hardcodes. */
|
|
27
|
+
--_bg: transparent;
|
|
28
|
+
--_fg: currentColor;
|
|
29
|
+
--_bd: transparent;
|
|
30
|
+
--_bw: 0px;
|
|
31
|
+
|
|
32
|
+
/* Size channels, defaulted to Standard. */
|
|
33
|
+
--_h: var(--ol8-component-button-height-standard);
|
|
34
|
+
--_px: var(--ol8-component-button-paddinginline-standard);
|
|
35
|
+
--_r: var(--ol8-component-button-radius-standard);
|
|
36
|
+
--_gap: var(--ol8-component-button-gap-compact);
|
|
37
|
+
--_icon: var(--ol8-size-icon-small);
|
|
38
|
+
|
|
39
|
+
box-sizing: border-box; /* Figma strokes are inside the frame */
|
|
40
|
+
display: inline-flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
gap: var(--_gap);
|
|
44
|
+
block-size: var(--_h);
|
|
45
|
+
padding-inline: var(--_px);
|
|
46
|
+
padding-block: 0;
|
|
47
|
+
border: var(--_bw) solid var(--_bd);
|
|
48
|
+
border-radius: var(--_r);
|
|
49
|
+
background: var(--_bg);
|
|
50
|
+
color: var(--_fg);
|
|
51
|
+
|
|
52
|
+
font-family: var(--ol8-font-family-functional);
|
|
53
|
+
font-size: var(--ol8-font-size-016);
|
|
54
|
+
line-height: var(--ol8-font-line-024);
|
|
55
|
+
font-weight: var(--ol8-font-weight-regular);
|
|
56
|
+
letter-spacing: 0;
|
|
57
|
+
/* AR One Sans variable axis. Figma sets this on every functional text node;
|
|
58
|
+
omitting it shifts glyph advances and the button measures narrow. */
|
|
59
|
+
font-variation-settings: "ARRR" var(--ol8-font-axis-arrr-baseline);
|
|
60
|
+
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
user-select: none;
|
|
63
|
+
white-space: nowrap;
|
|
64
|
+
text-decoration: none; /* when rendered as <a class="ol8-btn"> */
|
|
65
|
+
-webkit-appearance: none;
|
|
66
|
+
appearance: none;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.ol8-btn__label { white-space: nowrap; }
|
|
70
|
+
|
|
71
|
+
/* Button size governs icon size — the Figma Icon Frame is driven by the
|
|
72
|
+
button, never set on the glyph. Higher specificity than .ol8-icon's own
|
|
73
|
+
[data-ol8-icon-size] rule, deliberately. */
|
|
74
|
+
.ol8-btn .ol8-icon { --_ol8-icon-size: var(--_icon); }
|
|
75
|
+
|
|
76
|
+
/* ---- Size (Figma: Size variant axis) --------------------------------- */
|
|
77
|
+
.ol8-btn[data-ol8-size="compact"] {
|
|
78
|
+
--_h: var(--ol8-component-button-height-compact);
|
|
79
|
+
--_px: var(--ol8-component-button-paddinginline-compact);
|
|
80
|
+
--_r: var(--ol8-component-button-radius-compact);
|
|
81
|
+
--_gap: var(--ol8-component-button-gap-compact);
|
|
82
|
+
--_icon: var(--ol8-size-icon-small);
|
|
83
|
+
}
|
|
84
|
+
.ol8-btn[data-ol8-size="standard"] {
|
|
85
|
+
--_h: var(--ol8-component-button-height-standard);
|
|
86
|
+
--_px: var(--ol8-component-button-paddinginline-standard);
|
|
87
|
+
--_r: var(--ol8-component-button-radius-standard);
|
|
88
|
+
--_gap: var(--ol8-component-button-gap-compact); /* shared, by design */
|
|
89
|
+
--_icon: var(--ol8-size-icon-small);
|
|
90
|
+
}
|
|
91
|
+
.ol8-btn[data-ol8-size="comfortable"] {
|
|
92
|
+
--_h: var(--ol8-component-button-height-comfortable);
|
|
93
|
+
--_px: var(--ol8-component-button-paddinginline-comfortable);
|
|
94
|
+
--_r: var(--ol8-component-button-radius-comfortable);
|
|
95
|
+
--_gap: var(--ol8-component-button-gap-comfortable);
|
|
96
|
+
--_icon: var(--ol8-size-icon-standard);
|
|
97
|
+
}
|
|
98
|
+
.ol8-btn[data-ol8-size="large"] {
|
|
99
|
+
--_h: var(--ol8-component-button-height-large);
|
|
100
|
+
--_px: var(--ol8-component-button-paddinginline-large);
|
|
101
|
+
--_r: var(--ol8-component-button-radius-large);
|
|
102
|
+
--_gap: var(--ol8-component-button-gap-comfortable); /* shared, by design */
|
|
103
|
+
--_icon: var(--ol8-size-icon-standard);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* ---- Family: Primary ------------------------------------------------- */
|
|
107
|
+
.ol8-btn--primary { --_bg: var(--ol8-color-actionprimary-default); --_fg: var(--ol8-color-actionprimary-content); }
|
|
108
|
+
.ol8-btn--primary:hover:not(:disabled):not([data-ol8-loading]) { --_bg: var(--ol8-color-actionprimary-hover); }
|
|
109
|
+
.ol8-btn--primary:active:not(:disabled):not([data-ol8-loading]),
|
|
110
|
+
.ol8-btn--primary[aria-pressed="true"]:not(:disabled) { --_bg: var(--ol8-color-actionprimary-pressed); }
|
|
111
|
+
.ol8-btn--primary:disabled, .ol8-btn--primary[data-ol8-loading] {
|
|
112
|
+
--_bg: var(--ol8-color-actionprimary-disabled);
|
|
113
|
+
--_fg: var(--ol8-color-actionprimary-disabledcontent);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* ---- Family: Destructive (same shape as Primary) --------------------- */
|
|
117
|
+
.ol8-btn--destructive { --_bg: var(--ol8-color-actiondestructive-default); --_fg: var(--ol8-color-actiondestructive-content); }
|
|
118
|
+
.ol8-btn--destructive:hover:not(:disabled):not([data-ol8-loading]) { --_bg: var(--ol8-color-actiondestructive-hover); }
|
|
119
|
+
.ol8-btn--destructive:active:not(:disabled):not([data-ol8-loading]),
|
|
120
|
+
.ol8-btn--destructive[aria-pressed="true"]:not(:disabled) { --_bg: var(--ol8-color-actiondestructive-pressed); }
|
|
121
|
+
.ol8-btn--destructive:disabled, .ol8-btn--destructive[data-ol8-loading] {
|
|
122
|
+
--_bg: var(--ol8-color-actiondestructive-disabled);
|
|
123
|
+
--_fg: var(--ol8-color-actiondestructive-disabledcontent);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* ---- Family: Secondary ----------------------------------------------
|
|
127
|
+
Secondary and Quiet carry byte-identical color roles in Figma. The ONLY
|
|
128
|
+
differentiator is this boundary: Secondary has a 2px stroke whose color
|
|
129
|
+
tracks the content color; Quiet has none. Verified — Quiet's node binds no
|
|
130
|
+
--ol8-component-button-secondaryboundary at all. */
|
|
131
|
+
.ol8-btn--secondary {
|
|
132
|
+
--_bw: var(--ol8-component-button-secondaryboundary);
|
|
133
|
+
--_bg: var(--ol8-color-actionsecondary-surfacedefault);
|
|
134
|
+
--_fg: var(--ol8-color-actionsecondary-contentdefault);
|
|
135
|
+
--_bd: var(--ol8-color-actionsecondary-contentdefault);
|
|
136
|
+
}
|
|
137
|
+
.ol8-btn--secondary:hover:not(:disabled):not([data-ol8-loading]) {
|
|
138
|
+
--_bg: var(--ol8-color-actionsecondary-surfacehover);
|
|
139
|
+
--_fg: var(--ol8-color-actionsecondary-contenthover);
|
|
140
|
+
--_bd: var(--ol8-color-actionsecondary-contenthover);
|
|
141
|
+
}
|
|
142
|
+
.ol8-btn--secondary:active:not(:disabled):not([data-ol8-loading]),
|
|
143
|
+
.ol8-btn--secondary[aria-pressed="true"]:not(:disabled) {
|
|
144
|
+
--_bg: var(--ol8-color-actionsecondary-surfacepressed);
|
|
145
|
+
--_fg: var(--ol8-color-actionsecondary-contentpressed);
|
|
146
|
+
--_bd: var(--ol8-color-actionsecondary-contentpressed);
|
|
147
|
+
}
|
|
148
|
+
.ol8-btn--secondary:disabled, .ol8-btn--secondary[data-ol8-loading] {
|
|
149
|
+
--_bg: var(--ol8-color-actionsecondary-surfacedisabled);
|
|
150
|
+
--_fg: var(--ol8-color-actionsecondary-contentdisabled);
|
|
151
|
+
--_bd: var(--ol8-color-actionsecondary-contentdisabled);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* ---- Family: Quiet (identical colors, no boundary) ------------------- */
|
|
155
|
+
.ol8-btn--quiet {
|
|
156
|
+
--_bg: var(--ol8-color-actionquiet-surfacedefault);
|
|
157
|
+
--_fg: var(--ol8-color-actionquiet-contentdefault);
|
|
158
|
+
}
|
|
159
|
+
.ol8-btn--quiet:hover:not(:disabled):not([data-ol8-loading]) {
|
|
160
|
+
--_bg: var(--ol8-color-actionquiet-surfacehover);
|
|
161
|
+
--_fg: var(--ol8-color-actionquiet-contenthover);
|
|
162
|
+
}
|
|
163
|
+
.ol8-btn--quiet:active:not(:disabled):not([data-ol8-loading]),
|
|
164
|
+
.ol8-btn--quiet[aria-pressed="true"]:not(:disabled) {
|
|
165
|
+
--_bg: var(--ol8-color-actionquiet-surfacepressed);
|
|
166
|
+
--_fg: var(--ol8-color-actionquiet-contentpressed);
|
|
167
|
+
}
|
|
168
|
+
.ol8-btn--quiet:disabled, .ol8-btn--quiet[data-ol8-loading] {
|
|
169
|
+
--_bg: var(--ol8-color-actionquiet-surfacedisabled);
|
|
170
|
+
--_fg: var(--ol8-color-actionquiet-contentdisabled);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* ---- Focus ----------------------------------------------------------
|
|
174
|
+
Figma draws the focus ring as an INSIDE stroke, so the focused variant is
|
|
175
|
+
exactly the same size as the unfocused one (83×42 either way, Standard).
|
|
176
|
+
An inset box-shadow reproduces that without disturbing layout or fighting
|
|
177
|
+
Secondary's own 2px boundary — `outline` would sit outside and grow it. */
|
|
178
|
+
.ol8-btn:focus-visible {
|
|
179
|
+
outline: none;
|
|
180
|
+
box-shadow: inset 0 0 0 var(--ol8-focus-ring-innerwidth) var(--ol8-color-focus-outer);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* ---- Disabled / Loading --------------------------------------------- */
|
|
184
|
+
.ol8-btn:disabled, .ol8-btn[data-ol8-loading] { cursor: not-allowed; }
|
|
185
|
+
.ol8-btn[data-ol8-loading] { cursor: progress; }
|
|
186
|
+
|
|
187
|
+
/* "Platform motion adapters may rotate it; Reduced Motion keeps the static
|
|
188
|
+
busy mark." — Figma 379:7. Rotation is opt-in and motion-guarded. */
|
|
189
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
190
|
+
.ol8-btn[data-ol8-loading] .ol8-btn__spinner > svg { animation: ol8-spin 900ms linear infinite; }
|
|
191
|
+
}
|
|
192
|
+
@keyframes ol8-spin { to { transform: rotate(360deg); } }
|
|
193
|
+
|
|
194
|
+
/* ---- fullWidth (runtime-only, per the Figma description) ------------- */
|
|
195
|
+
.ol8-btn[data-ol8-fullwidth] { display: flex; inline-size: 100%; }
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/* GENERATED from packages/core/src/molecules/choice-chip/choice-chip.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 Chip (MOLECULE) — the Soft Hexagon
|
|
5
|
+
* Figma: Select and Combobox / Choice Chip / Soft Hexagon 636:847 (8 variants)
|
|
6
|
+
*
|
|
7
|
+
* GEOMETRY, read from all four sizes in Figma:
|
|
8
|
+
* size target visible rail inset gap label
|
|
9
|
+
* compact 48 30 9 9 12 / 18
|
|
10
|
+
* standard 48 36 12 9 12 / 18
|
|
11
|
+
* comfortable 48 42 15 9 16 / 24
|
|
12
|
+
* large 60 42 15 9 16 / 24
|
|
13
|
+
* The terminal is a fixed 12 at every size and never scales with the label,
|
|
14
|
+
* so peer chips of different lengths keep one shoulder depth and side angle.
|
|
15
|
+
*
|
|
16
|
+
* The visible silhouette sits inside a taller protected target, which is why
|
|
17
|
+
* `min-block-size` is the target and the rail carries the visible height.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
.ol8-chip {
|
|
21
|
+
--_height: var(--ol8-component-choice-chip-height-standard);
|
|
22
|
+
--_target: var(--ol8-component-choice-chip-target-standard);
|
|
23
|
+
--_inset: var(--ol8-component-choice-chip-rail-inset-standard);
|
|
24
|
+
--_label-size: var(--ol8-font-size-012);
|
|
25
|
+
--_label-line: var(--ol8-font-line-018);
|
|
26
|
+
|
|
27
|
+
/* Inactive is the quiet Neutral treatment; Selected is Primary Soft. */
|
|
28
|
+
--_surface: var(--ol8-color-surface-sunken);
|
|
29
|
+
--_edge: var(--ol8-color-border-subtle);
|
|
30
|
+
--_ink: var(--ol8-color-text-primary);
|
|
31
|
+
|
|
32
|
+
/* The ring is painted at every state and is simply invisible until focus,
|
|
33
|
+
so focus never changes the geometry, only the colour. */
|
|
34
|
+
--_focus-outer: transparent;
|
|
35
|
+
--_focus-inner: transparent;
|
|
36
|
+
|
|
37
|
+
box-sizing: border-box;
|
|
38
|
+
position: relative;
|
|
39
|
+
display: inline-flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
min-block-size: var(--_target);
|
|
42
|
+
font-family: var(--ol8-font-family-functional);
|
|
43
|
+
font-variation-settings: "ARRR" var(--ol8-font-axis-arrr-baseline);
|
|
44
|
+
color: var(--_ink);
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
}
|
|
47
|
+
.ol8-chip[data-ol8-availability="disabled"] { cursor: not-allowed; }
|
|
48
|
+
|
|
49
|
+
/* ---- Size ------------------------------------------------------------ */
|
|
50
|
+
.ol8-chip[data-ol8-size="compact"] {
|
|
51
|
+
--_height: var(--ol8-component-choice-chip-height-compact);
|
|
52
|
+
--_inset: var(--ol8-component-choice-chip-rail-inset-compact);
|
|
53
|
+
}
|
|
54
|
+
.ol8-chip[data-ol8-size="standard"] {
|
|
55
|
+
--_height: var(--ol8-component-choice-chip-height-standard);
|
|
56
|
+
--_inset: var(--ol8-component-choice-chip-rail-inset-standard);
|
|
57
|
+
}
|
|
58
|
+
.ol8-chip[data-ol8-size="comfortable"] {
|
|
59
|
+
--_height: var(--ol8-component-choice-chip-height-comfortable);
|
|
60
|
+
--_inset: var(--ol8-component-choice-chip-rail-inset-comfortable);
|
|
61
|
+
--_label-size: var(--ol8-font-size-016);
|
|
62
|
+
--_label-line: var(--ol8-font-line-024);
|
|
63
|
+
}
|
|
64
|
+
.ol8-chip[data-ol8-size="large"] {
|
|
65
|
+
--_height: var(--ol8-component-choice-chip-height-large);
|
|
66
|
+
--_target: var(--ol8-component-choice-chip-target-large);
|
|
67
|
+
--_inset: var(--ol8-component-choice-chip-rail-inset-large);
|
|
68
|
+
--_label-size: var(--ol8-font-size-016);
|
|
69
|
+
--_label-line: var(--ol8-font-line-024);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* ---- Selection ------------------------------------------------------- */
|
|
73
|
+
/* Never colour alone: the native checked state carries the same fact, and the
|
|
74
|
+
selected edge is a stronger contrast than the inactive one. */
|
|
75
|
+
.ol8-chip[data-ol8-selection="selected"] {
|
|
76
|
+
--_surface: var(--ol8-color-selection-surface);
|
|
77
|
+
--_edge: var(--ol8-color-primary-360);
|
|
78
|
+
--_ink: var(--ol8-color-selection-content);
|
|
79
|
+
}
|
|
80
|
+
.ol8-chip[data-ol8-availability="disabled"] {
|
|
81
|
+
--_surface: var(--ol8-color-surface-disabled);
|
|
82
|
+
--_edge: var(--ol8-color-border-disabled);
|
|
83
|
+
--_ink: var(--ol8-color-text-disabled);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.ol8-chip__input {
|
|
87
|
+
position: absolute;
|
|
88
|
+
inline-size: 1px;
|
|
89
|
+
block-size: 1px;
|
|
90
|
+
opacity: 0;
|
|
91
|
+
pointer-events: none;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* ---- Terminals ------------------------------------------------------- */
|
|
95
|
+
/* One viewBox serves every height: Figma's terminal paths share identical X
|
|
96
|
+
values at 30, 36 and 42, and Y values that are the same fractions of the
|
|
97
|
+
height, so stretching Y alone is exact rather than approximate. */
|
|
98
|
+
.ol8-chip__terminal {
|
|
99
|
+
flex: none;
|
|
100
|
+
inline-size: var(--ol8-component-choice-chip-terminal);
|
|
101
|
+
block-size: var(--_height);
|
|
102
|
+
/* The point reaches 0.15 beyond the box, as it does in Figma, and the focus
|
|
103
|
+
ring is painted outside the silhouette. Neither may be clipped. */
|
|
104
|
+
overflow: visible;
|
|
105
|
+
}
|
|
106
|
+
/* The trailing terminal is the leading one mirrored, so they cannot drift. */
|
|
107
|
+
.ol8-chip__terminal--trailing { transform: scaleX(-1); }
|
|
108
|
+
|
|
109
|
+
.ol8-chip__fill { fill: var(--_surface); }
|
|
110
|
+
|
|
111
|
+
/* Double width, clipped to the fill: the outer half is cut away and what
|
|
112
|
+
remains is an inside edge of exactly one border width, matching the rail's
|
|
113
|
+
own inside border. A centred stroke would stand 1px proud of the rail and
|
|
114
|
+
show a step at the shoulder. */
|
|
115
|
+
.ol8-chip__edge {
|
|
116
|
+
fill: none;
|
|
117
|
+
stroke: var(--_edge);
|
|
118
|
+
stroke-width: calc(var(--ol8-border-width-standard) * 2);
|
|
119
|
+
vector-effect: non-scaling-stroke;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Both rings are painted before the fill and at double width, so the inner
|
|
123
|
+
half of each lands under the fill and only the outer half shows. This is how
|
|
124
|
+
the ring follows the hexagon instead of boxing it. */
|
|
125
|
+
.ol8-chip__focus-outer,
|
|
126
|
+
.ol8-chip__focus-inner { fill: none; vector-effect: non-scaling-stroke; }
|
|
127
|
+
.ol8-chip__focus-outer {
|
|
128
|
+
stroke: var(--_focus-outer);
|
|
129
|
+
stroke-width: calc((var(--ol8-focus-ring-outerwidth) + var(--ol8-focus-ring-innerwidth)) * 2);
|
|
130
|
+
}
|
|
131
|
+
.ol8-chip__focus-inner {
|
|
132
|
+
stroke: var(--_focus-inner);
|
|
133
|
+
stroke-width: calc(var(--ol8-focus-ring-innerwidth) * 2);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.ol8-chip:has(> .ol8-chip__input:focus-visible) {
|
|
137
|
+
--_focus-outer: var(--ol8-color-focus-outer);
|
|
138
|
+
--_focus-inner: var(--ol8-color-focus-inner);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* ---- Centre rail ----------------------------------------------------- */
|
|
142
|
+
/* Only the rail grows with the label. It owns the top and bottom of the
|
|
143
|
+
perimeter; the terminals own the angled ends. */
|
|
144
|
+
.ol8-chip__rail {
|
|
145
|
+
box-sizing: border-box;
|
|
146
|
+
position: relative;
|
|
147
|
+
display: flex;
|
|
148
|
+
align-items: center;
|
|
149
|
+
gap: var(--ol8-component-choice-gap-standard);
|
|
150
|
+
block-size: var(--_height);
|
|
151
|
+
padding-inline: var(--_inset);
|
|
152
|
+
background: var(--_surface);
|
|
153
|
+
border-block: var(--ol8-border-width-standard) solid var(--_edge);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* The ring's flat runs, continuing the terminals' strokes across the rail.
|
|
157
|
+
Two colours in one band, ordered outward from the silhouette. */
|
|
158
|
+
.ol8-chip__rail::before,
|
|
159
|
+
.ol8-chip__rail::after {
|
|
160
|
+
content: "";
|
|
161
|
+
position: absolute;
|
|
162
|
+
inset-inline: 0;
|
|
163
|
+
block-size: calc(var(--ol8-focus-ring-outerwidth) + var(--ol8-focus-ring-innerwidth));
|
|
164
|
+
}
|
|
165
|
+
.ol8-chip__rail::before {
|
|
166
|
+
inset-block-end: 100%;
|
|
167
|
+
background: linear-gradient(
|
|
168
|
+
var(--_focus-outer) 0 var(--ol8-focus-ring-outerwidth),
|
|
169
|
+
var(--_focus-inner) var(--ol8-focus-ring-outerwidth) 100%);
|
|
170
|
+
}
|
|
171
|
+
.ol8-chip__rail::after {
|
|
172
|
+
inset-block-start: 100%;
|
|
173
|
+
background: linear-gradient(
|
|
174
|
+
var(--_focus-inner) 0 var(--ol8-focus-ring-innerwidth),
|
|
175
|
+
var(--_focus-outer) var(--ol8-focus-ring-innerwidth) 100%);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.ol8-chip__label {
|
|
179
|
+
font-size: var(--_label-size);
|
|
180
|
+
line-height: var(--_label-line);
|
|
181
|
+
font-weight: var(--ol8-font-weight-regular);
|
|
182
|
+
color: var(--_ink);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* ---- Mark ------------------------------------------------------------ */
|
|
186
|
+
/* Figma places the mark BEFORE the label in every variant, Remove included. */
|
|
187
|
+
.ol8-chip__mark {
|
|
188
|
+
flex: none;
|
|
189
|
+
display: inline-flex;
|
|
190
|
+
align-items: center;
|
|
191
|
+
justify-content: center;
|
|
192
|
+
inline-size: var(--ol8-component-choicechip-mark);
|
|
193
|
+
block-size: var(--ol8-component-choicechip-mark);
|
|
194
|
+
color: var(--_ink);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* The Check does not scale with its slot. Figma's `Icon / Selection / Check`
|
|
198
|
+
is a governed small master, not an Icon Frame instance: Size=Small and
|
|
199
|
+
Size=Standard both carry the same 10.2 x 6.9 artwork at the same 1.8 stroke,
|
|
200
|
+
centred in an 18 or 24 box. So the source box stays 24 units and overflows
|
|
201
|
+
the smaller slot, exactly as Figma draws it. */
|
|
202
|
+
.ol8-chip__mark > svg {
|
|
203
|
+
display: block;
|
|
204
|
+
flex: none;
|
|
205
|
+
inline-size: var(--ol8-size-icon-standard);
|
|
206
|
+
block-size: var(--ol8-size-icon-standard);
|
|
207
|
+
overflow: visible;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Both marks read at the one governed optical stroke, which is what Figma
|
|
211
|
+
draws and what the iconography contract calls contextual stroke
|
|
212
|
+
normalization: peer marks in one family match apparent weight. */
|
|
213
|
+
.ol8-chip__mark > svg > * {
|
|
214
|
+
vector-effect: non-scaling-stroke;
|
|
215
|
+
stroke-width: var(--ol8-component-choice-optical-stroke);
|
|
216
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/* GENERATED from packages/core/src/organisms/choice-group/choice-group.css by packages/sync/src/build-styles.mjs — do not edit.
|
|
2
|
+
Edit the source in packages/core/src and re-run `npm run sync:styles`. */
|
|
3
|
+
/**
|
|
4
|
+
* Oneli8 · Choice Group (ORGANISM)
|
|
5
|
+
* Figma: Documentation / Organism / Choice Group
|
|
6
|
+
* Checkbox Hierarchy 273:603 Aggregate = None | Some | All
|
|
7
|
+
* Radio Group 859:5180 Behavior = Safe Default | No Default |
|
|
8
|
+
* Horizontal | Invalid
|
|
9
|
+
*
|
|
10
|
+
* First organism in the system, and the first tier allowed to hold logic:
|
|
11
|
+
* the checkbox group computes its parent from its children, and the radio
|
|
12
|
+
* group owns group-level validation.
|
|
13
|
+
*
|
|
14
|
+
* From Figma (273:603): "Aggregate None/Some/All is computed from nested
|
|
15
|
+
* Checkbox molecules; MIXED IS NEVER A THIRD PREFERENCE. Runtime recursion,
|
|
16
|
+
* parent-child synchronization, group naming, required behavior, reset, and
|
|
17
|
+
* announcements live in code."
|
|
18
|
+
*
|
|
19
|
+
* So Aggregate is an OUTPUT, never an input. Nothing may author it.
|
|
20
|
+
*
|
|
21
|
+
* The two shells differ, deliberately:
|
|
22
|
+
* checkbox bordered, radius-container (24px), legend Title/Small
|
|
23
|
+
* radio unbordered, radius-standard (12px), legend Body/Medium
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
.ol8-choice-group {
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
gap: var(--ol8-spacing-stack-related);
|
|
31
|
+
padding: var(--ol8-spacing-inset-standard);
|
|
32
|
+
background: var(--ol8-color-surface-raised);
|
|
33
|
+
border: 0;
|
|
34
|
+
border-radius: var(--ol8-shape-radius-standard);
|
|
35
|
+
font-family: var(--ol8-font-family-functional);
|
|
36
|
+
font-variation-settings: "ARRR" var(--ol8-font-axis-arrr-baseline);
|
|
37
|
+
min-inline-size: 0; /* fieldset defaults to min-content; this lets it shrink */
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.ol8-choice-group--checkbox {
|
|
41
|
+
border: 1px solid var(--ol8-color-border-subtle);
|
|
42
|
+
border-radius: var(--ol8-shape-radius-container);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* <legend> is taken out of flow by default, so it is laid out as a normal
|
|
46
|
+
flex child instead — the group label is a real legend for assistive tech. */
|
|
47
|
+
.ol8-choice-group__legend {
|
|
48
|
+
display: block;
|
|
49
|
+
float: none;
|
|
50
|
+
padding: 0;
|
|
51
|
+
inline-size: 100%;
|
|
52
|
+
color: var(--ol8-color-text-primary);
|
|
53
|
+
}
|
|
54
|
+
.ol8-choice-group--checkbox > .ol8-choice-group__legend {
|
|
55
|
+
font-size: var(--ol8-font-size-021);
|
|
56
|
+
line-height: var(--ol8-font-line-030);
|
|
57
|
+
font-weight: var(--ol8-font-weight-semibold);
|
|
58
|
+
}
|
|
59
|
+
.ol8-choice-group--radio > .ol8-choice-group__legend {
|
|
60
|
+
font-size: var(--ol8-font-size-016);
|
|
61
|
+
line-height: var(--ol8-font-line-024);
|
|
62
|
+
font-weight: var(--ol8-font-weight-regular);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.ol8-choice-group__instruction {
|
|
66
|
+
font-size: var(--ol8-font-size-012);
|
|
67
|
+
line-height: var(--ol8-font-line-018);
|
|
68
|
+
letter-spacing: 0.12px;
|
|
69
|
+
color: var(--ol8-color-text-secondary);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.ol8-choice-group__options { display: flex; flex-direction: column; }
|
|
73
|
+
.ol8-choice-group[data-ol8-orientation="horizontal"] > .ol8-choice-group__options {
|
|
74
|
+
flex-direction: row;
|
|
75
|
+
gap: var(--ol8-spacing-stack-related);
|
|
76
|
+
flex-wrap: wrap;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Children of a hierarchy sit under their parent, indented one 24px step. */
|
|
80
|
+
.ol8-choice-group__children {
|
|
81
|
+
display: flex;
|
|
82
|
+
flex-direction: column;
|
|
83
|
+
padding-inline-start: var(--ol8-dimension-scale-024);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Group-level validation message. */
|
|
87
|
+
.ol8-choice-group__message {
|
|
88
|
+
font-size: var(--ol8-font-size-012);
|
|
89
|
+
line-height: var(--ol8-font-line-018);
|
|
90
|
+
letter-spacing: 0.12px;
|
|
91
|
+
color: var(--ol8-color-text-secondary);
|
|
92
|
+
}
|
|
93
|
+
.ol8-choice-group[data-ol8-invalid="true"] > .ol8-choice-group__message {
|
|
94
|
+
color: var(--ol8-color-feedbackcritical-inlinecontent);
|
|
95
|
+
}
|