@oneli8/core 1.0.0-beta.5 → 1.0.0-beta.7
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/README.md +19 -7
- package/ai-context.json +170 -3
- package/package.json +18 -3
- package/src/atoms/index.d.ts +1 -0
- package/src/atoms/index.js +1 -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/selection-indicator.css +13 -4
- package/src/materials/gem/gem.css +52 -5
- 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/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/index.d.ts +6 -0
- package/src/molecules/index.js +6 -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/selection-option.js +4 -4
- 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-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 +7 -0
- package/src/organisms/index.js +7 -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 +18 -2
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oneli8 · Link (MOLECULE)
|
|
3
|
+
*
|
|
4
|
+
* Figma (138:260, 139:260, 139:453): "Runtime href, native anchor behavior,
|
|
5
|
+
* visited resolution, aria-current, motion preference and navigation list
|
|
6
|
+
* hierarchy live in code." Figma's five States are visual evidence for review,
|
|
7
|
+
* not classes: they are :hover, :active, :focus-visible, :visited and
|
|
8
|
+
* [aria-current] here.
|
|
9
|
+
*
|
|
10
|
+
* "Icons remain intentionally parked and must not be added as local artwork."
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
.ol8-link {
|
|
14
|
+
position: relative;
|
|
15
|
+
color: var(--ol8-color-text-link-default);
|
|
16
|
+
font-family: var(--ol8-font-family-functional);
|
|
17
|
+
font-weight: var(--ol8-font-weight-semibold);
|
|
18
|
+
text-decoration-line: underline;
|
|
19
|
+
text-decoration-thickness: var(--ol8-component-link-underline-default);
|
|
20
|
+
text-underline-offset: var(--ol8-component-link-underline-offset);
|
|
21
|
+
transition-property: color, text-decoration-thickness, background-color, box-shadow;
|
|
22
|
+
transition-duration: var(--ol8-motion-interaction-hover-duration);
|
|
23
|
+
transition-timing-function: var(--ol8-motion-interaction-hover-timing-function);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* ---- Size ------------------------------------------------------------- */
|
|
27
|
+
.ol8-link[data-ol8-size="small"] {
|
|
28
|
+
font-size: var(--ol8-typography-body-small-font-size);
|
|
29
|
+
line-height: var(--ol8-typography-body-small-line-height);
|
|
30
|
+
letter-spacing: var(--ol8-typography-body-small-letter-spacing);
|
|
31
|
+
}
|
|
32
|
+
.ol8-link[data-ol8-size="standard"] {
|
|
33
|
+
font-size: var(--ol8-typography-body-medium-font-size);
|
|
34
|
+
line-height: var(--ol8-typography-body-medium-line-height);
|
|
35
|
+
letter-spacing: var(--ol8-typography-body-medium-letter-spacing);
|
|
36
|
+
}
|
|
37
|
+
.ol8-link[data-ol8-size="large"] {
|
|
38
|
+
font-size: var(--ol8-typography-body-large-font-size);
|
|
39
|
+
line-height: var(--ol8-typography-body-large-line-height);
|
|
40
|
+
letter-spacing: var(--ol8-typography-body-large-letter-spacing);
|
|
41
|
+
}
|
|
42
|
+
/* Not a Figma size. An inline link inside body copy that does not follow its
|
|
43
|
+
surrounding type is broken, and Figma cannot express "inherit" as a variant.
|
|
44
|
+
Flagged for the design source rather than left undocumented. */
|
|
45
|
+
.ol8-link[data-ol8-size="inherit"] {
|
|
46
|
+
font-size: inherit;
|
|
47
|
+
line-height: inherit;
|
|
48
|
+
letter-spacing: inherit;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* ---- State ------------------------------------------------------------ */
|
|
52
|
+
.ol8-link:hover,
|
|
53
|
+
.ol8-link:active {
|
|
54
|
+
text-decoration-thickness: var(--ol8-component-link-underline-emphasis);
|
|
55
|
+
}
|
|
56
|
+
.ol8-link:hover { color: var(--ol8-color-text-link-hover); }
|
|
57
|
+
.ol8-link:active { color: var(--ol8-color-text-link-pressed); }
|
|
58
|
+
|
|
59
|
+
/* Navigation links live in a list where visited carries no meaning. */
|
|
60
|
+
.ol8-link[data-ol8-form="inline"]:visited,
|
|
61
|
+
.ol8-link[data-ol8-form="standalone"]:visited {
|
|
62
|
+
color: var(--ol8-color-text-link-visited);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.ol8-link:focus-visible {
|
|
66
|
+
outline: none;
|
|
67
|
+
color: var(--ol8-color-text-primary);
|
|
68
|
+
text-decoration-color: var(--ol8-color-text-link-default);
|
|
69
|
+
border-radius: var(--ol8-shape-radius-subtle);
|
|
70
|
+
box-shadow:
|
|
71
|
+
0 0 0 var(--ol8-focus-ring-innerwidth) var(--ol8-color-focus-inner),
|
|
72
|
+
0 0 0 var(--ol8-focus-ring-totalwidth) var(--ol8-color-focus-outer);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ---- Form ------------------------------------------------------------- */
|
|
76
|
+
/* Standalone and navigation are targets in their own right, so they take the
|
|
77
|
+
protected minimum. Inline sits in a sentence and must not. Figma pads both by
|
|
78
|
+
the minimal inline step and carries a gap for the parked icon slot. */
|
|
79
|
+
.ol8-link[data-ol8-form="standalone"],
|
|
80
|
+
.ol8-link[data-ol8-form="navigation"] {
|
|
81
|
+
display: inline-flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
gap: var(--ol8-component-link-gap-standard);
|
|
84
|
+
min-block-size: var(--ol8-component-link-targetnavigation);
|
|
85
|
+
padding-inline: var(--ol8-spacing-inline-minimal);
|
|
86
|
+
}
|
|
87
|
+
.ol8-link[data-ol8-form="standalone"][data-ol8-size="small"],
|
|
88
|
+
.ol8-link[data-ol8-form="navigation"][data-ol8-size="small"] {
|
|
89
|
+
gap: var(--ol8-component-link-gap-small);
|
|
90
|
+
}
|
|
91
|
+
.ol8-link[data-ol8-form="standalone"][data-ol8-size="large"],
|
|
92
|
+
.ol8-link[data-ol8-form="navigation"][data-ol8-size="large"] {
|
|
93
|
+
gap: var(--ol8-component-link-gap-large);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Figma keeps the underline in every navigation state and marks the current
|
|
97
|
+
destination by thickening it to the current marker weight. */
|
|
98
|
+
.ol8-link[data-ol8-form="navigation"][aria-current] {
|
|
99
|
+
text-decoration-thickness: var(--ol8-component-link-underline-current);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* ---- Motion ----------------------------------------------------------- */
|
|
103
|
+
.ol8-link[data-ol8-motion="none"] { transition-duration: var(--ol8-motion-duration-instant); }
|
|
104
|
+
@media (prefers-reduced-motion: reduce) {
|
|
105
|
+
.ol8-link { transition-duration: var(--ol8-motion-duration-instant); }
|
|
106
|
+
}
|
|
107
|
+
@media (forced-colors: active) {
|
|
108
|
+
.ol8-link { forced-color-adjust: auto; }
|
|
109
|
+
.ol8-link:focus-visible {
|
|
110
|
+
outline: var(--ol8-focus-ring-innerwidth) solid Highlight;
|
|
111
|
+
outline-offset: calc(var(--ol8-focus-ring-innerwidth) * -1);
|
|
112
|
+
box-shadow: none;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link (MOLECULE) — headless behavior. Figma's five States are visual evidence
|
|
3
|
+
* for review, never classes: :hover, :active, :focus-visible, :visited and
|
|
4
|
+
* [aria-current] carry them. Icons are intentionally parked.
|
|
5
|
+
*/
|
|
6
|
+
export type Ol8LinkForm = 'inline' | 'standalone' | 'navigation';
|
|
7
|
+
export type Ol8LinkSize = 'small' | 'standard' | 'large';
|
|
8
|
+
export type Ol8LinkMotion = 'system' | 'none';
|
|
9
|
+
|
|
10
|
+
export declare const OL8_LINK_FORMS: readonly Ol8LinkForm[];
|
|
11
|
+
export declare const OL8_LINK_SIZES: readonly Ol8LinkSize[];
|
|
12
|
+
export declare const OL8_LINK_MOTIONS: readonly Ol8LinkMotion[];
|
|
13
|
+
|
|
14
|
+
export interface Ol8LinkOptions {
|
|
15
|
+
form?: Ol8LinkForm;
|
|
16
|
+
size?: Ol8LinkSize;
|
|
17
|
+
/** Navigation only. `true` emits aria-current="page"; a string names the kind. */
|
|
18
|
+
current?: boolean | string;
|
|
19
|
+
motion?: Ol8LinkMotion;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @throws without an href, on an unknown form or size, or for current outside navigation. */
|
|
24
|
+
export declare function renderLink(label: string, href: string, options?: Ol8LinkOptions): string;
|
|
25
|
+
|
|
26
|
+
/** Enforces the runtime contract on existing `.ol8-link` markup. Returns the count of elements that are not anchors with an href. */
|
|
27
|
+
export declare function hydrateLinks(root?: ParentNode): number;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oneli8 · Link — headless behavior.
|
|
3
|
+
*
|
|
4
|
+
* The Figma component description is explicit that "runtime href, native anchor
|
|
5
|
+
* behavior, visited resolution, aria-current, motion preference and navigation
|
|
6
|
+
* list hierarchy live in code". This module is that code.
|
|
7
|
+
*
|
|
8
|
+
* Figma's five States are visual evidence for review. They are never classes:
|
|
9
|
+
* :hover, :active, :focus-visible, :visited and [aria-current] carry them.
|
|
10
|
+
*/
|
|
11
|
+
export const OL8_LINK_FORMS = ['inline', 'standalone', 'navigation'];
|
|
12
|
+
export const OL8_LINK_SIZES = ['small', 'standard', 'large'];
|
|
13
|
+
export const OL8_LINK_MOTIONS = ['system', 'none'];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} label
|
|
17
|
+
* @param {string} href
|
|
18
|
+
* @param {{form?:string,size?:string,current?:boolean|string,motion?:string,className?:string}} [options]
|
|
19
|
+
*/
|
|
20
|
+
export function renderLink(label, href, options = {}) {
|
|
21
|
+
const { form = 'inline', size = 'standard', current, motion = 'system', className } = options;
|
|
22
|
+
|
|
23
|
+
if (!OL8_LINK_FORMS.includes(form)) throw new Error(`[ol8] unknown link form "${form}"`);
|
|
24
|
+
if (!OL8_LINK_SIZES.includes(size)) throw new Error(`[ol8] unknown link size "${size}"`);
|
|
25
|
+
if (!OL8_LINK_MOTIONS.includes(motion)) throw new Error(`[ol8] unknown link motion "${motion}"`);
|
|
26
|
+
if (typeof href !== 'string' || href === '') {
|
|
27
|
+
throw new Error('[ol8] Link requires an href. A link without a destination is a button.');
|
|
28
|
+
}
|
|
29
|
+
if (current !== undefined && form !== 'navigation') {
|
|
30
|
+
throw new Error('[ol8] aria-current belongs to a navigation link, not an inline or standalone one.');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const attrs = [
|
|
34
|
+
`class="ol8-link${className ? ` ${className}` : ''}"`,
|
|
35
|
+
`href="${escapeAttr(href)}"`,
|
|
36
|
+
`data-ol8-form="${form}"`,
|
|
37
|
+
`data-ol8-size="${size}"`,
|
|
38
|
+
motion === 'none' ? 'data-ol8-motion="none"' : '',
|
|
39
|
+
// true means this page; a string names what kind of current it is
|
|
40
|
+
current ? `aria-current="${current === true ? 'page' : escapeAttr(String(current))}"` : '',
|
|
41
|
+
].filter(Boolean).join(' ');
|
|
42
|
+
|
|
43
|
+
return `<a ${attrs}>${escapeText(label)}</a>`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Enforces the runtime contract on existing `.ol8-link` markup. Idempotent.
|
|
48
|
+
* - an anchor with no href is not a link, so it is reported rather than styled
|
|
49
|
+
* - a form is required for the size and target rules to apply
|
|
50
|
+
* - aria-current outside a navigation link is removed, since it claims a
|
|
51
|
+
* position in a set that does not exist
|
|
52
|
+
*/
|
|
53
|
+
export function hydrateLinks(root = document) {
|
|
54
|
+
const problems = [];
|
|
55
|
+
for (const el of root.querySelectorAll('.ol8-link')) {
|
|
56
|
+
if (el.tagName !== 'A' || !el.getAttribute('href')) {
|
|
57
|
+
problems.push(el);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!el.dataset.ol8Form) el.dataset.ol8Form = 'inline';
|
|
61
|
+
if (!el.dataset.ol8Size) el.dataset.ol8Size = 'standard';
|
|
62
|
+
if (el.hasAttribute('aria-current') && el.dataset.ol8Form !== 'navigation') {
|
|
63
|
+
el.removeAttribute('aria-current');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (problems.length > 0) {
|
|
67
|
+
console.warn(`[ol8] ${problems.length} .ol8-link element(s) are not anchors with an href`, problems);
|
|
68
|
+
}
|
|
69
|
+
return problems.length;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Defined locally, matching every other molecule in this package. Six modules
|
|
73
|
+
// carry their own copy; consolidating them is a separate change, not part of
|
|
74
|
+
// restoring Link.
|
|
75
|
+
function escapeAttr(v) { return String(v).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<'); }
|
|
76
|
+
function escapeText(v) { return String(v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { renderNavigationItem, OL8_NAVIGATION_KINDS, OL8_NAVIGATION_SIZES, OL8_NAVIGATION_LAYOUTS } from './navigation-item.js';
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oneli8 · Navigation Item (MOLECULE, internal)
|
|
3
|
+
* Figma: Tab Item 688:51, Segment Item 695:167, Destination Item 700:292.
|
|
4
|
+
*
|
|
5
|
+
* All three are one molecule. Read from Figma: a box at the size's control
|
|
6
|
+
* height with centred content, a horizontal content row at the canonical
|
|
7
|
+
* navigation gap, an eighteen unit icon, a Semibold label at the size's reading
|
|
8
|
+
* step, and an optional badge. States are identical across the three:
|
|
9
|
+
*
|
|
10
|
+
* Hover Action Quiet Surface Hover + Action Quiet Content Hover
|
|
11
|
+
* Pressed Action Quiet Surface Pressed + Action Quiet Content Pressed
|
|
12
|
+
* Focus an inward Focus Inner edge, so a parent cannot crop it
|
|
13
|
+
* Disabled Text Disabled, no surface
|
|
14
|
+
*
|
|
15
|
+
* Only the selection language differs. Tab Item raises an indicator bar in the
|
|
16
|
+
* primary colour, inset by the governed amount. Segment Item and Destination
|
|
17
|
+
* Item take the selection surface and content instead.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
.ol8-nav-item {
|
|
21
|
+
position: relative;
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
justify-content: center;
|
|
25
|
+
box-sizing: border-box;
|
|
26
|
+
block-size: var(--_height);
|
|
27
|
+
border: 0;
|
|
28
|
+
border-radius: var(--_radius);
|
|
29
|
+
padding-inline: var(--_inset);
|
|
30
|
+
background: transparent;
|
|
31
|
+
color: var(--ol8-color-text-secondary);
|
|
32
|
+
font-family: var(--ol8-font-family-functional);
|
|
33
|
+
font-weight: var(--ol8-font-weight-semibold);
|
|
34
|
+
text-decoration: none;
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
transition-property: background-color, color;
|
|
37
|
+
transition-duration: var(--ol8-motion-interaction-hover-duration);
|
|
38
|
+
transition-timing-function: var(--ol8-motion-interaction-hover-timing-function);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.ol8-nav-item__content {
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
gap: var(--ol8-component-navigation-item-gap);
|
|
45
|
+
min-inline-size: 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* "Labels do not wrap." A narrow list scrolls; it never grows a second line. */
|
|
49
|
+
.ol8-nav-item__label {
|
|
50
|
+
white-space: nowrap;
|
|
51
|
+
font-size: var(--_label-size);
|
|
52
|
+
line-height: var(--_label-line);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.ol8-nav-item__icon { flex: none; }
|
|
56
|
+
|
|
57
|
+
.ol8-nav-item[data-ol8-layout="stacked"] .ol8-nav-item__content {
|
|
58
|
+
flex-direction: column;
|
|
59
|
+
gap: 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ---- Size ------------------------------------------------------------- */
|
|
63
|
+
.ol8-nav-item[data-ol8-size="compact"] {
|
|
64
|
+
--_height: var(--ol8-component-tab-item-height-compact);
|
|
65
|
+
--_inset: var(--ol8-component-tab-item-inset-compact);
|
|
66
|
+
--_label-size: var(--ol8-font-size-012);
|
|
67
|
+
--_label-line: var(--ol8-font-line-018);
|
|
68
|
+
}
|
|
69
|
+
.ol8-nav-item[data-ol8-size="standard"] {
|
|
70
|
+
--_height: var(--ol8-component-tab-item-height-standard);
|
|
71
|
+
--_inset: var(--ol8-component-tab-item-inset-standard);
|
|
72
|
+
--_label-size: var(--ol8-font-size-016);
|
|
73
|
+
--_label-line: var(--ol8-font-line-024);
|
|
74
|
+
}
|
|
75
|
+
.ol8-nav-item[data-ol8-size="comfortable"] {
|
|
76
|
+
--_height: var(--ol8-component-tab-item-height-comfortable);
|
|
77
|
+
--_inset: var(--ol8-component-tab-item-inset-comfortable);
|
|
78
|
+
--_label-size: var(--ol8-font-size-016);
|
|
79
|
+
--_label-line: var(--ol8-font-line-024);
|
|
80
|
+
}
|
|
81
|
+
.ol8-nav-item[data-ol8-size="large"] {
|
|
82
|
+
--_height: var(--ol8-component-tab-item-height-large);
|
|
83
|
+
--_inset: var(--ol8-component-tab-item-inset-large);
|
|
84
|
+
--_label-size: var(--ol8-font-size-021);
|
|
85
|
+
--_label-line: var(--ol8-font-line-030);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.ol8-nav-item--tab,
|
|
89
|
+
.ol8-nav-item--segment { --_radius: var(--ol8-component-tab-item-radius); }
|
|
90
|
+
.ol8-nav-item--destination { --_radius: var(--ol8-component-tab-bar-item-radius); }
|
|
91
|
+
|
|
92
|
+
/* ---- State ------------------------------------------------------------ */
|
|
93
|
+
@media (hover: hover) and (pointer: fine) {
|
|
94
|
+
.ol8-nav-item:hover:not(:disabled) {
|
|
95
|
+
background: var(--ol8-color-actionquiet-surfacehover);
|
|
96
|
+
color: var(--ol8-color-actionquiet-contenthover);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
.ol8-nav-item:active:not(:disabled) {
|
|
100
|
+
background: var(--ol8-color-actionquiet-surfacepressed);
|
|
101
|
+
color: var(--ol8-color-actionquiet-contentpressed);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* "Focus uses the approved inward treatment so parent clipping cannot crop it."
|
|
105
|
+
An outward ring would be cut by a scrolling tab list. */
|
|
106
|
+
.ol8-nav-item:focus-visible {
|
|
107
|
+
outline: none;
|
|
108
|
+
box-shadow: inset 0 0 0 var(--ol8-component-navigation-focus-inset) var(--ol8-color-focus-inner);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.ol8-nav-item:disabled {
|
|
112
|
+
color: var(--ol8-color-text-disabled);
|
|
113
|
+
background: transparent;
|
|
114
|
+
cursor: not-allowed;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* ---- Selection · Tab Item raises an indicator ------------------------- */
|
|
118
|
+
.ol8-nav-item__indicator {
|
|
119
|
+
position: absolute;
|
|
120
|
+
inset-block-end: 0;
|
|
121
|
+
inset-inline: var(--ol8-component-tab-item-indicator-inset);
|
|
122
|
+
block-size: var(--ol8-component-tab-item-indicator-thickness);
|
|
123
|
+
border-radius: var(--ol8-shape-radius-full);
|
|
124
|
+
background: transparent;
|
|
125
|
+
}
|
|
126
|
+
.ol8-nav-item--tab[aria-selected="true"] {
|
|
127
|
+
color: var(--ol8-color-actionprimary-default);
|
|
128
|
+
}
|
|
129
|
+
.ol8-nav-item--tab[aria-selected="true"] > .ol8-nav-item__indicator {
|
|
130
|
+
background: var(--ol8-color-actionprimary-default);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* ---- Selection · Segment and Destination take the surface ------------- */
|
|
134
|
+
.ol8-nav-item--segment[aria-pressed="true"],
|
|
135
|
+
.ol8-nav-item--destination[aria-current] {
|
|
136
|
+
background: var(--ol8-color-selection-surface);
|
|
137
|
+
color: var(--ol8-color-selection-content);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@media (prefers-reduced-motion: reduce) {
|
|
141
|
+
.ol8-nav-item { transition-duration: var(--ol8-motion-duration-instant); }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@media (forced-colors: active) {
|
|
145
|
+
.ol8-nav-item:focus-visible {
|
|
146
|
+
outline: var(--ol8-component-navigation-focus-inset) solid Highlight;
|
|
147
|
+
outline-offset: calc(var(--ol8-component-navigation-focus-inset) * -1);
|
|
148
|
+
box-shadow: none;
|
|
149
|
+
}
|
|
150
|
+
.ol8-nav-item--tab[aria-selected="true"] > .ol8-nav-item__indicator { background: Highlight; }
|
|
151
|
+
.ol8-nav-item--segment[aria-pressed="true"],
|
|
152
|
+
.ol8-nav-item--destination[aria-current] { background: Highlight; color: HighlightText; }
|
|
153
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oneli8 · Navigation Item — the shared internal molecule.
|
|
3
|
+
*
|
|
4
|
+
* Figma draws three of these: Tab Item (688:51), Segment Item (695:167) and
|
|
5
|
+
* Destination Item (700:292). They are the same molecule with three selection
|
|
6
|
+
* languages, and the navigation contract is explicit that all three are
|
|
7
|
+
* internal: "The public package exports the three Organisms, preventing
|
|
8
|
+
* consumers from breaking group ownership or creating disconnected state
|
|
9
|
+
* behavior." So nothing here is re-exported from the package index.
|
|
10
|
+
*
|
|
11
|
+
* Figma's six States are review evidence, never classes. Hover, Pressed and
|
|
12
|
+
* Focus are browser interactions; Selected and Current are owned by the parent
|
|
13
|
+
* organism; Disabled is the native attribute.
|
|
14
|
+
*/
|
|
15
|
+
import { renderIcon } from '../../atoms/icon/index.js';
|
|
16
|
+
import { renderNavigationBadge } from '../../atoms/navigation-badge/index.js';
|
|
17
|
+
|
|
18
|
+
export const OL8_NAVIGATION_KINDS = ['tab', 'segment', 'destination'];
|
|
19
|
+
export const OL8_NAVIGATION_SIZES = ['compact', 'standard', 'comfortable', 'large'];
|
|
20
|
+
export const OL8_NAVIGATION_LAYOUTS = ['inline', 'stacked'];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {{
|
|
24
|
+
* kind:string, label?:string, icon?:string, badge?:string|number,
|
|
25
|
+
* layout?:string, selected?:boolean, disabled?:boolean,
|
|
26
|
+
* ariaLabel?:string, id?:string, controls?:string, href?:string,
|
|
27
|
+
* tabIndex?:number, extraAttrs?:string,
|
|
28
|
+
* }} item
|
|
29
|
+
*/
|
|
30
|
+
export function renderNavigationItem(item) {
|
|
31
|
+
const {
|
|
32
|
+
kind, label, icon, badge, layout = 'inline',
|
|
33
|
+
selected = false, disabled = false, ariaLabel, id, controls, href,
|
|
34
|
+
tabIndex, extraAttrs = '',
|
|
35
|
+
} = item;
|
|
36
|
+
|
|
37
|
+
if (!OL8_NAVIGATION_KINDS.includes(kind)) throw new Error(`[ol8] unknown navigation item kind "${kind}"`);
|
|
38
|
+
if (!OL8_NAVIGATION_LAYOUTS.includes(layout)) throw new Error(`[ol8] unknown navigation layout "${layout}"`);
|
|
39
|
+
if (!label && !icon) throw new Error('[ol8] a navigation item needs a label, an icon, or both.');
|
|
40
|
+
// "Icon-only items require ariaLabel and a product level discoverable Tooltip."
|
|
41
|
+
if (!label && !ariaLabel) {
|
|
42
|
+
throw new Error('[ol8] an icon only navigation item requires an ariaLabel, since a glyph is not a name.');
|
|
43
|
+
}
|
|
44
|
+
// "Disabled TabBar destination is unsupported and blocks; no silent redesign."
|
|
45
|
+
if (kind === 'destination' && disabled) {
|
|
46
|
+
throw new Error('[ol8] a destination cannot be disabled. A place a person cannot go does not belong in navigation.');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const content =
|
|
50
|
+
`<span class="ol8-nav-item__content">` +
|
|
51
|
+
(icon ? renderIcon(icon, { size: 18, className: 'ol8-nav-item__icon' }) : '') +
|
|
52
|
+
(label ? `<span class="ol8-nav-item__label">${escapeText(label)}</span>` : '') +
|
|
53
|
+
(badge !== undefined && badge !== null && badge !== '' ? renderNavigationBadge(badge) : '') +
|
|
54
|
+
`</span>`;
|
|
55
|
+
|
|
56
|
+
// Tab Item marks selection with an indicator bar; Segment and Destination
|
|
57
|
+
// mark it with the selection surface. The bar is always in the DOM so
|
|
58
|
+
// selecting does not change the box.
|
|
59
|
+
const indicator = kind === 'tab' ? `<span class="ol8-nav-item__indicator" aria-hidden="true"></span>` : '';
|
|
60
|
+
|
|
61
|
+
const shared = [
|
|
62
|
+
`class="ol8-nav-item ol8-nav-item--${kind}"`,
|
|
63
|
+
`data-ol8-layout="${layout}"`,
|
|
64
|
+
id ? `id="${escapeAttr(id)}"` : '',
|
|
65
|
+
ariaLabel ? `aria-label="${escapeAttr(ariaLabel)}"` : '',
|
|
66
|
+
extraAttrs,
|
|
67
|
+
].filter(Boolean);
|
|
68
|
+
|
|
69
|
+
if (kind === 'destination') {
|
|
70
|
+
if (typeof href !== 'string' || href === '') {
|
|
71
|
+
throw new Error('[ol8] a destination requires an href. Tab Bar navigates, so it uses real links.');
|
|
72
|
+
}
|
|
73
|
+
const attrs = [
|
|
74
|
+
...shared,
|
|
75
|
+
`href="${escapeAttr(href)}"`,
|
|
76
|
+
selected ? 'aria-current="page"' : '',
|
|
77
|
+
].filter(Boolean).join(' ');
|
|
78
|
+
return `<a ${attrs}>${content}</a>`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const attrs = [
|
|
82
|
+
...shared,
|
|
83
|
+
'type="button"',
|
|
84
|
+
kind === 'tab' ? 'role="tab"' : '',
|
|
85
|
+
kind === 'tab' ? `aria-selected="${selected}"` : `aria-pressed="${selected}"`,
|
|
86
|
+
kind === 'tab' && controls ? `aria-controls="${escapeAttr(controls)}"` : '',
|
|
87
|
+
disabled ? 'disabled' : '',
|
|
88
|
+
tabIndex !== undefined ? `tabindex="${tabIndex}"` : '',
|
|
89
|
+
].filter(Boolean).join(' ');
|
|
90
|
+
|
|
91
|
+
return `<button ${attrs}>${content}${indicator}</button>`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function escapeAttr(v) { return String(v).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<'); }
|
|
95
|
+
function escapeText(v) { return String(v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { renderSelect, OL8_SELECT_SIZES, OL8_SELECT_APPEARANCES, OL8_SELECT_MATERIALS, OL8_SELECT_NATIVE_POLICIES } from './select.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oneli8 · Select (MOLECULE)
|
|
3
|
+
* Figma: Select / Trigger 583:338, Molecule / Select 875:1778.
|
|
4
|
+
*
|
|
5
|
+
* "Geometry and appearance reuse approved Text Field tokens", and Figma proves
|
|
6
|
+
* it: every bound variable on the trigger control is a Text Field one. So
|
|
7
|
+
* Select carries the field classes and adds only what a native select needs,
|
|
8
|
+
* which is the disclosure mark and the removal of the platform's own arrow.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
.ol8-select__input {
|
|
12
|
+
appearance: none;
|
|
13
|
+
cursor: pointer;
|
|
14
|
+
}
|
|
15
|
+
.ol8-select__input:disabled { cursor: not-allowed; }
|
|
16
|
+
|
|
17
|
+
/* The native control keeps its own text colour rules, so the value takes the
|
|
18
|
+
primary ink while an unselected placeholder option stays secondary. */
|
|
19
|
+
.ol8-select__input:has(option[value=""]:checked) { color: var(--ol8-color-text-secondary); }
|
|
20
|
+
|
|
21
|
+
.ol8-select__disclosure {
|
|
22
|
+
flex: none;
|
|
23
|
+
color: var(--ol8-color-icon-secondary);
|
|
24
|
+
pointer-events: none;
|
|
25
|
+
}
|
|
26
|
+
.ol8-select[data-ol8-disabled] .ol8-select__disclosure { color: var(--ol8-color-icon-disabled); }
|
|
27
|
+
|
|
28
|
+
/* The disclosure sits over the control, so a click anywhere still reaches the
|
|
29
|
+
native select underneath and opens the platform picker. */
|
|
30
|
+
.ol8-select .ol8-field__control { position: relative; }
|
|
31
|
+
.ol8-select__input { flex: 1 1 auto; min-inline-size: 0; }
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oneli8 · Select — headless behavior.
|
|
3
|
+
*
|
|
4
|
+
* Figma (583:338 Trigger, 875:1778 Select): "Geometry and appearance reuse
|
|
5
|
+
* approved Text Field tokens; focus remains inward."
|
|
6
|
+
*
|
|
7
|
+
* The contract is native first and says so plainly: "On the web, ordinary
|
|
8
|
+
* Select uses a labelled native select whenever its option and appearance
|
|
9
|
+
* requirements fit native behavior", and "A native control is not replaced
|
|
10
|
+
* merely to force pixel identity."
|
|
11
|
+
*
|
|
12
|
+
* So this renders a real <select>. The user agent owns the keyboard, the type
|
|
13
|
+
* ahead, the picker and the announcements, which is exactly what the contract
|
|
14
|
+
* asks for: "Oneli8 does not intercept native keys to imitate a different
|
|
15
|
+
* platform." A custom listbox is available, but only when the caller says
|
|
16
|
+
* native="custom-allowed" and therefore accepts the whole contract that comes
|
|
17
|
+
* with it.
|
|
18
|
+
*/
|
|
19
|
+
import { renderIcon } from '../../atoms/icon/index.js';
|
|
20
|
+
import { renderFormMessage } from '../form-message/index.js';
|
|
21
|
+
|
|
22
|
+
export const OL8_SELECT_SIZES = ['compact', 'standard', 'comfortable', 'large'];
|
|
23
|
+
export const OL8_SELECT_APPEARANCES = ['outline', 'filled'];
|
|
24
|
+
export const OL8_SELECT_MATERIALS = ['regular', 'gem'];
|
|
25
|
+
export const OL8_SELECT_NATIVE_POLICIES = ['preferred', 'required', 'custom-allowed'];
|
|
26
|
+
|
|
27
|
+
let sequence = 0;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {{
|
|
31
|
+
* label:string, options:Array<{value:string,label:string,disabled?:boolean,group?:string}>,
|
|
32
|
+
* name?:string, id?:string, value?:string, size?:string, appearance?:string, material?:string,
|
|
33
|
+
* showLabel?:boolean, required?:boolean, disabled?:boolean, invalid?:boolean,
|
|
34
|
+
* placeholderOption?:string, leadingIcon?:string,
|
|
35
|
+
* instruction?:string, message?:string, messageTone?:string,
|
|
36
|
+
* native?:string, className?:string,
|
|
37
|
+
* }} options
|
|
38
|
+
*/
|
|
39
|
+
export function renderSelect(options) {
|
|
40
|
+
const {
|
|
41
|
+
label, options: items = [], name, id = `ol8-select-${++sequence}`, value,
|
|
42
|
+
size = 'comfortable', appearance = 'outline', material = 'regular',
|
|
43
|
+
showLabel = true, required = false, disabled = false, invalid = false,
|
|
44
|
+
placeholderOption, leadingIcon,
|
|
45
|
+
instruction, message, messageTone = 'critical',
|
|
46
|
+
native = 'preferred', className,
|
|
47
|
+
} = options ?? {};
|
|
48
|
+
|
|
49
|
+
if (typeof label !== 'string' || label === '') {
|
|
50
|
+
throw new Error('[ol8] Select requires a label. A placeholder is never the label.');
|
|
51
|
+
}
|
|
52
|
+
if (!Array.isArray(items) || items.length === 0) throw new Error('[ol8] Select needs at least one option.');
|
|
53
|
+
if (!OL8_SELECT_SIZES.includes(size)) throw new Error(`[ol8] unknown select size "${size}"`);
|
|
54
|
+
if (!OL8_SELECT_APPEARANCES.includes(appearance)) throw new Error(`[ol8] unknown select appearance "${appearance}"`);
|
|
55
|
+
if (!OL8_SELECT_MATERIALS.includes(material)) throw new Error(`[ol8] unknown select material "${material}"`);
|
|
56
|
+
if (!OL8_SELECT_NATIVE_POLICIES.includes(native)) throw new Error(`[ol8] unknown native policy "${native}"`);
|
|
57
|
+
if (native === 'custom-allowed') {
|
|
58
|
+
throw new Error('[ol8] a custom select is justified only when the product needs behaviour native Select cannot provide, and only with the complete focus, keyboard, announcement, form, mobile and forced colour contract implemented. Use Combobox, which implements it.');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const instructionId = instruction ? `${id}-instruction` : null;
|
|
62
|
+
const messageId = message ? `${id}-message` : null;
|
|
63
|
+
const describedBy = [instructionId, messageId].filter(Boolean).join(' ');
|
|
64
|
+
|
|
65
|
+
// Grouped options keep their source order, the way the popup groups do.
|
|
66
|
+
const groups = [];
|
|
67
|
+
for (const item of items) {
|
|
68
|
+
const group = item.group ?? null;
|
|
69
|
+
const last = groups[groups.length - 1];
|
|
70
|
+
if (last && last.name === group) last.items.push(item);
|
|
71
|
+
else groups.push({ name: group, items: [item] });
|
|
72
|
+
}
|
|
73
|
+
const optionMarkup = groups.map(group => {
|
|
74
|
+
const inner = group.items.map(item =>
|
|
75
|
+
`<option value="${escapeAttr(item.value)}"${item.value === value ? ' selected' : ''}` +
|
|
76
|
+
`${item.disabled ? ' disabled' : ''}>${escapeText(item.label)}</option>`).join('');
|
|
77
|
+
return group.name
|
|
78
|
+
? `<optgroup label="${escapeAttr(group.name)}">${inner}</optgroup>`
|
|
79
|
+
: inner;
|
|
80
|
+
}).join('');
|
|
81
|
+
|
|
82
|
+
// "placeholderOption only when an unselected value is valid or required
|
|
83
|
+
// validation needs an explicit prompt."
|
|
84
|
+
const placeholder = placeholderOption
|
|
85
|
+
? `<option value=""${value ? '' : ' selected'}${required ? ' disabled' : ''}>${escapeText(placeholderOption)}</option>`
|
|
86
|
+
: '';
|
|
87
|
+
|
|
88
|
+
const attrs = [
|
|
89
|
+
`class="ol8-field ol8-select${className ? ` ${className}` : ''}"`,
|
|
90
|
+
`data-ol8-size="${size}"`,
|
|
91
|
+
`data-ol8-appearance="${appearance}"`,
|
|
92
|
+
material === 'gem' ? 'data-ol8-material="gem"' : '',
|
|
93
|
+
disabled ? 'data-ol8-disabled="true"' : '',
|
|
94
|
+
invalid ? 'data-ol8-invalid="true"' : '',
|
|
95
|
+
].filter(Boolean).join(' ');
|
|
96
|
+
|
|
97
|
+
return `<div ${attrs}>` +
|
|
98
|
+
(showLabel
|
|
99
|
+
? `<div class="ol8-field__label-row">` +
|
|
100
|
+
`<label class="ol8-field__label" for="${escapeAttr(id)}">${escapeText(label)}</label>` +
|
|
101
|
+
(required ? `<span class="ol8-field__requirement" aria-hidden="true">*</span>` : '') +
|
|
102
|
+
`</div>`
|
|
103
|
+
: '') +
|
|
104
|
+
`<div class="ol8-field__control-stack"><div class="ol8-field__control">` +
|
|
105
|
+
(leadingIcon ? renderIcon(leadingIcon, { size: 18, className: 'ol8-field__leading-icon' }) : '') +
|
|
106
|
+
`<select class="ol8-field__input ol8-select__input" id="${escapeAttr(id)}"` +
|
|
107
|
+
(name ? ` name="${escapeAttr(name)}"` : '') +
|
|
108
|
+
(required ? ' required' : '') +
|
|
109
|
+
(disabled ? ' disabled' : '') +
|
|
110
|
+
(invalid ? ' aria-invalid="true"' : '') +
|
|
111
|
+
(invalid && messageId ? ` aria-errormessage="${messageId}"` : '') +
|
|
112
|
+
(describedBy ? ` aria-describedby="${escapeAttr(describedBy)}"` : '') +
|
|
113
|
+
(showLabel ? '' : ` aria-label="${escapeAttr(label)}"`) +
|
|
114
|
+
`>${placeholder}${optionMarkup}</select>` +
|
|
115
|
+
// The disclosure is decoration: the native control already tells a screen
|
|
116
|
+
// reader that it opens a list.
|
|
117
|
+
renderIcon('chevron-down', { size: 18, className: 'ol8-select__disclosure' }) +
|
|
118
|
+
`</div></div>` +
|
|
119
|
+
(instruction ? `<div class="ol8-field__instruction" id="${instructionId}">${escapeText(instruction)}</div>` : '') +
|
|
120
|
+
(message
|
|
121
|
+
? `<div class="ol8-field__supporting">${renderFormMessage(message, { tone: messageTone, id: messageId })}</div>`
|
|
122
|
+
: '') +
|
|
123
|
+
`</div>`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function escapeAttr(v) { return String(v).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<'); }
|
|
127
|
+
function escapeText(v) { return String(v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* virtual cursor with aria-activedescendant while real focus stays put.
|
|
15
15
|
* - "active" (the cursor) is distinct from "selected" (the data) and from
|
|
16
16
|
* :hover (the pointer), so it is its own attribute.
|
|
17
|
-
* - options cannot use the `disabled` attribute — it is not valid on
|
|
18
|
-
* so unavailability is aria-disabled.
|
|
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
19
|
*/
|
|
20
20
|
import { renderIcon, renderIconSvg, isOl8IconName } from '../../atoms/icon/icon.js';
|
|
21
21
|
|
|
@@ -58,14 +58,14 @@ export function renderSelectionOption(label, options = {}) {
|
|
|
58
58
|
const lead = leadingIcon ? renderIcon(leadingIcon, { size: 18 }) : '';
|
|
59
59
|
const check = `<span class="ol8-option__check" aria-hidden="true">${selected ? renderIconSvg('check') : ''}</span>`;
|
|
60
60
|
|
|
61
|
-
return `<
|
|
61
|
+
return `<div ${attrs}><span class="ol8-option__surface">` +
|
|
62
62
|
lead +
|
|
63
63
|
`<span class="ol8-option__text">` +
|
|
64
64
|
`<span class="ol8-option__label" id="${labelId}">${escapeText(label)}</span>` +
|
|
65
65
|
(description ? `<span class="ol8-option__description" id="${descId}">${escapeText(description)}</span>` : '') +
|
|
66
66
|
`</span>` +
|
|
67
67
|
check +
|
|
68
|
-
`</span></
|
|
68
|
+
`</span></div>`;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/** Flips one option's selected state and repaints its trailing check. */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './text-field.js';
|