@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.
Files changed (75) hide show
  1. package/API.md +529 -0
  2. package/DOCUMENTATION.md +81 -0
  3. package/GUEST_SERVICES.md +166 -0
  4. package/LICENSE +21 -0
  5. package/README.md +186 -0
  6. package/dist/api-version.d.ts +6 -0
  7. package/dist/api-version.js +5 -0
  8. package/dist/contract.d.ts +471 -0
  9. package/dist/contract.js +256 -0
  10. package/dist/host-version.d.ts +16 -0
  11. package/dist/host-version.js +50 -0
  12. package/dist/host.d.ts +101 -0
  13. package/dist/host.js +606 -0
  14. package/dist/index.d.ts +12 -0
  15. package/dist/index.js +7 -0
  16. package/dist/manifest.d.ts +298 -0
  17. package/dist/manifest.js +224 -0
  18. package/dist/parse.d.ts +279 -0
  19. package/dist/parse.js +379 -0
  20. package/dist/protocol.d.ts +1092 -0
  21. package/dist/protocol.js +450 -0
  22. package/dist/schemas.d.ts +4 -0
  23. package/dist/schemas.js +6 -0
  24. package/dist/ui/badge.d.ts +10 -0
  25. package/dist/ui/badge.js +26 -0
  26. package/dist/ui/banner.d.ts +13 -0
  27. package/dist/ui/banner.js +48 -0
  28. package/dist/ui/button.d.ts +14 -0
  29. package/dist/ui/button.js +49 -0
  30. package/dist/ui/checkbox.d.ts +12 -0
  31. package/dist/ui/checkbox.js +45 -0
  32. package/dist/ui/dom.d.ts +15 -0
  33. package/dist/ui/dom.js +60 -0
  34. package/dist/ui/empty.d.ts +11 -0
  35. package/dist/ui/empty.js +44 -0
  36. package/dist/ui/field.d.ts +18 -0
  37. package/dist/ui/field.js +49 -0
  38. package/dist/ui/icons.d.ts +10 -0
  39. package/dist/ui/icons.js +23 -0
  40. package/dist/ui/index.d.ts +35 -0
  41. package/dist/ui/index.js +17 -0
  42. package/dist/ui/list.d.ts +26 -0
  43. package/dist/ui/list.js +90 -0
  44. package/dist/ui/menu.d.ts +20 -0
  45. package/dist/ui/menu.js +111 -0
  46. package/dist/ui/navigation.d.ts +18 -0
  47. package/dist/ui/navigation.js +38 -0
  48. package/dist/ui/option.d.ts +16 -0
  49. package/dist/ui/option.js +35 -0
  50. package/dist/ui/popup.d.ts +5 -0
  51. package/dist/ui/popup.js +43 -0
  52. package/dist/ui/progress.d.ts +11 -0
  53. package/dist/ui/progress.js +44 -0
  54. package/dist/ui/search.d.ts +11 -0
  55. package/dist/ui/search.js +62 -0
  56. package/dist/ui/select.d.ts +22 -0
  57. package/dist/ui/select.js +164 -0
  58. package/dist/ui/separator.d.ts +6 -0
  59. package/dist/ui/separator.js +26 -0
  60. package/dist/ui/spinner.d.ts +8 -0
  61. package/dist/ui/spinner.js +29 -0
  62. package/dist/ui/style.d.ts +1 -0
  63. package/dist/ui/style.js +190 -0
  64. package/dist/ui/tabs.d.ts +15 -0
  65. package/dist/ui/tabs.js +61 -0
  66. package/dist/ui/text.d.ts +23 -0
  67. package/dist/ui/text.js +89 -0
  68. package/dist/ui/theme.d.ts +22 -0
  69. package/dist/ui/theme.js +69 -0
  70. package/dist/workspace-schemas.d.ts +136 -0
  71. package/dist/workspace-schemas.js +44 -0
  72. package/dist/workspace.d.ts +109 -0
  73. package/dist/workspace.js +4 -0
  74. package/package.json +55 -0
  75. package/scripts/bundle-guest.ts +44 -0
@@ -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,6 @@
1
+ import { type Handle } from './dom.ts';
2
+ export type SeparatorProps = {
3
+ label?: string;
4
+ };
5
+ export type SeparatorHandle = Handle<SeparatorProps>;
6
+ export declare const mountSeparator: (root: Element, initial?: SeparatorProps) => SeparatorHandle;
@@ -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;
@@ -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;