@openchamber/sdk 1.23.2-preview.1
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/API.md +529 -0
- package/DOCUMENTATION.md +81 -0
- package/GUEST_SERVICES.md +166 -0
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/api-version.d.ts +6 -0
- package/dist/api-version.js +5 -0
- package/dist/contract.d.ts +471 -0
- package/dist/contract.js +256 -0
- package/dist/host-version.d.ts +16 -0
- package/dist/host-version.js +50 -0
- package/dist/host.d.ts +101 -0
- package/dist/host.js +606 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +7 -0
- package/dist/manifest.d.ts +298 -0
- package/dist/manifest.js +224 -0
- package/dist/parse.d.ts +279 -0
- package/dist/parse.js +379 -0
- package/dist/protocol.d.ts +1092 -0
- package/dist/protocol.js +450 -0
- package/dist/schemas.d.ts +4 -0
- package/dist/schemas.js +6 -0
- package/dist/ui/badge.d.ts +10 -0
- package/dist/ui/badge.js +26 -0
- package/dist/ui/banner.d.ts +13 -0
- package/dist/ui/banner.js +48 -0
- package/dist/ui/button.d.ts +14 -0
- package/dist/ui/button.js +49 -0
- package/dist/ui/checkbox.d.ts +12 -0
- package/dist/ui/checkbox.js +45 -0
- package/dist/ui/dom.d.ts +15 -0
- package/dist/ui/dom.js +60 -0
- package/dist/ui/empty.d.ts +11 -0
- package/dist/ui/empty.js +44 -0
- package/dist/ui/field.d.ts +18 -0
- package/dist/ui/field.js +49 -0
- package/dist/ui/icons.d.ts +10 -0
- package/dist/ui/icons.js +23 -0
- package/dist/ui/index.d.ts +35 -0
- package/dist/ui/index.js +17 -0
- package/dist/ui/list.d.ts +26 -0
- package/dist/ui/list.js +90 -0
- package/dist/ui/menu.d.ts +20 -0
- package/dist/ui/menu.js +111 -0
- package/dist/ui/navigation.d.ts +18 -0
- package/dist/ui/navigation.js +38 -0
- package/dist/ui/option.d.ts +16 -0
- package/dist/ui/option.js +35 -0
- package/dist/ui/popup.d.ts +5 -0
- package/dist/ui/popup.js +43 -0
- package/dist/ui/progress.d.ts +11 -0
- package/dist/ui/progress.js +44 -0
- package/dist/ui/search.d.ts +11 -0
- package/dist/ui/search.js +62 -0
- package/dist/ui/select.d.ts +22 -0
- package/dist/ui/select.js +164 -0
- package/dist/ui/separator.d.ts +6 -0
- package/dist/ui/separator.js +26 -0
- package/dist/ui/spinner.d.ts +8 -0
- package/dist/ui/spinner.js +29 -0
- package/dist/ui/style.d.ts +1 -0
- package/dist/ui/style.js +190 -0
- package/dist/ui/tabs.d.ts +15 -0
- package/dist/ui/tabs.js +61 -0
- package/dist/ui/text.d.ts +23 -0
- package/dist/ui/text.js +89 -0
- package/dist/ui/theme.d.ts +22 -0
- package/dist/ui/theme.js +69 -0
- package/dist/workspace-schemas.d.ts +136 -0
- package/dist/workspace-schemas.js +44 -0
- package/dist/workspace.d.ts +109 -0
- package/dist/workspace.js +4 -0
- package/package.json +55 -0
- package/scripts/bundle-guest.ts +44 -0
package/dist/ui/popup.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { onOutsideClick } from "./dom.js";
|
|
2
|
+
/** Places a fixed popup under `trigger`; flips above when the room below is too small. */
|
|
3
|
+
const placePopup = (popup, trigger) => {
|
|
4
|
+
const rect = trigger.getBoundingClientRect();
|
|
5
|
+
popup.style.minWidth = `${Math.round(rect.width)}px`;
|
|
6
|
+
popup.style.left = `${Math.round(rect.left)}px`;
|
|
7
|
+
popup.style.top = `${Math.round(rect.bottom + 4)}px`;
|
|
8
|
+
const height = popup.offsetHeight;
|
|
9
|
+
const roomBelow = window.innerHeight - rect.bottom - 8;
|
|
10
|
+
if (height > roomBelow && rect.top - 8 > roomBelow) {
|
|
11
|
+
popup.style.top = `${Math.max(8, Math.round(rect.top - 4 - height))}px`;
|
|
12
|
+
}
|
|
13
|
+
const overflow = rect.left + popup.offsetWidth - window.innerWidth + 8;
|
|
14
|
+
if (overflow > 0) {
|
|
15
|
+
popup.style.left = `${Math.max(8, Math.round(rect.left - overflow))}px`;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Attaches `popup` next to `trigger` and wires the ways it closes: outside press,
|
|
20
|
+
* viewport resize, or scroll. Returns the disposer; the caller decides what `close` does.
|
|
21
|
+
*/
|
|
22
|
+
export const openPopup = (host, trigger, popup, close) => {
|
|
23
|
+
host.append(popup);
|
|
24
|
+
placePopup(popup, trigger);
|
|
25
|
+
const stopOutside = onOutsideClick(host, close);
|
|
26
|
+
const onResize = () => {
|
|
27
|
+
close();
|
|
28
|
+
};
|
|
29
|
+
const onScroll = (event) => {
|
|
30
|
+
if (event.target instanceof Node && popup.contains(event.target)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
close();
|
|
34
|
+
};
|
|
35
|
+
window.addEventListener('resize', onResize);
|
|
36
|
+
window.addEventListener('scroll', onScroll, true);
|
|
37
|
+
return () => {
|
|
38
|
+
stopOutside();
|
|
39
|
+
window.removeEventListener('resize', onResize);
|
|
40
|
+
window.removeEventListener('scroll', onScroll, true);
|
|
41
|
+
popup.remove();
|
|
42
|
+
};
|
|
43
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Tone } from './badge.ts';
|
|
2
|
+
import { type Handle } from './dom.ts';
|
|
3
|
+
export type ProgressProps = {
|
|
4
|
+
/** 0 to 100. Values outside that range are clamped. */
|
|
5
|
+
value: number;
|
|
6
|
+
tone?: Tone;
|
|
7
|
+
label?: string;
|
|
8
|
+
};
|
|
9
|
+
export type ProgressHandle = Handle<ProgressProps>;
|
|
10
|
+
export declare const clampProgress: (value: number) => number;
|
|
11
|
+
export declare const mountProgress: (root: Element, initial: ProgressProps) => ProgressHandle;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { applyTone } from "./badge.js";
|
|
2
|
+
import { el, ensureStyle, setText } from "./dom.js";
|
|
3
|
+
import { UI_CSS } from "./style.js";
|
|
4
|
+
export const clampProgress = (value) => (Number.isFinite(value) ? Math.min(100, Math.max(0, Math.round(value))) : 0);
|
|
5
|
+
export const mountProgress = (root, initial) => {
|
|
6
|
+
ensureStyle(UI_CSS);
|
|
7
|
+
let props = initial;
|
|
8
|
+
const node = el('div', 'oc-sdk oc-sdk-progress');
|
|
9
|
+
const caption = el('div', 'oc-sdk-progress-label');
|
|
10
|
+
const label = el('span');
|
|
11
|
+
const percent = el('span');
|
|
12
|
+
caption.append(label, percent);
|
|
13
|
+
const track = el('div', 'oc-sdk-progress-track');
|
|
14
|
+
track.setAttribute('role', 'progressbar');
|
|
15
|
+
track.setAttribute('aria-valuemin', '0');
|
|
16
|
+
track.setAttribute('aria-valuemax', '100');
|
|
17
|
+
const fill = el('div', 'oc-sdk-progress-fill');
|
|
18
|
+
track.append(fill);
|
|
19
|
+
node.append(caption, track);
|
|
20
|
+
root.append(node);
|
|
21
|
+
const paint = () => {
|
|
22
|
+
const value = clampProgress(props.value);
|
|
23
|
+
applyTone(fill, props.tone);
|
|
24
|
+
fill.style.transform = `scaleX(${value / 100})`;
|
|
25
|
+
track.setAttribute('aria-valuenow', String(value));
|
|
26
|
+
if (props.label)
|
|
27
|
+
track.setAttribute('aria-label', props.label);
|
|
28
|
+
else
|
|
29
|
+
track.removeAttribute('aria-label');
|
|
30
|
+
setText(label, props.label);
|
|
31
|
+
setText(percent, `${value}%`);
|
|
32
|
+
caption.hidden = !props.label;
|
|
33
|
+
};
|
|
34
|
+
paint();
|
|
35
|
+
return {
|
|
36
|
+
update: (next) => {
|
|
37
|
+
props = { ...props, ...next };
|
|
38
|
+
paint();
|
|
39
|
+
},
|
|
40
|
+
dispose: () => {
|
|
41
|
+
node.remove();
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Handle } from './dom.ts';
|
|
2
|
+
export type SearchFieldProps = {
|
|
3
|
+
value: string;
|
|
4
|
+
onChange: (value: string) => void;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
/** Accessible name. Falls back to the placeholder. */
|
|
7
|
+
label?: string;
|
|
8
|
+
autofocus?: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type SearchFieldHandle = Handle<SearchFieldProps>;
|
|
11
|
+
export declare const mountSearchField: (root: Element, initial: SearchFieldProps) => SearchFieldHandle;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { button, el, ensureStyle, setAttr } from "./dom.js";
|
|
2
|
+
import { icon } from "./icons.js";
|
|
3
|
+
import { UI_CSS } from "./style.js";
|
|
4
|
+
export const mountSearchField = (root, initial) => {
|
|
5
|
+
ensureStyle(UI_CSS);
|
|
6
|
+
let props = initial;
|
|
7
|
+
const wrap = el('div', 'oc-sdk oc-sdk-search');
|
|
8
|
+
const input = el('input', 'oc-sdk-input');
|
|
9
|
+
input.type = 'text';
|
|
10
|
+
input.spellcheck = false;
|
|
11
|
+
input.autocomplete = 'off';
|
|
12
|
+
input.setAttribute('role', 'searchbox');
|
|
13
|
+
const clear = button('oc-sdk-search-clear');
|
|
14
|
+
clear.append(icon('close', 14));
|
|
15
|
+
clear.tabIndex = -1;
|
|
16
|
+
wrap.append(icon('search', 16, 'oc-sdk-search-icon'), input, clear);
|
|
17
|
+
root.append(wrap);
|
|
18
|
+
const paint = () => {
|
|
19
|
+
const placeholder = props.placeholder ?? 'Search';
|
|
20
|
+
setAttr(input, 'placeholder', placeholder);
|
|
21
|
+
input.setAttribute('aria-label', props.label ?? placeholder);
|
|
22
|
+
clear.setAttribute('aria-label', 'Clear search');
|
|
23
|
+
if (input.value !== props.value) {
|
|
24
|
+
input.value = props.value;
|
|
25
|
+
}
|
|
26
|
+
wrap.dataset.active = props.value.trim() === '' ? 'false' : 'true';
|
|
27
|
+
};
|
|
28
|
+
const clearValue = () => {
|
|
29
|
+
if (props.value !== '') {
|
|
30
|
+
props.onChange('');
|
|
31
|
+
}
|
|
32
|
+
input.focus();
|
|
33
|
+
};
|
|
34
|
+
const onInput = () => {
|
|
35
|
+
props.onChange(input.value);
|
|
36
|
+
};
|
|
37
|
+
const onKeyDown = (event) => {
|
|
38
|
+
if (event.key === 'Escape' && input.value !== '') {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
clearValue();
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
input.addEventListener('input', onInput);
|
|
44
|
+
input.addEventListener('keydown', onKeyDown);
|
|
45
|
+
clear.addEventListener('click', clearValue);
|
|
46
|
+
paint();
|
|
47
|
+
if (props.autofocus) {
|
|
48
|
+
input.focus();
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
update: (next) => {
|
|
52
|
+
props = { ...props, ...next };
|
|
53
|
+
paint();
|
|
54
|
+
},
|
|
55
|
+
dispose: () => {
|
|
56
|
+
input.removeEventListener('input', onInput);
|
|
57
|
+
input.removeEventListener('keydown', onKeyDown);
|
|
58
|
+
clear.removeEventListener('click', clearValue);
|
|
59
|
+
wrap.remove();
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Handle } from './dom.ts';
|
|
2
|
+
export type SelectOption = {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** Small muted text at the right of the option. */
|
|
6
|
+
hint?: string;
|
|
7
|
+
};
|
|
8
|
+
export type SelectProps = {
|
|
9
|
+
label?: string;
|
|
10
|
+
value: string | null;
|
|
11
|
+
options: SelectOption[];
|
|
12
|
+
onChange: (id: string) => void;
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
/** Adds a search box at the top of the popup. */
|
|
15
|
+
searchable?: boolean;
|
|
16
|
+
searchPlaceholder?: string;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
};
|
|
19
|
+
export type SelectHandle = Handle<SelectProps>;
|
|
20
|
+
/** Case-insensitive substring match on label and id. Empty query keeps every option. */
|
|
21
|
+
export declare const filterSelectOptions: (options: readonly SelectOption[], query: string) => SelectOption[];
|
|
22
|
+
export declare const mountSelect: (root: Element, initial: SelectProps) => SelectHandle;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { button, clearNode, el, ensureStyle, setText } from "./dom.js";
|
|
2
|
+
import { icon } from "./icons.js";
|
|
3
|
+
import { moveListSelection, navigationKey } from "./navigation.js";
|
|
4
|
+
import { createOption, highlightOption } from "./option.js";
|
|
5
|
+
import { openPopup } from "./popup.js";
|
|
6
|
+
import { UI_CSS } from "./style.js";
|
|
7
|
+
/** Case-insensitive substring match on label and id. Empty query keeps every option. */
|
|
8
|
+
export const filterSelectOptions = (options, query) => {
|
|
9
|
+
const needle = query.trim().toLowerCase();
|
|
10
|
+
if (!needle) {
|
|
11
|
+
return [...options];
|
|
12
|
+
}
|
|
13
|
+
return options.filter((option) => (option.label.toLowerCase().includes(needle) || option.id.toLowerCase().includes(needle)));
|
|
14
|
+
};
|
|
15
|
+
let selectCount = 0;
|
|
16
|
+
export const mountSelect = (root, initial) => {
|
|
17
|
+
ensureStyle(UI_CSS);
|
|
18
|
+
let props = initial;
|
|
19
|
+
const uid = `oc-sdk-select-${selectCount += 1}`;
|
|
20
|
+
const wrap = el('div', 'oc-sdk oc-sdk-select');
|
|
21
|
+
const caption = el('span', 'oc-sdk-field-label');
|
|
22
|
+
caption.id = `${uid}-label`;
|
|
23
|
+
const trigger = button('oc-sdk-trigger');
|
|
24
|
+
trigger.setAttribute('aria-haspopup', 'listbox');
|
|
25
|
+
trigger.setAttribute('aria-labelledby', caption.id);
|
|
26
|
+
const value = el('span', 'oc-sdk-trigger-value');
|
|
27
|
+
trigger.append(value, icon('chevron', 14, 'oc-sdk-trigger-chevron'));
|
|
28
|
+
wrap.append(caption, trigger);
|
|
29
|
+
root.append(wrap);
|
|
30
|
+
let query = '';
|
|
31
|
+
let activeId = null;
|
|
32
|
+
let closePopup = null;
|
|
33
|
+
const popup = el('div', 'oc-sdk oc-sdk-popup');
|
|
34
|
+
const searchSlot = el('div', 'oc-sdk-popup-search');
|
|
35
|
+
const search = el('input', 'oc-sdk-input');
|
|
36
|
+
search.type = 'text';
|
|
37
|
+
search.autocomplete = 'off';
|
|
38
|
+
searchSlot.append(search);
|
|
39
|
+
const listbox = el('div');
|
|
40
|
+
listbox.setAttribute('role', 'listbox');
|
|
41
|
+
listbox.tabIndex = -1;
|
|
42
|
+
popup.append(listbox);
|
|
43
|
+
const visible = () => filterSelectOptions(props.options, props.searchable ? query : '');
|
|
44
|
+
const focusOwner = () => (props.searchable ? search : listbox);
|
|
45
|
+
const setActive = (id) => {
|
|
46
|
+
activeId = id;
|
|
47
|
+
highlightOption(listbox, focusOwner(), uid, id);
|
|
48
|
+
};
|
|
49
|
+
const paintOptions = () => {
|
|
50
|
+
clearNode(listbox);
|
|
51
|
+
const options = visible();
|
|
52
|
+
if (options.length === 0) {
|
|
53
|
+
const empty = el('div', 'oc-sdk-popup-empty');
|
|
54
|
+
empty.textContent = 'No matches';
|
|
55
|
+
listbox.append(empty);
|
|
56
|
+
}
|
|
57
|
+
for (const option of options) {
|
|
58
|
+
listbox.append(createOption(uid, 'option', { ...option, selected: option.id === props.value }, {
|
|
59
|
+
hover: () => setActive(option.id),
|
|
60
|
+
pick: () => pick(option.id),
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
setActive(options.some((option) => option.id === activeId) ? activeId : options[0]?.id ?? null);
|
|
64
|
+
};
|
|
65
|
+
const close = () => {
|
|
66
|
+
closePopup?.();
|
|
67
|
+
closePopup = null;
|
|
68
|
+
query = '';
|
|
69
|
+
search.value = '';
|
|
70
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
71
|
+
};
|
|
72
|
+
const open = () => {
|
|
73
|
+
if (closePopup || props.disabled) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
activeId = props.value;
|
|
77
|
+
search.placeholder = props.searchPlaceholder ?? 'Search';
|
|
78
|
+
if (props.searchable)
|
|
79
|
+
popup.prepend(searchSlot);
|
|
80
|
+
else
|
|
81
|
+
searchSlot.remove();
|
|
82
|
+
paintOptions();
|
|
83
|
+
closePopup = openPopup(wrap, trigger, popup, close);
|
|
84
|
+
trigger.setAttribute('aria-expanded', 'true');
|
|
85
|
+
focusOwner().focus();
|
|
86
|
+
};
|
|
87
|
+
const pick = (id) => {
|
|
88
|
+
close();
|
|
89
|
+
trigger.focus();
|
|
90
|
+
if (id !== props.value)
|
|
91
|
+
props.onChange(id);
|
|
92
|
+
};
|
|
93
|
+
const onTriggerClick = () => {
|
|
94
|
+
if (closePopup)
|
|
95
|
+
close();
|
|
96
|
+
else
|
|
97
|
+
open();
|
|
98
|
+
};
|
|
99
|
+
const onTriggerKey = (event) => {
|
|
100
|
+
if (!closePopup && navigationKey(event)) {
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
open();
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const onPopupKey = (event) => {
|
|
106
|
+
const step = navigationKey(event);
|
|
107
|
+
if (step) {
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
setActive(moveListSelection(visible(), activeId, step));
|
|
110
|
+
}
|
|
111
|
+
else if (event.key === 'Enter') {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
if (activeId)
|
|
114
|
+
pick(activeId);
|
|
115
|
+
}
|
|
116
|
+
else if (event.key === 'Escape') {
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
close();
|
|
119
|
+
trigger.focus();
|
|
120
|
+
}
|
|
121
|
+
else if (props.searchable && event.target !== search && event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
|
|
122
|
+
search.focus();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const onSearchInput = () => {
|
|
126
|
+
query = search.value;
|
|
127
|
+
paintOptions();
|
|
128
|
+
};
|
|
129
|
+
const onFocusOut = (event) => {
|
|
130
|
+
if (closePopup && !(event.relatedTarget instanceof Node && wrap.contains(event.relatedTarget)))
|
|
131
|
+
close();
|
|
132
|
+
};
|
|
133
|
+
const paint = () => {
|
|
134
|
+
setText(caption, props.label);
|
|
135
|
+
caption.hidden = !props.label;
|
|
136
|
+
const current = props.options.find((option) => option.id === props.value);
|
|
137
|
+
setText(value, current?.label ?? props.placeholder ?? 'Select');
|
|
138
|
+
value.dataset.empty = current ? 'false' : 'true';
|
|
139
|
+
trigger.disabled = Boolean(props.disabled);
|
|
140
|
+
if (closePopup)
|
|
141
|
+
paintOptions();
|
|
142
|
+
};
|
|
143
|
+
trigger.addEventListener('click', onTriggerClick);
|
|
144
|
+
trigger.addEventListener('keydown', onTriggerKey);
|
|
145
|
+
popup.addEventListener('keydown', onPopupKey);
|
|
146
|
+
search.addEventListener('input', onSearchInput);
|
|
147
|
+
wrap.addEventListener('focusout', onFocusOut);
|
|
148
|
+
paint();
|
|
149
|
+
return {
|
|
150
|
+
update: (next) => {
|
|
151
|
+
props = { ...props, ...next };
|
|
152
|
+
paint();
|
|
153
|
+
},
|
|
154
|
+
dispose: () => {
|
|
155
|
+
close();
|
|
156
|
+
trigger.removeEventListener('click', onTriggerClick);
|
|
157
|
+
trigger.removeEventListener('keydown', onTriggerKey);
|
|
158
|
+
popup.removeEventListener('keydown', onPopupKey);
|
|
159
|
+
search.removeEventListener('input', onSearchInput);
|
|
160
|
+
wrap.removeEventListener('focusout', onFocusOut);
|
|
161
|
+
wrap.remove();
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { el, ensureStyle, setText } from "./dom.js";
|
|
2
|
+
import { UI_CSS } from "./style.js";
|
|
3
|
+
export const mountSeparator = (root, initial = {}) => {
|
|
4
|
+
ensureStyle(UI_CSS);
|
|
5
|
+
let props = initial;
|
|
6
|
+
const node = el('div', 'oc-sdk oc-sdk-separator');
|
|
7
|
+
node.setAttribute('role', 'separator');
|
|
8
|
+
const label = el('span');
|
|
9
|
+
node.append(label);
|
|
10
|
+
root.append(node);
|
|
11
|
+
const paint = () => {
|
|
12
|
+
setText(label, props.label);
|
|
13
|
+
label.hidden = !props.label;
|
|
14
|
+
node.dataset.labeled = props.label ? 'true' : 'false';
|
|
15
|
+
};
|
|
16
|
+
paint();
|
|
17
|
+
return {
|
|
18
|
+
update: (next) => {
|
|
19
|
+
props = { ...props, ...next };
|
|
20
|
+
paint();
|
|
21
|
+
},
|
|
22
|
+
dispose: () => {
|
|
23
|
+
node.remove();
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Handle } from './dom.ts';
|
|
2
|
+
export type SpinnerProps = {
|
|
3
|
+
size?: 'sm' | 'default';
|
|
4
|
+
/** Text next to the ring. Also the accessible name. */
|
|
5
|
+
label?: string;
|
|
6
|
+
};
|
|
7
|
+
export type SpinnerHandle = Handle<SpinnerProps>;
|
|
8
|
+
export declare const mountSpinner: (root: Element, initial?: SpinnerProps) => SpinnerHandle;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { el, ensureStyle, setText } from "./dom.js";
|
|
2
|
+
import { UI_CSS } from "./style.js";
|
|
3
|
+
export const mountSpinner = (root, initial = {}) => {
|
|
4
|
+
ensureStyle(UI_CSS);
|
|
5
|
+
let props = initial;
|
|
6
|
+
const node = el('span', 'oc-sdk oc-sdk-spinner');
|
|
7
|
+
node.setAttribute('role', 'status');
|
|
8
|
+
const ring = el('span', 'oc-sdk-spinner-ring');
|
|
9
|
+
ring.setAttribute('aria-hidden', 'true');
|
|
10
|
+
const label = el('span');
|
|
11
|
+
node.append(ring, label);
|
|
12
|
+
root.append(node);
|
|
13
|
+
const paint = () => {
|
|
14
|
+
node.dataset.size = props.size ?? 'default';
|
|
15
|
+
setText(label, props.label);
|
|
16
|
+
label.hidden = !props.label;
|
|
17
|
+
node.setAttribute('aria-label', props.label ?? 'Loading');
|
|
18
|
+
};
|
|
19
|
+
paint();
|
|
20
|
+
return {
|
|
21
|
+
update: (next) => {
|
|
22
|
+
props = { ...props, ...next };
|
|
23
|
+
paint();
|
|
24
|
+
},
|
|
25
|
+
dispose: () => {
|
|
26
|
+
node.remove();
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const UI_CSS: string;
|
package/dist/ui/style.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/** Host CSS variable → `--oc-*` alias written by `theme.ts`. */
|
|
2
|
+
const OC_ALIAS = {
|
|
3
|
+
'surface-background': 'bg',
|
|
4
|
+
'surface-elevated': 'elevated',
|
|
5
|
+
'surface-elevated-foreground': 'elevated-fg',
|
|
6
|
+
'surface-foreground': 'fg',
|
|
7
|
+
'surface-muted-foreground': 'muted',
|
|
8
|
+
'surface-subtle': 'subtle',
|
|
9
|
+
'interactive-border': 'border',
|
|
10
|
+
'interactive-hover': 'hover',
|
|
11
|
+
'interactive-active': 'active',
|
|
12
|
+
'interactive-selection': 'selection',
|
|
13
|
+
'interactive-selection-foreground': 'selection-fg',
|
|
14
|
+
'interactive-focus-ring': 'focus',
|
|
15
|
+
'primary': 'primary',
|
|
16
|
+
'primary-foreground': 'primary-fg',
|
|
17
|
+
'status-success': 'success',
|
|
18
|
+
'status-warning': 'warning',
|
|
19
|
+
'status-error': 'error',
|
|
20
|
+
'status-info': 'info',
|
|
21
|
+
'font-sans': 'font',
|
|
22
|
+
'font-mono': 'mono',
|
|
23
|
+
'radius': 'radius',
|
|
24
|
+
};
|
|
25
|
+
const v = (name, fallback) => (`var(--${name}, var(--oc-${OC_ALIAS[name]}, ${fallback}))`);
|
|
26
|
+
const bg = v('surface-background', 'transparent');
|
|
27
|
+
const elevated = v('surface-elevated', 'transparent');
|
|
28
|
+
const elevatedFg = v('surface-elevated-foreground', 'inherit');
|
|
29
|
+
const fg = v('surface-foreground', 'inherit');
|
|
30
|
+
const muted = v('surface-muted-foreground', 'gray');
|
|
31
|
+
const subtle = v('surface-subtle', 'transparent');
|
|
32
|
+
const border = v('interactive-border', 'currentColor');
|
|
33
|
+
const hover = v('interactive-hover', 'transparent');
|
|
34
|
+
const active = v('interactive-active', 'transparent');
|
|
35
|
+
const selection = v('interactive-selection', 'transparent');
|
|
36
|
+
const selectionFg = v('interactive-selection-foreground', 'inherit');
|
|
37
|
+
const focus = v('interactive-focus-ring', 'currentColor');
|
|
38
|
+
const primary = v('primary', 'currentColor');
|
|
39
|
+
const font = v('font-sans', 'inherit');
|
|
40
|
+
const mono = v('font-mono', 'monospace');
|
|
41
|
+
const radius = v('radius', '9px');
|
|
42
|
+
const mix = (color, pct, base = 'transparent') => (`color-mix(in srgb, ${color} ${pct}%, ${base})`);
|
|
43
|
+
const focusRing = `box-shadow: 0 0 0 2px ${focus};`;
|
|
44
|
+
const tone = (name) => {
|
|
45
|
+
const color = v(`status-${name}`, 'currentColor');
|
|
46
|
+
return `
|
|
47
|
+
.oc-sdk[data-tone="${name}"], .oc-sdk [data-tone="${name}"] { --oc-sdk-tone: ${color}; }`;
|
|
48
|
+
};
|
|
49
|
+
export const UI_CSS = `
|
|
50
|
+
.oc-sdk { box-sizing: border-box; color: ${fg}; font-family: ${font}; font-size: 0.875rem; line-height: 1.45; }
|
|
51
|
+
.oc-sdk *, .oc-sdk *::before, .oc-sdk *::after { box-sizing: border-box; }
|
|
52
|
+
/* :where() keeps the reset at zero specificity so every primitive class below overrides it. */
|
|
53
|
+
:where(.oc-sdk) :where(button, input, textarea), :where(button.oc-sdk, input.oc-sdk, textarea.oc-sdk) { font: inherit; color: inherit; margin: 0; }
|
|
54
|
+
:where(.oc-sdk) :where(button), :where(button.oc-sdk) { cursor: pointer; background: none; border: 0; padding: 0; }
|
|
55
|
+
.oc-sdk button:disabled, button.oc-sdk:disabled, .oc-sdk[aria-disabled="true"], .oc-sdk [aria-disabled="true"] { opacity: .5; pointer-events: none; }
|
|
56
|
+
.oc-sdk :focus-visible { outline: none; ${focusRing} }
|
|
57
|
+
.oc-sdk-mono { font-family: ${mono}; }
|
|
58
|
+
.oc-sdk-muted { color: ${muted}; }
|
|
59
|
+
${tone('success')}${tone('warning')}${tone('error')}${tone('info')}
|
|
60
|
+
.oc-sdk[data-tone="primary"], .oc-sdk [data-tone="primary"] { --oc-sdk-tone: ${primary}; }
|
|
61
|
+
|
|
62
|
+
.oc-sdk-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 36px; padding: 0 14px; border: 1px solid transparent; border-radius: ${radius}; font-size: 0.875rem; font-weight: 500; line-height: 1; white-space: nowrap; transition: background 150ms ease-out, color 150ms ease-out; }
|
|
63
|
+
.oc-sdk-btn[data-size="sm"] { height: 32px; padding: 0 10px; font-size: 0.8125rem; }
|
|
64
|
+
.oc-sdk-btn[data-size="xs"] { height: 24px; padding: 0 8px; font-size: 0.75rem; border-radius: 6px; }
|
|
65
|
+
.oc-sdk-btn[data-variant="default"] { color: ${primary}; background: ${mix(primary, 10, bg)}; border-color: ${mix(primary, 12)}; }
|
|
66
|
+
.oc-sdk-btn[data-variant="default"]:hover { background: ${mix(primary, 16, bg)}; }
|
|
67
|
+
.oc-sdk-btn[data-variant="default"]:active { background: ${mix(primary, 22, bg)}; }
|
|
68
|
+
.oc-sdk-btn[data-variant="secondary"] { background: ${hover}; }
|
|
69
|
+
.oc-sdk-btn[data-variant="secondary"]:hover { background: ${active}; }
|
|
70
|
+
.oc-sdk-btn[data-variant="outline"] { background: ${elevated}; border-color: ${border}; }
|
|
71
|
+
.oc-sdk-btn[data-variant="outline"]:hover { background: ${hover}; }
|
|
72
|
+
.oc-sdk-btn[data-variant="ghost"] { background: transparent; }
|
|
73
|
+
.oc-sdk-btn[data-variant="ghost"]:hover { background: ${hover}; }
|
|
74
|
+
.oc-sdk-btn[data-variant="ghost"]:active { background: ${active}; }
|
|
75
|
+
.oc-sdk-btn[data-variant="destructive"] { --oc-sdk-tone: ${v('status-error', 'red')}; color: var(--oc-sdk-tone); background: ${mix('var(--oc-sdk-tone)', 7, bg)}; border-color: ${mix('var(--oc-sdk-tone)', 12)}; }
|
|
76
|
+
.oc-sdk-btn[data-variant="destructive"]:hover { background: ${mix('var(--oc-sdk-tone)', 9, bg)}; }
|
|
77
|
+
.oc-sdk-btn[data-variant="destructive"]:active { background: ${mix('var(--oc-sdk-tone)', 11, bg)}; }
|
|
78
|
+
.oc-sdk-btn[data-loading="true"] { opacity: .5; pointer-events: none; }
|
|
79
|
+
.oc-sdk-btn > .oc-sdk-spinner-ring { width: 14px; height: 14px; }
|
|
80
|
+
|
|
81
|
+
.oc-sdk-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
|
82
|
+
.oc-sdk-field-label { font-size: 0.8125rem; font-weight: 500; }
|
|
83
|
+
.oc-sdk-field-note { font-size: 0.75rem; color: ${muted}; }
|
|
84
|
+
.oc-sdk-field[data-invalid="true"] .oc-sdk-field-note { color: ${v('status-error', 'red')}; }
|
|
85
|
+
.oc-sdk-input { display: block; width: 100%; min-width: 0; height: 36px; padding: 0 12px; border: 0; border-radius: ${radius}; background: ${elevated}; color: ${fg}; font-size: 0.875rem; line-height: 1.45; appearance: none; box-shadow: inset 0 0 0 1px ${mix(border, 60)}; transition: background 150ms ease-out, box-shadow 150ms ease-out; }
|
|
86
|
+
textarea.oc-sdk-input { height: auto; padding: 8px 12px; resize: vertical; }
|
|
87
|
+
.oc-sdk-input::placeholder { color: ${muted}; }
|
|
88
|
+
.oc-sdk-input:hover:not(:focus) { background: ${subtle}; }
|
|
89
|
+
.oc-sdk-input:focus, .oc-sdk-input:focus-visible { box-shadow: inset 0 0 0 2px ${focus}; }
|
|
90
|
+
.oc-sdk-field[data-invalid="true"] .oc-sdk-input { box-shadow: inset 0 0 0 1px ${v('status-error', 'red')}; }
|
|
91
|
+
.oc-sdk-field[data-invalid="true"] .oc-sdk-input:focus { box-shadow: inset 0 0 0 2px ${v('status-error', 'red')}; }
|
|
92
|
+
.oc-sdk-input[data-mono="true"] { font-family: ${mono}; }
|
|
93
|
+
|
|
94
|
+
.oc-sdk-search { position: relative; min-width: 0; }
|
|
95
|
+
.oc-sdk-search .oc-sdk-input { padding-left: 34px; padding-right: 34px; }
|
|
96
|
+
.oc-sdk-search-icon { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); color: ${muted}; pointer-events: none; }
|
|
97
|
+
.oc-sdk-search[data-active="true"] .oc-sdk-search-icon { color: ${primary}; }
|
|
98
|
+
.oc-sdk-search-clear { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); display: none; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 6px; color: ${muted}; }
|
|
99
|
+
.oc-sdk-search[data-active="true"] .oc-sdk-search-clear { display: inline-flex; }
|
|
100
|
+
.oc-sdk-search-clear:hover { background: ${hover}; color: ${fg}; }
|
|
101
|
+
|
|
102
|
+
.oc-sdk-select { position: relative; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
|
103
|
+
.oc-sdk-trigger { display: inline-flex; align-items: center; gap: 6px; width: 100%; min-width: 0; height: 32px; padding: 0 8px 0 10px; border: 1px solid ${border}; border-radius: 6px; background: transparent; font-size: 0.8125rem; text-align: left; transition: background 150ms ease-out; }
|
|
104
|
+
.oc-sdk-trigger:hover { background: ${hover}; }
|
|
105
|
+
.oc-sdk-trigger[aria-expanded="true"] { background: ${active}; }
|
|
106
|
+
.oc-sdk-trigger-value { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
107
|
+
.oc-sdk-trigger-value[data-empty="true"] { color: ${muted}; }
|
|
108
|
+
.oc-sdk-trigger-chevron { flex: 0 0 auto; color: ${muted}; }
|
|
109
|
+
.oc-sdk-popup { position: fixed; z-index: 50; display: flex; flex-direction: column; gap: 2px; min-width: 160px; max-width: calc(100vw - 16px); max-height: min(320px, calc(100vh - 16px)); overflow: auto; padding: 4px; border: 1px solid ${mix(border, 60)}; border-radius: 12px; background: ${elevated}; color: ${elevatedFg}; box-shadow: 0 8px 24px ${mix(fg, 12)}; }
|
|
110
|
+
.oc-sdk-popup-search { flex: 0 0 auto; padding: 2px 2px 4px; }
|
|
111
|
+
.oc-sdk-popup-search .oc-sdk-input { height: 32px; font-size: 0.8125rem; }
|
|
112
|
+
.oc-sdk-option { display: flex; align-items: center; gap: 8px; width: 100%; padding: 6px 8px; border-radius: 8px; font-size: 0.8125rem; text-align: left; }
|
|
113
|
+
.oc-sdk-option[data-active="true"] { background: ${hover}; }
|
|
114
|
+
.oc-sdk-option[aria-selected="true"] { background: ${selection}; color: ${selectionFg}; }
|
|
115
|
+
.oc-sdk-option[data-destructive="true"] { color: ${v('status-error', 'red')}; }
|
|
116
|
+
.oc-sdk-option[data-destructive="true"][data-active="true"] { background: ${mix(v('status-error', 'red'), 10)}; }
|
|
117
|
+
.oc-sdk-option-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
118
|
+
.oc-sdk-option-hint { flex: 0 0 auto; font-size: 0.75rem; color: ${muted}; }
|
|
119
|
+
.oc-sdk-option-check { flex: 0 0 auto; width: 12px; }
|
|
120
|
+
.oc-sdk-popup-empty { padding: 8px; font-size: 0.8125rem; color: ${muted}; }
|
|
121
|
+
|
|
122
|
+
.oc-sdk-check { display: inline-flex; align-items: flex-start; gap: 8px; width: 100%; text-align: left; }
|
|
123
|
+
.oc-sdk-check-box { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; margin-top: 3px; border: 1px solid ${border}; border-radius: 4px; color: ${primary}; transition: border-color 150ms ease-out; }
|
|
124
|
+
.oc-sdk-check[aria-checked="true"] .oc-sdk-check-box { border-color: ${mix(primary, 65, border)}; }
|
|
125
|
+
.oc-sdk-check-box > svg { display: none; }
|
|
126
|
+
.oc-sdk-check[aria-checked="true"] .oc-sdk-check-box > svg { display: block; }
|
|
127
|
+
.oc-sdk-check-thumb { flex: 0 0 auto; position: relative; width: 36px; height: 20px; border-radius: 9999px; background: ${border}; transition: background 150ms ease-out; }
|
|
128
|
+
.oc-sdk-check-thumb::after { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 9999px; background: ${bg}; transition: transform 150ms ease-out; }
|
|
129
|
+
.oc-sdk-check[aria-checked="true"] .oc-sdk-check-thumb { background: ${primary}; }
|
|
130
|
+
.oc-sdk-check[aria-checked="true"] .oc-sdk-check-thumb::after { transform: translateX(16px); }
|
|
131
|
+
.oc-sdk-check:focus-visible { box-shadow: none; }
|
|
132
|
+
.oc-sdk-check:focus-visible .oc-sdk-check-box, .oc-sdk-check:focus-visible .oc-sdk-check-thumb { ${focusRing} }
|
|
133
|
+
.oc-sdk-check-text { display: flex; flex-direction: column; min-width: 0; }
|
|
134
|
+
.oc-sdk-check-label { font-size: 0.875rem; }
|
|
135
|
+
.oc-sdk-check-desc { font-size: 0.75rem; color: ${muted}; }
|
|
136
|
+
|
|
137
|
+
.oc-sdk-tabs { display: inline-flex; gap: 2px; padding: 2px; border-radius: 10px; max-width: 100%; overflow: auto; }
|
|
138
|
+
.oc-sdk-tabs[data-track="true"] { background: ${mix(fg, 4)}; }
|
|
139
|
+
.oc-sdk-tab { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px; border: 1px solid transparent; border-radius: 8px; font-size: 0.8125rem; font-weight: 500; color: ${muted}; white-space: nowrap; transition: color 150ms ease-out, background 150ms ease-out; }
|
|
140
|
+
.oc-sdk-tab:hover { color: ${fg}; }
|
|
141
|
+
.oc-sdk-tab[aria-selected="true"] { color: ${fg}; background: ${elevated}; border-color: ${mix(fg, 7)}; box-shadow: 0 1px 2px ${mix(fg, 6)}; }
|
|
142
|
+
.oc-sdk-tab-count { font-size: 0.75rem; font-variant-numeric: tabular-nums; color: ${muted}; }
|
|
143
|
+
|
|
144
|
+
.oc-sdk-badge { display: inline-flex; align-items: center; padding: 1px 6px; border-radius: 9999px; font-size: 11px; font-weight: 500; line-height: 16px; white-space: nowrap; background: ${hover}; color: ${muted}; }
|
|
145
|
+
.oc-sdk-badge[data-tone] { color: var(--oc-sdk-tone); background: ${mix('var(--oc-sdk-tone)', 15)}; }
|
|
146
|
+
|
|
147
|
+
.oc-sdk-list { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
|
|
148
|
+
.oc-sdk-row { display: flex; align-items: center; gap: 8px; width: 100%; padding: 6px 8px; border-radius: 6px; text-align: left; transition: background 120ms ease-out; }
|
|
149
|
+
.oc-sdk-row:hover, .oc-sdk-row[data-active="true"] { background: ${hover}; }
|
|
150
|
+
.oc-sdk-row[aria-selected="true"] { background: ${selection}; color: ${selectionFg}; }
|
|
151
|
+
.oc-sdk-row-lead { flex: 0 0 auto; width: 64px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ${mono}; font-size: 0.75rem; color: ${muted}; }
|
|
152
|
+
.oc-sdk-row-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
|
|
153
|
+
.oc-sdk-row-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
154
|
+
.oc-sdk-row-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.75rem; color: ${muted}; }
|
|
155
|
+
.oc-sdk-row-meta { flex: 0 0 auto; font-size: 0.75rem; font-variant-numeric: tabular-nums; color: ${muted}; }
|
|
156
|
+
.oc-sdk-row[aria-selected="true"] .oc-sdk-row-lead, .oc-sdk-row[aria-selected="true"] .oc-sdk-row-sub, .oc-sdk-row[aria-selected="true"] .oc-sdk-row-meta { color: inherit; opacity: .75; }
|
|
157
|
+
.oc-sdk-list-empty { padding: 16px 8px; text-align: center; font-size: 0.8125rem; color: ${muted}; }
|
|
158
|
+
|
|
159
|
+
.oc-sdk-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; padding: 40px 16px; text-align: center; }
|
|
160
|
+
.oc-sdk-empty-title { margin: 0; font-size: 0.8125rem; font-weight: 600; }
|
|
161
|
+
.oc-sdk-empty-body { margin: 0; max-width: 32rem; font-size: 0.8125rem; color: ${muted}; }
|
|
162
|
+
.oc-sdk-empty-action { margin-top: 12px; }
|
|
163
|
+
|
|
164
|
+
@keyframes oc-sdk-spin { to { transform: rotate(360deg); } }
|
|
165
|
+
.oc-sdk-spinner { display: inline-flex; align-items: center; gap: 8px; font-size: 0.8125rem; color: ${muted}; }
|
|
166
|
+
.oc-sdk-spinner-ring { width: 16px; height: 16px; border: 2px solid ${border}; border-top-color: ${primary}; border-radius: 9999px; animation: oc-sdk-spin .8s linear infinite; }
|
|
167
|
+
.oc-sdk-spinner[data-size="sm"] .oc-sdk-spinner-ring { width: 12px; height: 12px; }
|
|
168
|
+
|
|
169
|
+
.oc-sdk-banner { display: flex; align-items: flex-start; gap: 12px; padding: 8px 12px; border: 1px solid ${mix('var(--oc-sdk-tone)', 40)}; border-radius: 8px; background: ${mix('var(--oc-sdk-tone)', 10)}; }
|
|
170
|
+
.oc-sdk-banner-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
171
|
+
.oc-sdk-banner-title { font-size: 0.8125rem; font-weight: 500; color: var(--oc-sdk-tone); }
|
|
172
|
+
.oc-sdk-banner-body { font-size: 0.8125rem; color: ${muted}; }
|
|
173
|
+
.oc-sdk-banner-action { flex: 0 0 auto; }
|
|
174
|
+
|
|
175
|
+
.oc-sdk-separator { display: flex; align-items: center; gap: 8px; width: 100%; margin: 8px 0; font-size: 0.75rem; color: ${muted}; }
|
|
176
|
+
.oc-sdk-separator::before, .oc-sdk-separator::after { content: ""; flex: 1 1 auto; height: 1px; background: ${mix(border, 40)}; }
|
|
177
|
+
.oc-sdk-separator[data-labeled="false"]::after { display: none; }
|
|
178
|
+
.oc-sdk-popup > .oc-sdk-separator { margin: 4px 0; }
|
|
179
|
+
|
|
180
|
+
.oc-sdk-progress { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
|
181
|
+
.oc-sdk-progress-label { display: flex; justify-content: space-between; font-size: 0.75rem; color: ${muted}; font-variant-numeric: tabular-nums; }
|
|
182
|
+
.oc-sdk-progress-track { height: 6px; border-radius: 9999px; background: ${border}; overflow: hidden; }
|
|
183
|
+
.oc-sdk-progress-fill { height: 100%; border-radius: 9999px; background: var(--oc-sdk-tone, ${primary}); transform-origin: left; transition: transform 200ms ease-out; }
|
|
184
|
+
|
|
185
|
+
.oc-sdk-menu { position: relative; display: inline-flex; }
|
|
186
|
+
|
|
187
|
+
.oc-sdk-text { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
188
|
+
.oc-sdk-text a { color: ${primary}; text-decoration: underline; text-underline-offset: 2px; }
|
|
189
|
+
.oc-sdk-text img { display: block; max-width: 100%; margin: 8px 0; border-radius: 8px; border: 1px solid ${mix(border, 60)}; }
|
|
190
|
+
`;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Handle } from './dom.ts';
|
|
2
|
+
export type TabItem = {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
count?: number;
|
|
6
|
+
};
|
|
7
|
+
export type TabsProps = {
|
|
8
|
+
items: TabItem[];
|
|
9
|
+
activeId: string;
|
|
10
|
+
onChange: (id: string) => void;
|
|
11
|
+
/** Paints a faint track behind the pills. */
|
|
12
|
+
trackBackground?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export type TabsHandle = Handle<TabsProps>;
|
|
15
|
+
export declare const mountTabs: (root: Element, initial: TabsProps) => TabsHandle;
|