@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.
- package/AGENTS.md +16 -0
- package/LICENSE +202 -0
- package/NOTICE +8 -0
- package/PRINCIPLES.md +48 -0
- package/README.md +130 -0
- package/ai-context.json +348 -0
- package/package.json +100 -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/icon.css +54 -0
- package/src/atoms/icon/icon.d.ts +19 -0
- package/src/atoms/icon/icon.js +81 -0
- package/src/atoms/icon/icons.generated.d.ts +48 -0
- package/src/atoms/icon/icons.generated.js +562 -0
- package/src/atoms/icon/index.d.ts +2 -0
- package/src/atoms/icon/index.js +2 -0
- package/src/atoms/index.d.ts +3 -0
- package/src/atoms/index.js +3 -0
- package/src/atoms/navigation-badge/index.d.ts +1 -0
- package/src/atoms/navigation-badge/index.js +1 -0
- package/src/atoms/navigation-badge/navigation-badge.css +35 -0
- package/src/atoms/navigation-badge/navigation-badge.d.ts +5 -0
- package/src/atoms/navigation-badge/navigation-badge.js +21 -0
- package/src/atoms/selection-indicator/index.d.ts +1 -0
- package/src/atoms/selection-indicator/index.js +1 -0
- package/src/atoms/selection-indicator/selection-indicator.css +160 -0
- package/src/atoms/selection-indicator/selection-indicator.d.ts +36 -0
- package/src/atoms/selection-indicator/selection-indicator.js +61 -0
- package/src/index.d.ts +3 -0
- package/src/index.js +3 -0
- package/src/materials/gem/gem.css +374 -0
- package/src/molecules/button/button.css +193 -0
- package/src/molecules/button/button.d.ts +51 -0
- package/src/molecules/button/button.js +134 -0
- package/src/molecules/button/index.d.ts +1 -0
- package/src/molecules/button/index.js +1 -0
- package/src/molecules/choice-chip/choice-chip.css +214 -0
- package/src/molecules/choice-chip/choice-chip.d.ts +27 -0
- package/src/molecules/choice-chip/choice-chip.js +170 -0
- package/src/molecules/choice-chip/index.d.ts +2 -0
- package/src/molecules/choice-chip/index.js +10 -0
- package/src/molecules/choice-item/choice-item.css +265 -0
- package/src/molecules/choice-item/choice-item.d.ts +54 -0
- package/src/molecules/choice-item/choice-item.js +153 -0
- package/src/molecules/choice-item/index.d.ts +1 -0
- package/src/molecules/choice-item/index.js +1 -0
- package/src/molecules/form-message/form-message.css +46 -0
- package/src/molecules/form-message/form-message.d.ts +23 -0
- package/src/molecules/form-message/form-message.js +73 -0
- package/src/molecules/form-message/index.d.ts +1 -0
- package/src/molecules/form-message/index.js +1 -0
- package/src/molecules/icon-button/icon-button.css +222 -0
- package/src/molecules/icon-button/icon-button.d.ts +40 -0
- package/src/molecules/icon-button/icon-button.js +121 -0
- package/src/molecules/icon-button/index.d.ts +1 -0
- package/src/molecules/icon-button/index.js +1 -0
- package/src/molecules/index.d.ts +10 -0
- package/src/molecules/index.js +10 -0
- package/src/molecules/link/index.d.ts +1 -0
- package/src/molecules/link/index.js +1 -0
- package/src/molecules/link/link.css +114 -0
- package/src/molecules/link/link.d.ts +27 -0
- package/src/molecules/link/link.js +76 -0
- package/src/molecules/navigation-item/index.js +1 -0
- package/src/molecules/navigation-item/navigation-item.css +153 -0
- package/src/molecules/navigation-item/navigation-item.js +95 -0
- package/src/molecules/select/index.js +1 -0
- package/src/molecules/select/select.css +31 -0
- package/src/molecules/select/select.js +127 -0
- package/src/molecules/selection-option/index.d.ts +1 -0
- package/src/molecules/selection-option/index.js +1 -0
- package/src/molecules/selection-option/selection-option.css +121 -0
- package/src/molecules/selection-option/selection-option.d.ts +42 -0
- package/src/molecules/selection-option/selection-option.js +97 -0
- package/src/molecules/text-field/index.d.ts +1 -0
- package/src/molecules/text-field/index.js +5 -0
- package/src/molecules/text-field/text-field.css +266 -0
- package/src/molecules/text-field/text-field.d.ts +69 -0
- package/src/molecules/text-field/text-field.js +234 -0
- package/src/molecules/token/index.d.ts +2 -0
- package/src/molecules/token/index.js +1 -0
- package/src/molecules/token/token.css +63 -0
- package/src/molecules/token/token.d.ts +35 -0
- package/src/molecules/token/token.js +83 -0
- package/src/organisms/choice-group/choice-group.css +93 -0
- package/src/organisms/choice-group/choice-group.d.ts +80 -0
- package/src/organisms/choice-group/choice-group.js +159 -0
- package/src/organisms/choice-group/index.d.ts +1 -0
- package/src/organisms/choice-group/index.js +1 -0
- package/src/organisms/choice-picker/choice-picker.css +98 -0
- package/src/organisms/choice-picker/choice-picker.d.ts +62 -0
- package/src/organisms/choice-picker/choice-picker.js +201 -0
- package/src/organisms/choice-picker/index.d.ts +1 -0
- package/src/organisms/choice-picker/index.js +1 -0
- package/src/organisms/combobox/combobox.css +40 -0
- package/src/organisms/combobox/combobox.js +309 -0
- package/src/organisms/combobox/index.js +5 -0
- package/src/organisms/index.d.ts +8 -0
- package/src/organisms/index.js +8 -0
- package/src/organisms/multi-select-field/index.d.ts +1 -0
- package/src/organisms/multi-select-field/index.js +1 -0
- package/src/organisms/multi-select-field/multi-select-field.css +76 -0
- package/src/organisms/multi-select-field/multi-select-field.d.ts +64 -0
- package/src/organisms/multi-select-field/multi-select-field.js +317 -0
- package/src/organisms/navigation-keys.js +58 -0
- package/src/organisms/segmented-control/index.d.ts +1 -0
- package/src/organisms/segmented-control/index.js +1 -0
- package/src/organisms/segmented-control/segmented-control.css +113 -0
- package/src/organisms/segmented-control/segmented-control.d.ts +43 -0
- package/src/organisms/segmented-control/segmented-control.js +163 -0
- package/src/organisms/selection-popup/index.js +1 -0
- package/src/organisms/selection-popup/selection-popup.css +90 -0
- package/src/organisms/selection-popup/selection-popup.js +100 -0
- package/src/organisms/tab-bar/index.d.ts +1 -0
- package/src/organisms/tab-bar/index.js +1 -0
- package/src/organisms/tab-bar/tab-bar.css +45 -0
- package/src/organisms/tab-bar/tab-bar.d.ts +37 -0
- package/src/organisms/tab-bar/tab-bar.js +104 -0
- package/src/organisms/tabs/index.d.ts +1 -0
- package/src/organisms/tabs/index.js +1 -0
- package/src/organisms/tabs/tabs.css +53 -0
- package/src/organisms/tabs/tabs.d.ts +48 -0
- package/src/organisms/tabs/tabs.js +152 -0
- package/styles.css +29 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oneli8 · Selection Option (MOLECULE)
|
|
3
|
+
* Figma: Select and Combobox -> 01 · Atomic Selection Building Blocks
|
|
4
|
+
* Building Blocks / Selection Option 570:122 (8 variants)
|
|
5
|
+
* Size Standard|Large x Selection x Availability
|
|
6
|
+
*
|
|
7
|
+
* From the Figma description (570:122):
|
|
8
|
+
* "Description is associated through aria-describedby and REMAINS OUTSIDE
|
|
9
|
+
* the option accessible name. Selection Popup composes this owner;
|
|
10
|
+
* KEYBOARD FOCUS REMAINS ON THE COMBOBOX INPUT through
|
|
11
|
+
* aria-activedescendant."
|
|
12
|
+
*
|
|
13
|
+
* Two consequences that shape everything here:
|
|
14
|
+
* - the option is never focusable and carries no tabindex
|
|
15
|
+
* - "active" (the aria-activedescendant highlight) is a DIFFERENT thing from
|
|
16
|
+
* :hover and from selected, so it gets its own attribute
|
|
17
|
+
*
|
|
18
|
+
* Row geometry: 48px (Standard) / 60px (Large) minimum, but the content box
|
|
19
|
+
* stays 24px tall in both — the padding absorbs the difference.
|
|
20
|
+
* padding-block = (minimum - 24) / 2 -> 12px and 18px
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
.ol8-option {
|
|
24
|
+
--_min: var(--ol8-component-selectionoption-minimum-standard);
|
|
25
|
+
--_inset: var(--ol8-component-selectionoption-inset-standard);
|
|
26
|
+
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
min-block-size: var(--_min);
|
|
31
|
+
font-family: var(--ol8-font-family-functional);
|
|
32
|
+
font-variation-settings: "ARRR" var(--ol8-font-axis-arrr-baseline);
|
|
33
|
+
list-style: none;
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
}
|
|
36
|
+
.ol8-option[data-ol8-size="large"] {
|
|
37
|
+
--_min: var(--ol8-component-selectionoption-minimum-large);
|
|
38
|
+
--_inset: var(--ol8-component-selectionoption-inset-large);
|
|
39
|
+
}
|
|
40
|
+
.ol8-option[aria-disabled="true"] { cursor: not-allowed; }
|
|
41
|
+
|
|
42
|
+
/* The surface carries every wash and the focus ring; the row itself stays
|
|
43
|
+
clean so a popup can lay options out without fighting them. */
|
|
44
|
+
.ol8-option__surface {
|
|
45
|
+
box-sizing: border-box;
|
|
46
|
+
position: relative;
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: var(--ol8-component-choice-gap-standard);
|
|
50
|
+
flex: 1 0 0;
|
|
51
|
+
min-inline-size: 0;
|
|
52
|
+
block-size: 100%;
|
|
53
|
+
padding-inline: var(--_inset);
|
|
54
|
+
padding-block: calc((var(--_min) - var(--ol8-font-line-024)) / 2);
|
|
55
|
+
border-radius: var(--ol8-shape-radius-medium);
|
|
56
|
+
overflow: clip;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Selected, hover and active all paint the same wash. Selected is a state of
|
|
60
|
+
the data; active is the aria-activedescendant cursor; hover is the pointer.
|
|
61
|
+
They coincide visually by design. */
|
|
62
|
+
.ol8-option[aria-selected="true"] > .ol8-option__surface,
|
|
63
|
+
.ol8-option[data-ol8-active="true"] > .ol8-option__surface,
|
|
64
|
+
.ol8-option:hover:not([aria-disabled="true"]) > .ol8-option__surface {
|
|
65
|
+
background: var(--ol8-color-selection-surface);
|
|
66
|
+
}
|
|
67
|
+
.ol8-option:active:not([aria-disabled="true"]) > .ol8-option__surface {
|
|
68
|
+
background: var(--ol8-color-selection-surface);
|
|
69
|
+
opacity: 0.84;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Focus lives on the combobox input, never here — but the option still shows
|
|
73
|
+
the active ring when a popup marks it, so the two-tone ring is reproduced. */
|
|
74
|
+
.ol8-option[data-ol8-focus-ring="true"] > .ol8-option__surface {
|
|
75
|
+
border: var(--ol8-focus-ring-outerwidth) solid var(--ol8-color-focus-outer);
|
|
76
|
+
}
|
|
77
|
+
.ol8-option[data-ol8-focus-ring="true"] > .ol8-option__surface::after {
|
|
78
|
+
content: "";
|
|
79
|
+
position: absolute;
|
|
80
|
+
inset: 2px;
|
|
81
|
+
border: var(--ol8-focus-ring-innerwidth) solid var(--ol8-color-focus-inner);
|
|
82
|
+
border-radius: 7px;
|
|
83
|
+
pointer-events: none;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.ol8-option__text {
|
|
87
|
+
display: flex;
|
|
88
|
+
flex-direction: column;
|
|
89
|
+
flex: 1 0 0;
|
|
90
|
+
min-inline-size: 0;
|
|
91
|
+
}
|
|
92
|
+
.ol8-option__label {
|
|
93
|
+
font-size: var(--ol8-font-size-016);
|
|
94
|
+
line-height: var(--ol8-font-line-024);
|
|
95
|
+
font-weight: var(--ol8-font-weight-regular);
|
|
96
|
+
color: var(--ol8-color-text-primary);
|
|
97
|
+
overflow-wrap: break-word;
|
|
98
|
+
}
|
|
99
|
+
.ol8-option__description {
|
|
100
|
+
font-size: var(--ol8-font-size-012);
|
|
101
|
+
line-height: var(--ol8-font-line-018);
|
|
102
|
+
letter-spacing: 0.12px;
|
|
103
|
+
color: var(--ol8-color-text-secondary);
|
|
104
|
+
overflow-wrap: break-word;
|
|
105
|
+
}
|
|
106
|
+
.ol8-option[aria-disabled="true"] .ol8-option__label,
|
|
107
|
+
.ol8-option[aria-disabled="true"] .ol8-option__description {
|
|
108
|
+
color: var(--ol8-color-text-disabled);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* The trailing slot is ALWAYS reserved, selected or not, so the label column
|
|
112
|
+
never shifts when selection changes. Figma keeps an empty 18px node there. */
|
|
113
|
+
.ol8-option__check {
|
|
114
|
+
flex: none;
|
|
115
|
+
inline-size: var(--ol8-size-icon-small);
|
|
116
|
+
block-size: var(--ol8-size-icon-small);
|
|
117
|
+
color: var(--ol8-color-selection-content);
|
|
118
|
+
}
|
|
119
|
+
.ol8-option__check > svg { display: block; inline-size: 100%; block-size: 100%; }
|
|
120
|
+
|
|
121
|
+
.ol8-option .ol8-icon { --_ol8-icon-size: var(--ol8-size-icon-small); }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selection Option (MOLECULE) — headless. The option is NEVER focusable: a
|
|
3
|
+
* listbox moves a virtual cursor with aria-activedescendant while real focus
|
|
4
|
+
* stays on the controlling combobox input.
|
|
5
|
+
*/
|
|
6
|
+
import type { Ol8IconName } from '../../atoms/icon/icons.generated.js';
|
|
7
|
+
|
|
8
|
+
export type Ol8OptionSize = 'standard' | 'large';
|
|
9
|
+
export declare const OL8_OPTION_SIZES: readonly Ol8OptionSize[];
|
|
10
|
+
|
|
11
|
+
export interface Ol8SelectionOptionOptions {
|
|
12
|
+
value?: string;
|
|
13
|
+
size?: Ol8OptionSize;
|
|
14
|
+
selected?: boolean;
|
|
15
|
+
/** The listbox's virtual cursor, not DOM focus. */
|
|
16
|
+
active?: boolean;
|
|
17
|
+
/** Options cannot be natively disabled, so this emits aria-disabled. */
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
/** Associated with aria-describedby, kept out of the accessible name. */
|
|
20
|
+
description?: string;
|
|
21
|
+
leadingIcon?: Ol8IconName;
|
|
22
|
+
id?: string;
|
|
23
|
+
focusRing?: boolean;
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export declare function renderSelectionOption(
|
|
28
|
+
label: string,
|
|
29
|
+
options?: Ol8SelectionOptionOptions,
|
|
30
|
+
): string;
|
|
31
|
+
|
|
32
|
+
export declare function setOptionSelected(option: Element, selected: boolean): void;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Moves the virtual cursor within a listbox: marks one option active and points
|
|
36
|
+
* the controller's aria-activedescendant at it. Real focus is never moved.
|
|
37
|
+
*/
|
|
38
|
+
export declare function setActiveOption(
|
|
39
|
+
listbox: Element,
|
|
40
|
+
option: Element,
|
|
41
|
+
controller: Element,
|
|
42
|
+
): void;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oneli8 · Selection Option (MOLECULE) — headless.
|
|
3
|
+
*
|
|
4
|
+
* Figma (570:122): "Description is associated through aria-describedby and
|
|
5
|
+
* remains outside the option accessible name. Selection Popup composes this
|
|
6
|
+
* owner; keyboard focus remains on the Combobox input through
|
|
7
|
+
* aria-activedescendant."
|
|
8
|
+
*
|
|
9
|
+
* That single sentence sets the whole contract:
|
|
10
|
+
* - role="option", named by its LABEL only. The description is referenced,
|
|
11
|
+
* never nested into the name, so aria-labelledby points at the label and
|
|
12
|
+
* aria-describedby at the description.
|
|
13
|
+
* - the option is NOT focusable. No tabindex, ever. A listbox moves a
|
|
14
|
+
* virtual cursor with aria-activedescendant while real focus stays put.
|
|
15
|
+
* - "active" (the cursor) is distinct from "selected" (the data) and from
|
|
16
|
+
* :hover (the pointer), so it is its own attribute.
|
|
17
|
+
* - options cannot use the `disabled` attribute — it is not valid on the
|
|
18
|
+
* element an option renders as — so unavailability is aria-disabled.
|
|
19
|
+
*/
|
|
20
|
+
import { renderIcon, renderIconSvg, isOl8IconName } from '../../atoms/icon/icon.js';
|
|
21
|
+
|
|
22
|
+
export const OL8_OPTION_SIZES = ['standard', 'large'];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} label
|
|
26
|
+
* @param {{value?:string,size?:string,selected?:boolean,active?:boolean,disabled?:boolean,
|
|
27
|
+
* description?:string,leadingIcon?:string,id?:string,focusRing?:boolean,className?:string}} [options]
|
|
28
|
+
*/
|
|
29
|
+
export function renderSelectionOption(label, options = {}) {
|
|
30
|
+
const {
|
|
31
|
+
value, size = 'standard', selected = false, active = false, disabled = false,
|
|
32
|
+
description, leadingIcon, id, focusRing = false, className,
|
|
33
|
+
} = options;
|
|
34
|
+
|
|
35
|
+
if (!OL8_OPTION_SIZES.includes(size)) throw new Error(`[ol8] unknown option size "${size}"`);
|
|
36
|
+
if (!label || !String(label).trim()) throw new Error('[ol8] a selection option requires a label');
|
|
37
|
+
if (leadingIcon && !isOl8IconName(leadingIcon)) throw new Error(`[ol8] unknown icon "${leadingIcon}"`);
|
|
38
|
+
|
|
39
|
+
const uid = id ?? `ol8-option-${Math.random().toString(36).slice(2, 9)}`;
|
|
40
|
+
const labelId = `${uid}-label`;
|
|
41
|
+
const descId = description ? `${uid}-desc` : null;
|
|
42
|
+
|
|
43
|
+
const attrs = [
|
|
44
|
+
`class="ol8-option${className ? ` ${className}` : ''}"`,
|
|
45
|
+
'role="option"',
|
|
46
|
+
`id="${uid}"`,
|
|
47
|
+
value !== undefined ? `data-ol8-value="${escapeAttr(value)}"` : '',
|
|
48
|
+
`data-ol8-size="${size}"`,
|
|
49
|
+
`aria-selected="${selected ? 'true' : 'false'}"`,
|
|
50
|
+
active ? 'data-ol8-active="true"' : '',
|
|
51
|
+
disabled ? 'aria-disabled="true"' : '',
|
|
52
|
+
focusRing ? 'data-ol8-focus-ring="true"' : '',
|
|
53
|
+
// Named by the label alone; the description is referenced, not nested.
|
|
54
|
+
`aria-labelledby="${labelId}"`,
|
|
55
|
+
descId ? `aria-describedby="${descId}"` : '',
|
|
56
|
+
].filter(Boolean).join(' ');
|
|
57
|
+
|
|
58
|
+
const lead = leadingIcon ? renderIcon(leadingIcon, { size: 18 }) : '';
|
|
59
|
+
const check = `<span class="ol8-option__check" aria-hidden="true">${selected ? renderIconSvg('check') : ''}</span>`;
|
|
60
|
+
|
|
61
|
+
return `<div ${attrs}><span class="ol8-option__surface">` +
|
|
62
|
+
lead +
|
|
63
|
+
`<span class="ol8-option__text">` +
|
|
64
|
+
`<span class="ol8-option__label" id="${labelId}">${escapeText(label)}</span>` +
|
|
65
|
+
(description ? `<span class="ol8-option__description" id="${descId}">${escapeText(description)}</span>` : '') +
|
|
66
|
+
`</span>` +
|
|
67
|
+
check +
|
|
68
|
+
`</span></div>`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Flips one option's selected state and repaints its trailing check. */
|
|
72
|
+
export function setOptionSelected(option, selected) {
|
|
73
|
+
option.setAttribute('aria-selected', selected ? 'true' : 'false');
|
|
74
|
+
const check = option.querySelector('.ol8-option__check');
|
|
75
|
+
if (check) check.innerHTML = selected ? renderIconSvg('check') : '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Moves the virtual cursor within a listbox: marks one option active and
|
|
80
|
+
* points the controlling element's aria-activedescendant at it. Real focus is
|
|
81
|
+
* never moved, which is the entire point of the pattern.
|
|
82
|
+
*/
|
|
83
|
+
export function setActiveOption(listbox, option, controller) {
|
|
84
|
+
for (const other of listbox.querySelectorAll('.ol8-option[data-ol8-active="true"]')) {
|
|
85
|
+
if (other !== option) delete other.dataset.ol8Active;
|
|
86
|
+
}
|
|
87
|
+
if (!option) {
|
|
88
|
+
controller?.removeAttribute('aria-activedescendant');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
option.dataset.ol8Active = 'true';
|
|
92
|
+
controller?.setAttribute('aria-activedescendant', option.id);
|
|
93
|
+
option.scrollIntoView({ block: 'nearest' });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function escapeAttr(v) { return String(v).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<'); }
|
|
97
|
+
function escapeText(v) { return String(v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './text-field.js';
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oneli8 · Text Field (MOLECULE)
|
|
3
|
+
* Figma: Text Field / Outline 182:33, Text Field / Filled 186:53,
|
|
4
|
+
* Text Field / Outline / Gem 211:36, Text Field / Filled / Gem 215:226,
|
|
5
|
+
* Text Field / Content 175:2, Form Message 177:13.
|
|
6
|
+
*
|
|
7
|
+
* ANATOMY, read from Figma rather than assumed:
|
|
8
|
+
* root vertical, gap Component / Text Field / Gap / Supporting
|
|
9
|
+
* · Label Row horizontal, gap Component / Text Field / Gap / Label
|
|
10
|
+
* · Label twelve over eighteen, Semibold, Tracking Open, Text Primary
|
|
11
|
+
* · Requirement a star in Feedback Critical Content, Regular weight
|
|
12
|
+
* · Control Stack fixed at Component / Text Field / Height / <size>
|
|
13
|
+
* · Focus Edge shown only while focused, Focus Ring Inner Width
|
|
14
|
+
* · Control gap Inline; inset, radius and boundary per size; surface and border
|
|
15
|
+
* per appearance and condition
|
|
16
|
+
* · Leading Icon Size / Icon / Small
|
|
17
|
+
* · Prefix the content size, Text Secondary
|
|
18
|
+
* · Input fills the row
|
|
19
|
+
* · Suffix the content size, Text Secondary
|
|
20
|
+
* · Trailing twenty four
|
|
21
|
+
* · Instruction twelve over eighteen, Text Secondary
|
|
22
|
+
* · Supporting Region horizontal, space between, gap Message Counter
|
|
23
|
+
* · Form Message its own molecule
|
|
24
|
+
* · Counter twelve over eighteen, Text Secondary
|
|
25
|
+
*
|
|
26
|
+
* CONDITION IS NOT A CLASS. Figma's five Conditions are review evidence. Here
|
|
27
|
+
* they are :hover, [aria-invalid], [readonly] and :disabled on the real input,
|
|
28
|
+
* exactly as the Figma description asks.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
.ol8-field {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
gap: var(--ol8-component-textfield-gap-supporting);
|
|
35
|
+
inline-size: 100%;
|
|
36
|
+
|
|
37
|
+
--_surface: var(--ol8-color-surface-default);
|
|
38
|
+
--_border: var(--ol8-color-border-default);
|
|
39
|
+
--_label: var(--ol8-color-text-primary);
|
|
40
|
+
--_ink: var(--ol8-color-text-primary);
|
|
41
|
+
--_muted: var(--ol8-color-text-secondary);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* ---- Label ------------------------------------------------------------ */
|
|
45
|
+
.ol8-field__label-row {
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: flex-start;
|
|
48
|
+
gap: var(--ol8-component-textfield-gap-requirement);
|
|
49
|
+
}
|
|
50
|
+
.ol8-field__label {
|
|
51
|
+
color: var(--_label);
|
|
52
|
+
font-family: var(--ol8-font-family-functional);
|
|
53
|
+
font-size: var(--ol8-font-size-012);
|
|
54
|
+
font-weight: var(--ol8-font-weight-semibold);
|
|
55
|
+
line-height: var(--ol8-font-line-018);
|
|
56
|
+
letter-spacing: var(--ol8-font-tracking-open);
|
|
57
|
+
}
|
|
58
|
+
.ol8-field__requirement {
|
|
59
|
+
color: var(--ol8-color-feedbackcritical-content);
|
|
60
|
+
font-family: var(--ol8-font-family-functional);
|
|
61
|
+
font-size: var(--ol8-font-size-012);
|
|
62
|
+
font-weight: var(--ol8-font-weight-regular);
|
|
63
|
+
line-height: var(--ol8-font-line-018);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* ---- Control ---------------------------------------------------------- */
|
|
67
|
+
.ol8-field__control-stack {
|
|
68
|
+
position: relative;
|
|
69
|
+
display: flex;
|
|
70
|
+
block-size: var(--_height);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* The contract asks a coarse pointer adapter to expand the activation region
|
|
74
|
+
for Compact and Standard, which are shorter than the protected minimum,
|
|
75
|
+
without overlapping a neighbouring field. That belongs to a platform adapter:
|
|
76
|
+
an overlay drawn here would sit over the value and take the caret away from
|
|
77
|
+
the person, which is worse than the shortfall it fixes. Not implemented. */
|
|
78
|
+
|
|
79
|
+
.ol8-field__control {
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
gap: var(--ol8-component-textfield-gap-inline);
|
|
83
|
+
inline-size: 100%;
|
|
84
|
+
block-size: var(--_height);
|
|
85
|
+
padding-inline: var(--_inset);
|
|
86
|
+
border: var(--ol8-component-textfield-boundary) solid var(--_border);
|
|
87
|
+
border-radius: var(--_radius);
|
|
88
|
+
background: var(--_surface);
|
|
89
|
+
transition-property: border-color, background-color;
|
|
90
|
+
transition-duration: var(--ol8-motion-interaction-hover-duration);
|
|
91
|
+
transition-timing-function: var(--ol8-motion-interaction-hover-timing-function);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.ol8-field__leading-icon { flex: none; color: var(--ol8-color-icon-secondary); }
|
|
95
|
+
|
|
96
|
+
.ol8-field__affix {
|
|
97
|
+
flex: none;
|
|
98
|
+
color: var(--_muted);
|
|
99
|
+
font-family: var(--ol8-font-family-functional);
|
|
100
|
+
font-size: var(--_content-size);
|
|
101
|
+
font-weight: var(--ol8-font-weight-regular);
|
|
102
|
+
line-height: var(--_content-line);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* The input carries the value, so it takes the primary ink and hands the
|
|
106
|
+
secondary ink to its placeholder. That is exactly the Content molecule's
|
|
107
|
+
Value and Placeholder types. */
|
|
108
|
+
.ol8-field__input {
|
|
109
|
+
flex: 1 1 auto;
|
|
110
|
+
min-inline-size: 0;
|
|
111
|
+
border: 0;
|
|
112
|
+
padding: 0;
|
|
113
|
+
background: none;
|
|
114
|
+
color: var(--_ink);
|
|
115
|
+
font-family: var(--ol8-font-family-functional);
|
|
116
|
+
font-size: var(--_content-size);
|
|
117
|
+
font-weight: var(--ol8-font-weight-regular);
|
|
118
|
+
line-height: var(--_content-line);
|
|
119
|
+
}
|
|
120
|
+
.ol8-field__input::placeholder { color: var(--_muted); opacity: 1; }
|
|
121
|
+
.ol8-field__input:focus { outline: none; }
|
|
122
|
+
|
|
123
|
+
.ol8-field__trailing-action {
|
|
124
|
+
flex: none;
|
|
125
|
+
display: inline-flex;
|
|
126
|
+
border: 0;
|
|
127
|
+
padding: 0;
|
|
128
|
+
background: none;
|
|
129
|
+
color: var(--ol8-color-icon-secondary);
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
}
|
|
132
|
+
.ol8-field__trailing-action:disabled { color: var(--ol8-color-icon-disabled); cursor: not-allowed; }
|
|
133
|
+
|
|
134
|
+
/* ---- Size ------------------------------------------------------------- */
|
|
135
|
+
.ol8-field[data-ol8-size="compact"] {
|
|
136
|
+
--_height: var(--ol8-component-textfield-height-compact);
|
|
137
|
+
--_inset: var(--ol8-component-textfield-inset-compact);
|
|
138
|
+
--_radius: var(--ol8-component-textfield-radius-compact);
|
|
139
|
+
--_content-size: var(--ol8-font-size-012);
|
|
140
|
+
--_content-line: var(--ol8-font-line-018);
|
|
141
|
+
}
|
|
142
|
+
.ol8-field[data-ol8-size="standard"] {
|
|
143
|
+
--_height: var(--ol8-component-textfield-height-standard);
|
|
144
|
+
--_inset: var(--ol8-component-textfield-inset-standard);
|
|
145
|
+
--_radius: var(--ol8-component-textfield-radius-standard);
|
|
146
|
+
--_content-size: var(--ol8-font-size-016);
|
|
147
|
+
--_content-line: var(--ol8-font-line-024);
|
|
148
|
+
}
|
|
149
|
+
/* Comfortable grows its box and keeps the standard reading size, which is what
|
|
150
|
+
Figma's Content set does at this step. */
|
|
151
|
+
.ol8-field[data-ol8-size="comfortable"] {
|
|
152
|
+
--_height: var(--ol8-component-textfield-height-comfortable);
|
|
153
|
+
--_inset: var(--ol8-component-textfield-inset-comfortable);
|
|
154
|
+
--_radius: var(--ol8-component-textfield-radius-comfortable);
|
|
155
|
+
--_content-size: var(--ol8-font-size-016);
|
|
156
|
+
--_content-line: var(--ol8-font-line-024);
|
|
157
|
+
}
|
|
158
|
+
.ol8-field[data-ol8-size="large"] {
|
|
159
|
+
--_height: var(--ol8-component-textfield-height-large);
|
|
160
|
+
--_inset: var(--ol8-component-textfield-inset-large);
|
|
161
|
+
--_radius: var(--ol8-component-textfield-radius-large);
|
|
162
|
+
--_content-size: var(--ol8-font-size-021);
|
|
163
|
+
--_content-line: var(--ol8-font-line-030);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* ---- Appearance ------------------------------------------------------- */
|
|
167
|
+
.ol8-field[data-ol8-appearance="filled"] {
|
|
168
|
+
--_surface: var(--ol8-color-surface-sunken);
|
|
169
|
+
--_border: var(--ol8-color-border-subtle);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* ---- Condition, carried by the real input ----------------------------- */
|
|
173
|
+
/* Base condition precedence is Disabled, then Read only, then Invalid, then
|
|
174
|
+
Default. Hover is an overlay that renders only above Default or Invalid, and
|
|
175
|
+
only for a fine pointer. */
|
|
176
|
+
@media (hover: hover) and (pointer: fine) {
|
|
177
|
+
.ol8-field:hover:not([data-ol8-disabled]):not([data-ol8-readonly]) .ol8-field__control {
|
|
178
|
+
--_border: var(--ol8-color-border-strong);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.ol8-field[data-ol8-readonly] .ol8-field__control { --_border: var(--ol8-color-border-subtle); }
|
|
183
|
+
|
|
184
|
+
/* Focus never suppresses Invalid; the focus edge is added on top of it. */
|
|
185
|
+
.ol8-field[data-ol8-invalid] .ol8-field__control,
|
|
186
|
+
.ol8-field:has(.ol8-field__input[aria-invalid="true"]) .ol8-field__control {
|
|
187
|
+
--_border: var(--ol8-color-feedbackcritical-inlinecontent);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.ol8-field[data-ol8-disabled] {
|
|
191
|
+
--_label: var(--ol8-color-text-disabled);
|
|
192
|
+
--_muted: var(--ol8-color-text-disabled);
|
|
193
|
+
--_ink: var(--ol8-color-text-disabled);
|
|
194
|
+
}
|
|
195
|
+
.ol8-field[data-ol8-disabled] .ol8-field__control {
|
|
196
|
+
--_surface: var(--ol8-color-surface-disabled);
|
|
197
|
+
--_border: var(--ol8-color-border-disabled);
|
|
198
|
+
}
|
|
199
|
+
.ol8-field[data-ol8-disabled] .ol8-field__input { cursor: not-allowed; }
|
|
200
|
+
.ol8-field[data-ol8-disabled] .ol8-field__leading-icon { color: var(--ol8-color-icon-disabled); }
|
|
201
|
+
|
|
202
|
+
/* ---- Focus -----------------------------------------------------------
|
|
203
|
+
Figma draws a Focus Edge sibling that is shown only while focused, at the
|
|
204
|
+
Focus Ring Inner Width in the outer focus colour. The stack owns it so the
|
|
205
|
+
edge cannot change the control's own metrics. */
|
|
206
|
+
.ol8-field__control-stack::after {
|
|
207
|
+
content: "";
|
|
208
|
+
position: absolute;
|
|
209
|
+
inset: 0;
|
|
210
|
+
border-radius: var(--_radius);
|
|
211
|
+
box-shadow: inset 0 0 0 var(--ol8-focus-ring-innerwidth) var(--ol8-color-focus-outer);
|
|
212
|
+
opacity: 0;
|
|
213
|
+
pointer-events: none;
|
|
214
|
+
transition-property: opacity;
|
|
215
|
+
transition-duration: var(--ol8-motion-interaction-focus-duration);
|
|
216
|
+
transition-timing-function: var(--ol8-motion-interaction-focus-timing-function);
|
|
217
|
+
}
|
|
218
|
+
.ol8-field:has(.ol8-field__input:focus-visible) .ol8-field__control-stack::after { opacity: 1; }
|
|
219
|
+
|
|
220
|
+
@media (prefers-reduced-motion: reduce) {
|
|
221
|
+
.ol8-field__control,
|
|
222
|
+
.ol8-field__control-stack::after { transition-duration: var(--ol8-motion-duration-instant); }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* ---- Instruction, message and counter --------------------------------- */
|
|
226
|
+
.ol8-field__instruction {
|
|
227
|
+
color: var(--_muted);
|
|
228
|
+
font-family: var(--ol8-font-family-functional);
|
|
229
|
+
font-size: var(--ol8-font-size-012);
|
|
230
|
+
font-weight: var(--ol8-font-weight-regular);
|
|
231
|
+
line-height: var(--ol8-font-line-018);
|
|
232
|
+
letter-spacing: var(--ol8-font-tracking-open);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* The message and the counter share one row, counter at the logical inline end.
|
|
236
|
+
At a narrow width or with enlarged text the counter wraps below the message in
|
|
237
|
+
source order, and the row gap becomes the tighter status step. The counter is
|
|
238
|
+
never hidden merely because a message appeared. */
|
|
239
|
+
.ol8-field__supporting {
|
|
240
|
+
display: flex;
|
|
241
|
+
flex-wrap: wrap;
|
|
242
|
+
align-items: baseline;
|
|
243
|
+
justify-content: space-between;
|
|
244
|
+
column-gap: var(--ol8-component-textfield-gap-messagecounter);
|
|
245
|
+
row-gap: var(--ol8-component-textfield-gap-status);
|
|
246
|
+
}
|
|
247
|
+
.ol8-field__supporting-spacer { flex: 1 1 auto; }
|
|
248
|
+
|
|
249
|
+
/* Counter is information, not validation, so it never takes a feedback colour. */
|
|
250
|
+
.ol8-field__counter {
|
|
251
|
+
flex: none;
|
|
252
|
+
color: var(--_muted);
|
|
253
|
+
font-family: var(--ol8-font-family-functional);
|
|
254
|
+
font-size: var(--ol8-font-size-012);
|
|
255
|
+
font-weight: var(--ol8-font-weight-regular);
|
|
256
|
+
line-height: var(--ol8-font-line-018);
|
|
257
|
+
letter-spacing: var(--ol8-font-tracking-open);
|
|
258
|
+
font-variant-numeric: tabular-nums;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
@media (forced-colors: active) {
|
|
262
|
+
.ol8-field__control { border-color: ButtonBorder; }
|
|
263
|
+
.ol8-field:has(.ol8-field__input:focus-visible) .ol8-field__control-stack::after {
|
|
264
|
+
box-shadow: inset 0 0 0 var(--ol8-focus-ring-innerwidth) Highlight;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Field (MOLECULE). Figma 182:33 Outline, 186:53 Filled, 211:36 and
|
|
3
|
+
* 215:226 Gem.
|
|
4
|
+
*
|
|
5
|
+
* Figma's five Conditions are review evidence, never options. Default, Hover,
|
|
6
|
+
* Invalid, Read-only and Disabled are :hover, aria-invalid, readOnly and
|
|
7
|
+
* disabled on the native input. Gem changes token bound material only.
|
|
8
|
+
*/
|
|
9
|
+
export type Ol8TextFieldSize = 'compact' | 'standard' | 'comfortable' | 'large';
|
|
10
|
+
export type Ol8TextFieldAppearance = 'outline' | 'filled';
|
|
11
|
+
export type Ol8TextFieldMaterial = 'regular' | 'gem';
|
|
12
|
+
/** Pending is a validation process status, not a fifth message tone. */
|
|
13
|
+
export type Ol8MessageTone = 'critical' | 'caution' | 'positive' | 'informative';
|
|
14
|
+
export type Ol8ValidationStatus = 'idle' | 'pending' | 'resolved';
|
|
15
|
+
export type Ol8CharacterLimitBehavior = 'soft' | 'hard';
|
|
16
|
+
|
|
17
|
+
export declare const OL8_TEXT_FIELD_SIZES: readonly Ol8TextFieldSize[];
|
|
18
|
+
export declare const OL8_TEXT_FIELD_APPEARANCES: readonly Ol8TextFieldAppearance[];
|
|
19
|
+
export declare const OL8_TEXT_FIELD_MATERIALS: readonly Ol8TextFieldMaterial[];
|
|
20
|
+
export declare const OL8_MESSAGE_TONES: readonly Ol8MessageTone[];
|
|
21
|
+
export declare const OL8_VALIDATION_STATUSES: readonly Ol8ValidationStatus[];
|
|
22
|
+
export declare const OL8_CHARACTER_LIMIT_BEHAVIORS: readonly Ol8CharacterLimitBehavior[];
|
|
23
|
+
|
|
24
|
+
export interface Ol8TextFieldOptions {
|
|
25
|
+
/** Required. A field nobody can name is a field nobody can fill. */
|
|
26
|
+
label: string;
|
|
27
|
+
name?: string;
|
|
28
|
+
id?: string;
|
|
29
|
+
type?: string;
|
|
30
|
+
value?: string;
|
|
31
|
+
placeholder?: string;
|
|
32
|
+
size?: Ol8TextFieldSize;
|
|
33
|
+
appearance?: Ol8TextFieldAppearance;
|
|
34
|
+
material?: Ol8TextFieldMaterial;
|
|
35
|
+
/** False keeps the label as the accessible name without showing it, which needs a documented exception. */
|
|
36
|
+
showLabel?: boolean;
|
|
37
|
+
required?: boolean;
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
readOnly?: boolean;
|
|
40
|
+
/** Correction is required. Never set without also exposing a Critical message. */
|
|
41
|
+
invalid?: boolean;
|
|
42
|
+
leadingIcon?: string;
|
|
43
|
+
prefix?: string;
|
|
44
|
+
suffix?: string;
|
|
45
|
+
trailingAction?: { icon: string; label: string };
|
|
46
|
+
instruction?: string;
|
|
47
|
+
message?: string;
|
|
48
|
+
messageTone?: Ol8MessageTone;
|
|
49
|
+
messageIcon?: boolean;
|
|
50
|
+
/** Pending shows the loader; it never freezes typing. */
|
|
51
|
+
validationStatus?: Ol8ValidationStatus;
|
|
52
|
+
/** Counted in graphemes, so an emoji counts as one. */
|
|
53
|
+
characterLimit?: number;
|
|
54
|
+
/** Soft lets a person overshoot and then correct. Hard is for real technical limits. */
|
|
55
|
+
characterLimitBehavior?: Ol8CharacterLimitBehavior;
|
|
56
|
+
className?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @throws without a label, on an unknown size, appearance, material, tone or status, or when a read only field is marked invalid. */
|
|
60
|
+
export declare function renderTextField(options: Ol8TextFieldOptions): string;
|
|
61
|
+
|
|
62
|
+
/** Enforces the runtime contract on existing `.ol8-field` markup. Returns the count of fields with no native input. */
|
|
63
|
+
export declare function hydrateTextFields(root?: ParentNode): number;
|
|
64
|
+
|
|
65
|
+
/** Counts what a reader would call characters. */
|
|
66
|
+
export declare function countGraphemes(value: unknown): number;
|
|
67
|
+
|
|
68
|
+
/** Keeps the first `limit` graphemes, so a cluster is never cut in half. */
|
|
69
|
+
export declare function clipToGraphemes(value: unknown, limit: number): string;
|