@medeo-plugin/sdk 0.7.0 → 0.9.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/src/ui.ts ADDED
@@ -0,0 +1,102 @@
1
+ export { installStyles, uiStyles } from './ui/styles.js';
2
+
3
+ export interface Identified { testid: string }
4
+ export interface CardOptions extends Identified { title: string; description?: string }
5
+ export interface PillOptions extends Identified { label: string; tone?: 'neutral' | 'info' | 'error' }
6
+ export interface ToggleOptions extends Identified { label: string; checked: boolean; disabled?: boolean; onChange: (checked: boolean) => void }
7
+ export interface Segment<T extends string = string> { value: T; label: string; disabled?: boolean }
8
+ export interface SegmentedOptions<T extends string = string> extends Identified { label: string; options: readonly Segment<T>[]; value: T; disabled?: boolean; onChange: (value: T) => void }
9
+ export interface ListOptions extends Identified { label: string }
10
+ export type Child = Node | string | number | null | undefined | false;
11
+
12
+ function node<K extends keyof HTMLElementTagNameMap>(tag: K, className: string, testid?: string, text?: string): HTMLElementTagNameMap[K] {
13
+ const el = document.createElement(tag);
14
+ el.className = className;
15
+ if (testid !== undefined) el.dataset.testid = testid;
16
+ if (text !== undefined) el.textContent = text;
17
+ return el;
18
+ }
19
+ function append(el: HTMLElement, children: readonly Child[]): void {
20
+ for (const child of children) if (child !== null && child !== undefined && child !== false) el.append(typeof child === 'number' ? String(child) : child);
21
+ }
22
+ export function card(options: CardOptions, ...children: Child[]): HTMLElement {
23
+ const root = node('section', 'medeo-ui medeo-card', options.testid);
24
+ root.setAttribute('aria-label', options.title);
25
+ root.append(node('h2', '', `${options.testid}.title`, options.title));
26
+ if (options.description !== undefined) root.append(node('p', 'medeo-description', undefined, options.description));
27
+ append(root, children);
28
+ return root;
29
+ }
30
+ export function pill(options: PillOptions): HTMLSpanElement {
31
+ const root = node('span', 'medeo-ui medeo-pill', options.testid, options.label);
32
+ root.dataset.tone = options.tone ?? 'info';
33
+ return root;
34
+ }
35
+ /** Native checkbox keyboard behavior (Space), with switch semantics. */
36
+ export function toggle(options: ToggleOptions): HTMLLabelElement & { input: HTMLInputElement } {
37
+ const root = node('label', 'medeo-ui medeo-toggle', `${options.testid}.root`);
38
+ const input = node('input', '', options.testid);
39
+ input.type = 'checkbox'; input.setAttribute('role', 'switch');
40
+ input.checked = options.checked; input.disabled = options.disabled ?? false;
41
+ input.addEventListener('change', () => options.onChange(input.checked));
42
+ root.append(input, node('span', '', undefined, options.label));
43
+ return Object.assign(root, { input });
44
+ }
45
+ /** Shared radio keyboard navigation; disabled choices are skipped, edges wrap. */
46
+ export function segmentTarget<T extends string>(options: readonly Segment<T>[], index: number, key: string, rtl = false): number | undefined {
47
+ const enabled = options.map((o, i) => o.disabled ? -1 : i).filter(i => i >= 0);
48
+ if (!enabled.length) return undefined;
49
+ if (key === 'Home') return enabled[0];
50
+ if (key === 'End') return enabled[enabled.length - 1];
51
+ let direction = key === 'ArrowDown' ? 1 : key === 'ArrowUp' ? -1 : key === 'ArrowRight' ? (rtl ? -1 : 1) : key === 'ArrowLeft' ? (rtl ? 1 : -1) : 0;
52
+ if (!direction) return undefined;
53
+ return enabled[(enabled.indexOf(index) + direction + enabled.length) % enabled.length];
54
+ }
55
+ export function segmentTabIndex<T extends string>(options: readonly Segment<T>[], value: T, index: number): number {
56
+ let active = options.findIndex(o => o.value === value && !o.disabled);
57
+ if (active < 0) active = options.findIndex(o => !o.disabled);
58
+ return index === active ? 0 : -1;
59
+ }
60
+ export function segmented<T extends string>(options: SegmentedOptions<T>): HTMLDivElement & { setValue(value: T): void; setDisabled(disabled: boolean): void } {
61
+ const root = node('div', 'medeo-ui medeo-segmented', options.testid);
62
+ root.setAttribute('role', 'radiogroup'); root.setAttribute('aria-label', options.label);
63
+ let value = options.value, disabled = options.disabled ?? false;
64
+ const buttons = options.options.map((option, index) => {
65
+ const button = node('button', '', `${options.testid}.${index}`, option.label);
66
+ button.type = 'button'; button.setAttribute('role', 'radio');
67
+ button.addEventListener('click', () => {
68
+ if (disabled || option.disabled) return;
69
+ value = option.value; render(); options.onChange(value);
70
+ });
71
+ button.addEventListener('keydown', event => {
72
+ if (disabled) return;
73
+ const target = segmentTarget(options.options, index, event.key, getComputedStyle(root).direction === 'rtl');
74
+ if (target === undefined) return;
75
+ event.preventDefault(); buttons[target].focus(); buttons[target].click();
76
+ });
77
+ return button;
78
+ });
79
+ function render() {
80
+ buttons.forEach((button, index) => {
81
+ button.disabled = disabled || !!options.options[index].disabled;
82
+ button.tabIndex = disabled ? -1 : segmentTabIndex(options.options, value, index);
83
+ button.setAttribute('aria-checked', String(options.options[index].value === value));
84
+ });
85
+ }
86
+ root.append(...buttons); render();
87
+ return Object.assign(root, { setValue(next: T) { value = next; render(); }, setDisabled(next: boolean) { disabled = next; render(); } });
88
+ }
89
+ /** Each child becomes one semantic list item; caller owns empty-state wording. */
90
+ export function list(options: ListOptions, ...items: Child[]): HTMLUListElement {
91
+ const root = node('ul', 'medeo-ui medeo-list', options.testid);
92
+ root.setAttribute('aria-label', options.label);
93
+ items.forEach((item, index) => { const li = node('li', '', `${options.testid}.${index}`); append(li, [item]); root.append(li); });
94
+ return root;
95
+ }
96
+ export function emptyState(options: CardOptions): HTMLElement {
97
+ const root = node('section', 'medeo-ui medeo-empty', options.testid);
98
+ root.setAttribute('aria-label', options.title);
99
+ root.append(node('h2', '', undefined, options.title));
100
+ if (options.description !== undefined) root.append(node('p', 'medeo-description', undefined, options.description));
101
+ return root;
102
+ }