@aiworker/sdk 1.24.0
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 +575 -0
- package/DOCUMENTATION.md +120 -0
- package/GUEST_SERVICES.md +166 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/dist/api-version.d.ts +6 -0
- package/dist/api-version.js +5 -0
- package/dist/contract.d.ts +504 -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 +107 -0
- package/dist/host.js +639 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +8 -0
- package/dist/manifest.d.ts +303 -0
- package/dist/manifest.js +222 -0
- package/dist/parse.d.ts +293 -0
- package/dist/parse.js +398 -0
- package/dist/protocol.d.ts +1148 -0
- package/dist/protocol.js +473 -0
- package/dist/schemas.d.ts +4 -0
- package/dist/schemas.js +6 -0
- package/dist/scrollbar-style.d.ts +2 -0
- package/dist/scrollbar-style.js +32 -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 +115 -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 +168 -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 +202 -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 +79 -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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { button, ensureStyle, setText } from "./dom.js";
|
|
2
|
+
import { UI_CSS } from "./style.js";
|
|
3
|
+
const ring = () => {
|
|
4
|
+
const spinner = document.createElement('span');
|
|
5
|
+
spinner.className = 'oc-sdk-spinner-ring';
|
|
6
|
+
spinner.setAttribute('aria-hidden', 'true');
|
|
7
|
+
return spinner;
|
|
8
|
+
};
|
|
9
|
+
export const mountButton = (root, initial) => {
|
|
10
|
+
ensureStyle(UI_CSS);
|
|
11
|
+
let props = initial;
|
|
12
|
+
const node = button('oc-sdk oc-sdk-btn');
|
|
13
|
+
const spinner = ring();
|
|
14
|
+
const label = document.createElement('span');
|
|
15
|
+
node.append(label);
|
|
16
|
+
root.append(node);
|
|
17
|
+
const paint = () => {
|
|
18
|
+
node.dataset.variant = props.variant ?? 'default';
|
|
19
|
+
node.dataset.size = props.size ?? 'default';
|
|
20
|
+
node.disabled = Boolean(props.disabled) || Boolean(props.loading);
|
|
21
|
+
node.dataset.loading = props.loading ? 'true' : 'false';
|
|
22
|
+
node.setAttribute('aria-busy', props.loading ? 'true' : 'false');
|
|
23
|
+
if (props.loading && spinner.parentNode !== node) {
|
|
24
|
+
node.prepend(spinner);
|
|
25
|
+
}
|
|
26
|
+
else if (!props.loading && spinner.parentNode === node) {
|
|
27
|
+
spinner.remove();
|
|
28
|
+
}
|
|
29
|
+
setText(label, props.label);
|
|
30
|
+
};
|
|
31
|
+
const onClick = () => {
|
|
32
|
+
if (props.disabled || props.loading) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
props.onClick();
|
|
36
|
+
};
|
|
37
|
+
node.addEventListener('click', onClick);
|
|
38
|
+
paint();
|
|
39
|
+
return {
|
|
40
|
+
update: (next) => {
|
|
41
|
+
props = { ...props, ...next };
|
|
42
|
+
paint();
|
|
43
|
+
},
|
|
44
|
+
dispose: () => {
|
|
45
|
+
node.removeEventListener('click', onClick);
|
|
46
|
+
node.remove();
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Handle } from './dom.ts';
|
|
2
|
+
export type CheckboxProps = {
|
|
3
|
+
label: string;
|
|
4
|
+
checked: boolean;
|
|
5
|
+
onChange: (checked: boolean) => void;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
/** Muted line under the label. */
|
|
8
|
+
description?: string;
|
|
9
|
+
};
|
|
10
|
+
export type CheckboxHandle = Handle<CheckboxProps>;
|
|
11
|
+
export declare const mountCheckbox: (root: Element, initial: CheckboxProps) => CheckboxHandle;
|
|
12
|
+
export declare const mountSwitch: (root: Element, initial: CheckboxProps) => CheckboxHandle;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { button, el, ensureStyle, setText } from "./dom.js";
|
|
2
|
+
import { icon } from "./icons.js";
|
|
3
|
+
import { UI_CSS } from "./style.js";
|
|
4
|
+
const mountToggle = (root, initial, role) => {
|
|
5
|
+
ensureStyle(UI_CSS);
|
|
6
|
+
let props = initial;
|
|
7
|
+
const node = button('oc-sdk oc-sdk-check');
|
|
8
|
+
node.setAttribute('role', role);
|
|
9
|
+
const control = el('span', role === 'switch' ? 'oc-sdk-check-thumb' : 'oc-sdk-check-box');
|
|
10
|
+
if (role === 'checkbox') {
|
|
11
|
+
control.append(icon('check', 12));
|
|
12
|
+
}
|
|
13
|
+
const text = el('span', 'oc-sdk-check-text');
|
|
14
|
+
const label = el('span', 'oc-sdk-check-label');
|
|
15
|
+
const description = el('span', 'oc-sdk-check-desc');
|
|
16
|
+
text.append(label, description);
|
|
17
|
+
node.append(control, text);
|
|
18
|
+
root.append(node);
|
|
19
|
+
const paint = () => {
|
|
20
|
+
node.setAttribute('aria-checked', props.checked ? 'true' : 'false');
|
|
21
|
+
node.disabled = Boolean(props.disabled);
|
|
22
|
+
setText(label, props.label);
|
|
23
|
+
setText(description, props.description);
|
|
24
|
+
description.hidden = !props.description;
|
|
25
|
+
};
|
|
26
|
+
const onClick = () => {
|
|
27
|
+
if (!props.disabled) {
|
|
28
|
+
props.onChange(!props.checked);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
node.addEventListener('click', onClick);
|
|
32
|
+
paint();
|
|
33
|
+
return {
|
|
34
|
+
update: (next) => {
|
|
35
|
+
props = { ...props, ...next };
|
|
36
|
+
paint();
|
|
37
|
+
},
|
|
38
|
+
dispose: () => {
|
|
39
|
+
node.removeEventListener('click', onClick);
|
|
40
|
+
node.remove();
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export const mountCheckbox = (root, initial) => (mountToggle(root, initial, 'checkbox'));
|
|
45
|
+
export const mountSwitch = (root, initial) => (mountToggle(root, initial, 'switch'));
|
package/dist/ui/dom.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Every mount returns this. `update` merges props and repaints; `dispose` removes the node and every listener. */
|
|
2
|
+
export type Handle<P> = {
|
|
3
|
+
update: (next: Partial<P>) => void;
|
|
4
|
+
dispose: () => void;
|
|
5
|
+
};
|
|
6
|
+
export declare const clearNode: (node: Element) => void;
|
|
7
|
+
export declare const ensureStyle: (css: string) => void;
|
|
8
|
+
export declare const el: <K extends keyof HTMLElementTagNameMap>(tag: K, className?: string) => HTMLElementTagNameMap[K];
|
|
9
|
+
export declare const button: (className: string) => HTMLButtonElement;
|
|
10
|
+
/** Writes text only when it changed so a repaint does not disturb selection or layout. */
|
|
11
|
+
export declare const setText: (node: Element, text: string | null | undefined) => void;
|
|
12
|
+
/** Sets or removes an attribute depending on whether a value is present. */
|
|
13
|
+
export declare const setAttr: (node: Element, name: string, value: string | null | undefined) => void;
|
|
14
|
+
/** Runs `handler` on a pointer press outside `node`. Returns the disposer. */
|
|
15
|
+
export declare const onOutsideClick: (node: Element, handler: () => void) => (() => void);
|
package/dist/ui/dom.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const STYLE_ID = 'oc-sdk-ui-style';
|
|
2
|
+
export const clearNode = (node) => {
|
|
3
|
+
while (node.firstChild) {
|
|
4
|
+
node.removeChild(node.firstChild);
|
|
5
|
+
}
|
|
6
|
+
};
|
|
7
|
+
export const ensureStyle = (css) => {
|
|
8
|
+
const existing = document.getElementById(STYLE_ID);
|
|
9
|
+
if (existing instanceof HTMLStyleElement) {
|
|
10
|
+
if (existing.textContent !== css) {
|
|
11
|
+
existing.textContent = css;
|
|
12
|
+
}
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const style = document.createElement('style');
|
|
16
|
+
style.id = STYLE_ID;
|
|
17
|
+
style.textContent = css;
|
|
18
|
+
document.head.appendChild(style);
|
|
19
|
+
};
|
|
20
|
+
export const el = (tag, className) => {
|
|
21
|
+
const node = document.createElement(tag);
|
|
22
|
+
if (className) {
|
|
23
|
+
node.className = className;
|
|
24
|
+
}
|
|
25
|
+
return node;
|
|
26
|
+
};
|
|
27
|
+
export const button = (className) => {
|
|
28
|
+
const node = el('button', className);
|
|
29
|
+
node.type = 'button';
|
|
30
|
+
return node;
|
|
31
|
+
};
|
|
32
|
+
/** Writes text only when it changed so a repaint does not disturb selection or layout. */
|
|
33
|
+
export const setText = (node, text) => {
|
|
34
|
+
const next = text ?? '';
|
|
35
|
+
if (node.textContent !== next) {
|
|
36
|
+
node.textContent = next;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
/** Sets or removes an attribute depending on whether a value is present. */
|
|
40
|
+
export const setAttr = (node, name, value) => {
|
|
41
|
+
if (value === undefined || value === null || value === '') {
|
|
42
|
+
node.removeAttribute(name);
|
|
43
|
+
}
|
|
44
|
+
else if (node.getAttribute(name) !== value) {
|
|
45
|
+
node.setAttribute(name, value);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
/** Runs `handler` on a pointer press outside `node`. Returns the disposer. */
|
|
49
|
+
export const onOutsideClick = (node, handler) => {
|
|
50
|
+
const listener = (event) => {
|
|
51
|
+
if (event.target instanceof Node && node.contains(event.target)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
handler();
|
|
55
|
+
};
|
|
56
|
+
document.addEventListener('pointerdown', listener, true);
|
|
57
|
+
return () => {
|
|
58
|
+
document.removeEventListener('pointerdown', listener, true);
|
|
59
|
+
};
|
|
60
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Handle } from './dom.ts';
|
|
2
|
+
export type EmptyProps = {
|
|
3
|
+
title: string;
|
|
4
|
+
body?: string;
|
|
5
|
+
action?: {
|
|
6
|
+
label: string;
|
|
7
|
+
onClick: () => void;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export type EmptyHandle = Handle<EmptyProps>;
|
|
11
|
+
export declare const mountEmpty: (root: Element, initial: EmptyProps) => EmptyHandle;
|
package/dist/ui/empty.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { mountButton } from "./button.js";
|
|
2
|
+
import { el, ensureStyle, setText } from "./dom.js";
|
|
3
|
+
import { UI_CSS } from "./style.js";
|
|
4
|
+
export const mountEmpty = (root, initial) => {
|
|
5
|
+
ensureStyle(UI_CSS);
|
|
6
|
+
let props = initial;
|
|
7
|
+
const shell = el('div', 'oc-sdk oc-sdk-empty');
|
|
8
|
+
const title = el('h2', 'oc-sdk-empty-title');
|
|
9
|
+
const body = el('p', 'oc-sdk-empty-body');
|
|
10
|
+
const slot = el('div', 'oc-sdk-empty-action');
|
|
11
|
+
shell.append(title, body, slot);
|
|
12
|
+
root.append(shell);
|
|
13
|
+
let action = null;
|
|
14
|
+
const paint = () => {
|
|
15
|
+
setText(title, props.title);
|
|
16
|
+
setText(body, props.body);
|
|
17
|
+
body.hidden = !props.body;
|
|
18
|
+
slot.hidden = !props.action;
|
|
19
|
+
if (!props.action) {
|
|
20
|
+
action?.dispose();
|
|
21
|
+
action = null;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const next = { label: props.action.label, onClick: props.action.onClick };
|
|
25
|
+
if (action) {
|
|
26
|
+
action.update(next);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
action = mountButton(slot, { ...next, variant: 'outline', size: 'sm' });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
paint();
|
|
33
|
+
return {
|
|
34
|
+
update: (next) => {
|
|
35
|
+
props = { ...props, ...next };
|
|
36
|
+
paint();
|
|
37
|
+
},
|
|
38
|
+
dispose: () => {
|
|
39
|
+
action?.dispose();
|
|
40
|
+
action = null;
|
|
41
|
+
shell.remove();
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Handle } from './dom.ts';
|
|
2
|
+
export type TextFieldProps = {
|
|
3
|
+
label?: string;
|
|
4
|
+
value: string;
|
|
5
|
+
onChange: (value: string) => void;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
password?: boolean;
|
|
8
|
+
multiline?: boolean;
|
|
9
|
+
rows?: number;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
/** Error text. Turns the ring red and replaces `helper`. */
|
|
12
|
+
error?: string;
|
|
13
|
+
helper?: string;
|
|
14
|
+
/** Monospace input, for tokens and identifiers. */
|
|
15
|
+
mono?: boolean;
|
|
16
|
+
};
|
|
17
|
+
export type TextFieldHandle = Handle<TextFieldProps>;
|
|
18
|
+
export declare const mountTextField: (root: Element, initial: TextFieldProps) => TextFieldHandle;
|
package/dist/ui/field.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { el, ensureStyle, setAttr, setText } from "./dom.js";
|
|
2
|
+
import { UI_CSS } from "./style.js";
|
|
3
|
+
export const mountTextField = (root, initial) => {
|
|
4
|
+
ensureStyle(UI_CSS);
|
|
5
|
+
let props = initial;
|
|
6
|
+
const field = el('label', 'oc-sdk oc-sdk-field');
|
|
7
|
+
const caption = el('span', 'oc-sdk-field-label');
|
|
8
|
+
const input = props.multiline ? el('textarea', 'oc-sdk-input') : el('input', 'oc-sdk-input');
|
|
9
|
+
const note = el('span', 'oc-sdk-field-note');
|
|
10
|
+
field.append(caption, input, note);
|
|
11
|
+
root.append(field);
|
|
12
|
+
const paint = () => {
|
|
13
|
+
setText(caption, props.label);
|
|
14
|
+
caption.hidden = !props.label;
|
|
15
|
+
if (input instanceof HTMLInputElement) {
|
|
16
|
+
input.type = props.password ? 'password' : 'text';
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
input.rows = props.rows ?? 3;
|
|
20
|
+
}
|
|
21
|
+
if (input.value !== props.value) {
|
|
22
|
+
input.value = props.value;
|
|
23
|
+
}
|
|
24
|
+
input.disabled = Boolean(props.disabled);
|
|
25
|
+
setAttr(input, 'placeholder', props.placeholder);
|
|
26
|
+
input.dataset.mono = props.mono ? 'true' : 'false';
|
|
27
|
+
const invalid = Boolean(props.error);
|
|
28
|
+
field.dataset.invalid = invalid ? 'true' : 'false';
|
|
29
|
+
input.setAttribute('aria-invalid', invalid ? 'true' : 'false');
|
|
30
|
+
const text = props.error ?? props.helper ?? '';
|
|
31
|
+
setText(note, text);
|
|
32
|
+
note.hidden = text === '';
|
|
33
|
+
};
|
|
34
|
+
const onInput = () => {
|
|
35
|
+
props.onChange(input.value);
|
|
36
|
+
};
|
|
37
|
+
input.addEventListener('input', onInput);
|
|
38
|
+
paint();
|
|
39
|
+
return {
|
|
40
|
+
update: (next) => {
|
|
41
|
+
props = { ...props, ...next };
|
|
42
|
+
paint();
|
|
43
|
+
},
|
|
44
|
+
dispose: () => {
|
|
45
|
+
input.removeEventListener('input', onInput);
|
|
46
|
+
field.remove();
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Remixicon outlines. Add a shape only when a primitive paints it. */
|
|
2
|
+
declare const ICON_PATH: {
|
|
3
|
+
readonly search: "M18.031 16.617l4.283 4.282-1.415 1.415-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9 9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617zm-2.006-.742A6.977 6.977 0 0 0 18 11c0-3.868-3.133-7-7-7-3.868 0-7 3.132-7 7 0 3.867 3.132 7 7 7a6.977 6.977 0 0 0 4.875-1.975l.15-.15z";
|
|
4
|
+
readonly chevron: "M12 13.172l4.95-4.95 1.414 1.414L12 16 5.636 9.636 7.05 8.222z";
|
|
5
|
+
readonly check: "M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z";
|
|
6
|
+
readonly close: "M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z";
|
|
7
|
+
};
|
|
8
|
+
type IconName = keyof typeof ICON_PATH;
|
|
9
|
+
export declare const icon: (name: IconName, size: number, className?: string) => SVGSVGElement;
|
|
10
|
+
export {};
|
package/dist/ui/icons.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
2
|
+
/** Remixicon outlines. Add a shape only when a primitive paints it. */
|
|
3
|
+
const ICON_PATH = {
|
|
4
|
+
search: 'M18.031 16.617l4.283 4.282-1.415 1.415-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9 9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617zm-2.006-.742A6.977 6.977 0 0 0 18 11c0-3.868-3.133-7-7-7-3.868 0-7 3.132-7 7 0 3.867 3.132 7 7 7a6.977 6.977 0 0 0 4.875-1.975l.15-.15z',
|
|
5
|
+
chevron: 'M12 13.172l4.95-4.95 1.414 1.414L12 16 5.636 9.636 7.05 8.222z',
|
|
6
|
+
check: 'M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z',
|
|
7
|
+
close: 'M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z',
|
|
8
|
+
};
|
|
9
|
+
export const icon = (name, size, className) => {
|
|
10
|
+
const node = document.createElementNS(SVG_NS, 'svg');
|
|
11
|
+
node.setAttribute('viewBox', '0 0 24 24');
|
|
12
|
+
node.setAttribute('width', String(size));
|
|
13
|
+
node.setAttribute('height', String(size));
|
|
14
|
+
node.setAttribute('aria-hidden', 'true');
|
|
15
|
+
node.setAttribute('fill', 'currentColor');
|
|
16
|
+
if (className) {
|
|
17
|
+
node.setAttribute('class', className);
|
|
18
|
+
}
|
|
19
|
+
const path = document.createElementNS(SVG_NS, 'path');
|
|
20
|
+
path.setAttribute('d', ICON_PATH[name]);
|
|
21
|
+
node.append(path);
|
|
22
|
+
return node;
|
|
23
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export { applyHostReady, applyHostTheme } from './theme.ts';
|
|
2
|
+
export type { ThemeRoot } from './theme.ts';
|
|
3
|
+
export type { Handle } from './dom.ts';
|
|
4
|
+
export { mountButton } from './button.ts';
|
|
5
|
+
export type { ButtonHandle, ButtonProps, ButtonSize, ButtonVariant } from './button.ts';
|
|
6
|
+
export { mountTextField } from './field.ts';
|
|
7
|
+
export type { TextFieldHandle, TextFieldProps } from './field.ts';
|
|
8
|
+
export { mountSearchField } from './search.ts';
|
|
9
|
+
export type { SearchFieldHandle, SearchFieldProps } from './search.ts';
|
|
10
|
+
export { filterSelectOptions, mountSelect } from './select.ts';
|
|
11
|
+
export type { SelectHandle, SelectOption, SelectProps } from './select.ts';
|
|
12
|
+
export { mountCheckbox, mountSwitch } from './checkbox.ts';
|
|
13
|
+
export type { CheckboxHandle, CheckboxProps } from './checkbox.ts';
|
|
14
|
+
export { mountTabs } from './tabs.ts';
|
|
15
|
+
export type { TabItem, TabsHandle, TabsProps } from './tabs.ts';
|
|
16
|
+
export { mountBadge } from './badge.ts';
|
|
17
|
+
export type { BadgeHandle, BadgeProps, Tone } from './badge.ts';
|
|
18
|
+
export { mountList } from './list.ts';
|
|
19
|
+
export type { ListHandle, ListItem, ListProps } from './list.ts';
|
|
20
|
+
export { moveListSelection, navigationKey } from './navigation.ts';
|
|
21
|
+
export type { NavigableItem, NavigationKey } from './navigation.ts';
|
|
22
|
+
export { mountEmpty } from './empty.ts';
|
|
23
|
+
export type { EmptyHandle, EmptyProps } from './empty.ts';
|
|
24
|
+
export { mountSpinner } from './spinner.ts';
|
|
25
|
+
export type { SpinnerHandle, SpinnerProps } from './spinner.ts';
|
|
26
|
+
export { mountBanner } from './banner.ts';
|
|
27
|
+
export type { BannerHandle, BannerProps, BannerTone } from './banner.ts';
|
|
28
|
+
export { mountSeparator } from './separator.ts';
|
|
29
|
+
export type { SeparatorHandle, SeparatorProps } from './separator.ts';
|
|
30
|
+
export { mountProgress } from './progress.ts';
|
|
31
|
+
export type { ProgressHandle, ProgressProps } from './progress.ts';
|
|
32
|
+
export { mountMenu } from './menu.ts';
|
|
33
|
+
export type { MenuHandle, MenuItem, MenuProps } from './menu.ts';
|
|
34
|
+
export { mountText, splitTextMedia } from './text.ts';
|
|
35
|
+
export type { TextHandle, TextPart, TextProps } from './text.ts';
|
package/dist/ui/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { applyHostReady, applyHostTheme } from "./theme.js";
|
|
2
|
+
export { mountButton } from "./button.js";
|
|
3
|
+
export { mountTextField } from "./field.js";
|
|
4
|
+
export { mountSearchField } from "./search.js";
|
|
5
|
+
export { filterSelectOptions, mountSelect } from "./select.js";
|
|
6
|
+
export { mountCheckbox, mountSwitch } from "./checkbox.js";
|
|
7
|
+
export { mountTabs } from "./tabs.js";
|
|
8
|
+
export { mountBadge } from "./badge.js";
|
|
9
|
+
export { mountList } from "./list.js";
|
|
10
|
+
export { moveListSelection, navigationKey } from "./navigation.js";
|
|
11
|
+
export { mountEmpty } from "./empty.js";
|
|
12
|
+
export { mountSpinner } from "./spinner.js";
|
|
13
|
+
export { mountBanner } from "./banner.js";
|
|
14
|
+
export { mountSeparator } from "./separator.js";
|
|
15
|
+
export { mountProgress } from "./progress.js";
|
|
16
|
+
export { mountMenu } from "./menu.js";
|
|
17
|
+
export { mountText, splitTextMedia } from "./text.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Tone } from './badge.ts';
|
|
2
|
+
import { type Handle } from './dom.ts';
|
|
3
|
+
export type ListItem = {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
/** Micro muted line under the title. */
|
|
7
|
+
subtitle?: string;
|
|
8
|
+
/** Fixed-width mono text before the title, like an issue key. */
|
|
9
|
+
leading?: string;
|
|
10
|
+
/** Muted tabular text at the right, like a date or count. */
|
|
11
|
+
meta?: string;
|
|
12
|
+
badge?: {
|
|
13
|
+
label: string;
|
|
14
|
+
tone?: Tone;
|
|
15
|
+
};
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
};
|
|
18
|
+
export type ListProps = {
|
|
19
|
+
items: ListItem[];
|
|
20
|
+
selectedId?: string | null;
|
|
21
|
+
onSelect: (id: string) => void;
|
|
22
|
+
emptyText?: string;
|
|
23
|
+
ariaLabel?: string;
|
|
24
|
+
};
|
|
25
|
+
export type ListHandle = Handle<ListProps>;
|
|
26
|
+
export declare const mountList: (root: Element, initial: ListProps) => ListHandle;
|
package/dist/ui/list.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { applyTone } from "./badge.js";
|
|
2
|
+
import { button, clearNode, el, ensureStyle, setAttr } from "./dom.js";
|
|
3
|
+
import { moveListSelection, navigationKey } from "./navigation.js";
|
|
4
|
+
import { UI_CSS } from "./style.js";
|
|
5
|
+
let listCount = 0;
|
|
6
|
+
export const mountList = (root, initial) => {
|
|
7
|
+
ensureStyle(UI_CSS);
|
|
8
|
+
let props = initial;
|
|
9
|
+
const uid = `oc-sdk-list-${listCount += 1}`;
|
|
10
|
+
const list = el('div', 'oc-sdk oc-sdk-list');
|
|
11
|
+
list.setAttribute('role', 'listbox');
|
|
12
|
+
list.tabIndex = 0;
|
|
13
|
+
root.append(list);
|
|
14
|
+
let activeId = null;
|
|
15
|
+
const rowId = (id) => `${uid}-${id}`;
|
|
16
|
+
const setActive = (id) => {
|
|
17
|
+
activeId = id;
|
|
18
|
+
for (const row of Array.from(list.children)) {
|
|
19
|
+
if (row instanceof HTMLElement) {
|
|
20
|
+
row.dataset.active = row.id === rowId(id ?? '') ? 'true' : 'false';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
setAttr(list, 'aria-activedescendant', id ? rowId(id) : null);
|
|
24
|
+
list.querySelector('[data-active="true"]')?.scrollIntoView({ block: 'nearest' });
|
|
25
|
+
};
|
|
26
|
+
const span = (className, text) => {
|
|
27
|
+
const node = el('span', className);
|
|
28
|
+
node.textContent = text;
|
|
29
|
+
return node;
|
|
30
|
+
};
|
|
31
|
+
const paint = () => {
|
|
32
|
+
clearNode(list);
|
|
33
|
+
setAttr(list, 'aria-label', props.ariaLabel);
|
|
34
|
+
if (props.items.length === 0) {
|
|
35
|
+
list.append(span('oc-sdk-list-empty', props.emptyText ?? 'Nothing here'));
|
|
36
|
+
setActive(null);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
for (const item of props.items) {
|
|
40
|
+
const row = button('oc-sdk-row');
|
|
41
|
+
row.id = rowId(item.id);
|
|
42
|
+
row.setAttribute('role', 'option');
|
|
43
|
+
row.setAttribute('aria-selected', item.id === props.selectedId ? 'true' : 'false');
|
|
44
|
+
row.disabled = Boolean(item.disabled);
|
|
45
|
+
row.tabIndex = -1;
|
|
46
|
+
if (item.leading)
|
|
47
|
+
row.append(span('oc-sdk-row-lead', item.leading));
|
|
48
|
+
const main = el('span', 'oc-sdk-row-main');
|
|
49
|
+
main.append(span('oc-sdk-row-title', item.title));
|
|
50
|
+
if (item.subtitle)
|
|
51
|
+
main.append(span('oc-sdk-row-sub', item.subtitle));
|
|
52
|
+
row.append(main);
|
|
53
|
+
if (item.badge) {
|
|
54
|
+
const badge = span('oc-sdk-badge', item.badge.label);
|
|
55
|
+
applyTone(badge, item.badge.tone);
|
|
56
|
+
row.append(badge);
|
|
57
|
+
}
|
|
58
|
+
if (item.meta)
|
|
59
|
+
row.append(span('oc-sdk-row-meta', item.meta));
|
|
60
|
+
row.addEventListener('click', () => props.onSelect(item.id));
|
|
61
|
+
list.append(row);
|
|
62
|
+
}
|
|
63
|
+
const stillThere = props.items.some((item) => item.id === activeId && !item.disabled);
|
|
64
|
+
setActive(stillThere ? activeId : props.selectedId ?? null);
|
|
65
|
+
};
|
|
66
|
+
const onKeyDown = (event) => {
|
|
67
|
+
const step = navigationKey(event);
|
|
68
|
+
if (step) {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
setActive(moveListSelection(props.items, activeId, step));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if ((event.key === 'Enter' || event.key === ' ') && activeId) {
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
props.onSelect(activeId);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
list.addEventListener('keydown', onKeyDown);
|
|
79
|
+
paint();
|
|
80
|
+
return {
|
|
81
|
+
update: (next) => {
|
|
82
|
+
props = { ...props, ...next };
|
|
83
|
+
paint();
|
|
84
|
+
},
|
|
85
|
+
dispose: () => {
|
|
86
|
+
list.removeEventListener('keydown', onKeyDown);
|
|
87
|
+
list.remove();
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ButtonSize, type ButtonVariant } from './button.ts';
|
|
2
|
+
import { type Handle } from './dom.ts';
|
|
3
|
+
export type MenuItem = {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
destructive?: boolean;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
} | {
|
|
9
|
+
separator: true;
|
|
10
|
+
};
|
|
11
|
+
export type MenuProps = {
|
|
12
|
+
/** Trigger button label. */
|
|
13
|
+
label: string;
|
|
14
|
+
variant?: ButtonVariant;
|
|
15
|
+
size?: ButtonSize;
|
|
16
|
+
items: MenuItem[];
|
|
17
|
+
onSelect: (id: string) => void;
|
|
18
|
+
};
|
|
19
|
+
export type MenuHandle = Handle<MenuProps>;
|
|
20
|
+
export declare const mountMenu: (root: Element, initial: MenuProps) => MenuHandle;
|
package/dist/ui/menu.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { mountButton } from "./button.js";
|
|
2
|
+
import { clearNode, el, ensureStyle } from "./dom.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
|
+
const actions = (items) => (items.filter((item) => !('separator' in item)));
|
|
8
|
+
let menuCount = 0;
|
|
9
|
+
export const mountMenu = (root, initial) => {
|
|
10
|
+
ensureStyle(UI_CSS);
|
|
11
|
+
let props = initial;
|
|
12
|
+
const uid = `oc-sdk-menu-${menuCount += 1}`;
|
|
13
|
+
const wrap = el('div', 'oc-sdk oc-sdk-menu');
|
|
14
|
+
root.append(wrap);
|
|
15
|
+
const popup = el('div', 'oc-sdk oc-sdk-popup');
|
|
16
|
+
popup.setAttribute('role', 'menu');
|
|
17
|
+
popup.tabIndex = -1;
|
|
18
|
+
let closePopup = null;
|
|
19
|
+
let activeId = null;
|
|
20
|
+
const trigger = mountButton(wrap, {
|
|
21
|
+
label: props.label,
|
|
22
|
+
variant: props.variant,
|
|
23
|
+
size: props.size,
|
|
24
|
+
onClick: () => {
|
|
25
|
+
if (closePopup)
|
|
26
|
+
close();
|
|
27
|
+
else
|
|
28
|
+
open();
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
const triggerNode = wrap.querySelector('button');
|
|
32
|
+
triggerNode?.setAttribute('aria-haspopup', 'menu');
|
|
33
|
+
const setActive = (id) => {
|
|
34
|
+
activeId = id;
|
|
35
|
+
highlightOption(popup, popup, uid, id);
|
|
36
|
+
};
|
|
37
|
+
const paintItems = () => {
|
|
38
|
+
clearNode(popup);
|
|
39
|
+
for (const item of props.items) {
|
|
40
|
+
if ('separator' in item) {
|
|
41
|
+
const line = el('div', 'oc-sdk-separator');
|
|
42
|
+
line.dataset.labeled = 'false';
|
|
43
|
+
popup.append(line);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
popup.append(createOption(uid, 'menuitem', item, {
|
|
47
|
+
hover: () => setActive(item.id),
|
|
48
|
+
pick: () => pick(item.id),
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
setActive(actions(props.items).some((item) => item.id === activeId) ? activeId : null);
|
|
52
|
+
};
|
|
53
|
+
const close = () => {
|
|
54
|
+
// Removing a focused row fires `focusout` synchronously from inside the disposer, and
|
|
55
|
+
// that handler calls `close` again. Clearing the slot first makes the re-entry a no-op
|
|
56
|
+
// instead of a second `popup.remove()` that throws before the pick reaches `onChange`.
|
|
57
|
+
const dispose = closePopup;
|
|
58
|
+
closePopup = null;
|
|
59
|
+
dispose?.();
|
|
60
|
+
activeId = null;
|
|
61
|
+
triggerNode?.setAttribute('aria-expanded', 'false');
|
|
62
|
+
};
|
|
63
|
+
const open = () => {
|
|
64
|
+
if (closePopup || !triggerNode) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
paintItems();
|
|
68
|
+
closePopup = openPopup(wrap, triggerNode, popup, close);
|
|
69
|
+
triggerNode.setAttribute('aria-expanded', 'true');
|
|
70
|
+
popup.focus();
|
|
71
|
+
};
|
|
72
|
+
const pick = (id) => {
|
|
73
|
+
close();
|
|
74
|
+
triggerNode?.focus();
|
|
75
|
+
props.onSelect(id);
|
|
76
|
+
};
|
|
77
|
+
const onPopupKey = (event) => {
|
|
78
|
+
const step = navigationKey(event);
|
|
79
|
+
if (step) {
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
setActive(moveListSelection(actions(props.items), activeId, step));
|
|
82
|
+
}
|
|
83
|
+
else if (event.key === 'Enter' || event.key === ' ') {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
if (activeId)
|
|
86
|
+
pick(activeId);
|
|
87
|
+
}
|
|
88
|
+
else if (event.key === 'Escape') {
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
close();
|
|
91
|
+
triggerNode?.focus();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const onFocusOut = (event) => {
|
|
95
|
+
if (closePopup && !(event.relatedTarget instanceof Node && wrap.contains(event.relatedTarget)))
|
|
96
|
+
close();
|
|
97
|
+
};
|
|
98
|
+
popup.addEventListener('keydown', onPopupKey);
|
|
99
|
+
wrap.addEventListener('focusout', onFocusOut);
|
|
100
|
+
return {
|
|
101
|
+
update: (next) => {
|
|
102
|
+
props = { ...props, ...next };
|
|
103
|
+
trigger.update({ label: props.label, variant: props.variant, size: props.size });
|
|
104
|
+
if (closePopup)
|
|
105
|
+
paintItems();
|
|
106
|
+
},
|
|
107
|
+
dispose: () => {
|
|
108
|
+
close();
|
|
109
|
+
popup.removeEventListener('keydown', onPopupKey);
|
|
110
|
+
wrap.removeEventListener('focusout', onFocusOut);
|
|
111
|
+
trigger.dispose();
|
|
112
|
+
wrap.remove();
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
};
|