@haiyue/ui 0.1.2 → 0.2.2
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/CHANGELOG.md +43 -0
- package/LICENSE +27 -0
- package/README.md +643 -2
- package/dist/adapters/web/controls.d.ts +19 -0
- package/dist/adapters/web/controls.js +307 -0
- package/dist/adapters/web/dom.d.ts +10 -0
- package/dist/adapters/web/dom.js +98 -0
- package/dist/adapters/web/index.d.ts +3 -0
- package/dist/adapters/web/index.js +3 -0
- package/dist/adapters/web/renderer.d.ts +18 -0
- package/dist/adapters/web/renderer.js +553 -0
- package/dist/adapters/web/styles.d.ts +3 -0
- package/dist/adapters/web/styles.js +83 -0
- package/dist/adapters/web/types.d.ts +46 -0
- package/dist/adapters/web/types.js +2 -0
- package/dist/affix.d.ts +35 -0
- package/dist/affix.js +137 -0
- package/dist/badge.d.ts +45 -0
- package/dist/badge.js +168 -0
- package/dist/breadcrumb.d.ts +37 -0
- package/dist/breadcrumb.js +210 -0
- package/dist/button-group.d.ts +12 -0
- package/dist/button-group.js +72 -0
- package/dist/button.d.ts +15 -1
- package/dist/button.js +101 -12
- package/dist/carousel.d.ts +92 -0
- package/dist/carousel.js +509 -0
- package/dist/cascader.d.ts +65 -0
- package/dist/cascader.js +479 -0
- package/dist/collapse.d.ts +68 -0
- package/dist/collapse.js +348 -0
- package/dist/core/actions.d.ts +32 -0
- package/dist/core/actions.js +145 -0
- package/dist/core/data.d.ts +4 -0
- package/dist/core/data.js +27 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.js +6 -0
- package/dist/core/platform-view.d.ts +17 -0
- package/dist/core/platform-view.js +84 -0
- package/dist/core/selection.d.ts +14 -0
- package/dist/core/selection.js +93 -0
- package/dist/core/text.d.ts +31 -0
- package/dist/core/text.js +154 -0
- package/dist/core/types.d.ts +269 -0
- package/dist/core/types.js +2 -0
- package/dist/core/validation.d.ts +24 -0
- package/dist/core/validation.js +243 -0
- package/dist/date-picker.d.ts +60 -0
- package/dist/date-picker.js +355 -0
- package/dist/dialog.d.ts +4 -0
- package/dist/dialog.js +23 -5
- package/dist/divider.d.ts +33 -0
- package/dist/divider.js +113 -0
- package/dist/draggable.d.ts +78 -0
- package/dist/draggable.js +255 -0
- package/dist/index.d.ts +53 -1
- package/dist/index.js +78 -0
- package/dist/input.d.ts +30 -1
- package/dist/input.js +166 -54
- package/dist/internal/qrcodegen.d.ts +94 -0
- package/dist/internal/qrcodegen.js +856 -0
- package/dist/masonry.d.ts +36 -0
- package/dist/masonry.js +171 -0
- package/dist/modal.d.ts +73 -0
- package/dist/modal.js +361 -0
- package/dist/portable/icons.d.ts +5 -0
- package/dist/portable/icons.js +24 -0
- package/dist/portable/index.d.ts +7 -0
- package/dist/portable/index.js +5 -0
- package/dist/portable/page.d.ts +38 -0
- package/dist/portable/page.js +361 -0
- package/dist/portable/responsive.d.ts +20 -0
- package/dist/portable/responsive.js +15 -0
- package/dist/portable/theme.d.ts +24 -0
- package/dist/portable/theme.js +60 -0
- package/dist/portable/tree.d.ts +10 -0
- package/dist/portable/tree.js +96 -0
- package/dist/qr-code.d.ts +62 -0
- package/dist/qr-code.js +224 -0
- package/dist/range.d.ts +3 -0
- package/dist/range.js +16 -5
- package/dist/rate.d.ts +46 -0
- package/dist/rate.js +193 -0
- package/dist/resizable.d.ts +97 -0
- package/dist/resizable.js +426 -0
- package/dist/skeleton.d.ts +41 -0
- package/dist/skeleton.js +141 -0
- package/dist/steps.d.ts +45 -0
- package/dist/steps.js +203 -0
- package/dist/table.d.ts +106 -0
- package/dist/table.js +689 -0
- package/dist/tabs.d.ts +3 -0
- package/dist/tabs.js +11 -1
- package/dist/tag.d.ts +55 -0
- package/dist/tag.js +225 -0
- package/dist/time-picker.d.ts +62 -0
- package/dist/time-picker.js +298 -0
- package/dist/timeline.d.ts +48 -0
- package/dist/timeline.js +201 -0
- package/dist/toast.d.ts +55 -0
- package/dist/toast.js +224 -0
- package/dist/tour.d.ts +89 -0
- package/dist/tour.js +306 -0
- package/dist/virtual-keyboard.d.ts +95 -0
- package/dist/virtual-keyboard.js +432 -0
- package/dist/virtual-mouse.d.ts +82 -0
- package/dist/virtual-mouse.js +358 -0
- package/dist/watermark.d.ts +44 -0
- package/dist/watermark.js +148 -0
- package/package.json +132 -4
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Public vector geometry. 24 × 24, 1.8 unit round strokes, no font or platform assets. */
|
|
2
|
+
export const HY_ICONS = Object.freeze({
|
|
3
|
+
save: [[[4, 3], [17, 3], [21, 7], [21, 21], [3, 21], [3, 3], [4, 3]], [[7, 3], [7, 9], [17, 9], [17, 3]], [[7, 21], [7, 14], [17, 14], [17, 21]]],
|
|
4
|
+
export: [[[12, 15], [12, 3]], [[7, 8], [12, 3], [17, 8]], [[4, 14], [4, 21], [20, 21], [20, 14]]],
|
|
5
|
+
play: [[[7, 4], [20, 12], [7, 20], [7, 4]]], pause: [[[8, 4], [8, 20]], [[16, 4], [16, 20]]],
|
|
6
|
+
reset: [[[4, 9], [6, 5], [10, 3], [15, 3], [19, 6], [21, 10], [21, 14], [18, 19], [14, 21], [9, 20], [6, 17]], [[4, 3], [4, 9], [10, 9]]],
|
|
7
|
+
step: [[[5, 5], [15, 12], [5, 19], [5, 5]], [[19, 5], [19, 19]]],
|
|
8
|
+
settings: [[[19.438, 10.007], [21.848, 10.264], [21.848, 13.736], [19.438, 13.993], [18.668, 15.85], [20.192, 17.736], [17.736, 20.192], [15.85, 18.668], [13.993, 19.438], [13.736, 21.848], [10.264, 21.848], [10.007, 19.438], [8.15, 18.668], [6.264, 20.192], [3.808, 17.736], [5.332, 15.85], [4.562, 13.993], [2.152, 13.736], [2.152, 10.264], [4.562, 10.007], [5.332, 8.15], [3.808, 6.264], [6.264, 3.808], [8.15, 5.332], [10.007, 4.562], [10.264, 2.152], [13.736, 2.152], [13.993, 4.562], [15.85, 5.332], [17.736, 3.808], [20.192, 6.264], [18.668, 8.15], [19.438, 10.007]], [[15.0, 12.0], [14.942, 12.585], [14.772, 13.148], [14.494, 13.667], [14.121, 14.121], [13.667, 14.494], [13.148, 14.772], [12.585, 14.942], [12.0, 15.0], [11.415, 14.942], [10.852, 14.772], [10.333, 14.494], [9.879, 14.121], [9.506, 13.667], [9.228, 13.148], [9.058, 12.585], [9.0, 12.0], [9.058, 11.415], [9.228, 10.852], [9.506, 10.333], [9.879, 9.879], [10.333, 9.506], [10.852, 9.228], [11.415, 9.058], [12.0, 9.0], [12.585, 9.058], [13.148, 9.228], [13.667, 9.506], [14.121, 9.879], [14.494, 10.333], [14.772, 10.852], [14.942, 11.415], [15.0, 12.0]]],
|
|
9
|
+
back: [[[14, 4], [6, 12], [14, 20]], [[6, 12], [21, 12]]], chevronDown: [[[6, 9], [12, 15], [18, 9]]],
|
|
10
|
+
check: [[[4, 12], [9, 17], [20, 6]]], close: [[[6, 6], [18, 18]], [[18, 6], [6, 18]]], add: [[[12, 4], [12, 20]], [[4, 12], [20, 12]]],
|
|
11
|
+
});
|
|
12
|
+
for (const paths of Object.values(HY_ICONS)) {
|
|
13
|
+
for (const points of paths) {
|
|
14
|
+
for (const point of points)
|
|
15
|
+
Object.freeze(point);
|
|
16
|
+
Object.freeze(points);
|
|
17
|
+
}
|
|
18
|
+
Object.freeze(paths);
|
|
19
|
+
}
|
|
20
|
+
/** Safe SVG built only from the library's numeric paths. Color inherits from its host. */
|
|
21
|
+
export function hyIconSvg(name) {
|
|
22
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${HY_ICONS[name].map(points => `<polyline points="${points.map(p => p.join(',')).join(' ')}"/>`).join('')}</svg>`;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=icons.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { HYPage } from './page.js';
|
|
2
|
+
export type { HYPageOptions, HYPageStatus, HYBinding } from './page.js';
|
|
3
|
+
export { HY_THEME_TOKENS, hyNativeTheme, hyThemeVariables } from './theme.js';
|
|
4
|
+
export type { HYThemeVariables } from './theme.js';
|
|
5
|
+
export { HY_ICONS, hyIconSvg } from './icons.js';
|
|
6
|
+
export { hyResponsiveGrid, hyResponsiveWorkspace } from './responsive.js';
|
|
7
|
+
export type { HYResponsiveGridOptions, HYResponsiveWorkspaceOptions } from './responsive.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { HYPage } from './page.js';
|
|
2
|
+
export { HY_THEME_TOKENS, hyNativeTheme, hyThemeVariables } from './theme.js';
|
|
3
|
+
export { HY_ICONS, hyIconSvg } from './icons.js';
|
|
4
|
+
export { hyResponsiveGrid, hyResponsiveWorkspace } from './responsive.js';
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { HYCapabilities, HYComponentKind, HYComponentProps, HYDispose, HYNode, HYReason, HYRenderer, HYStore, HYUIEvent } from '../core/types.js';
|
|
2
|
+
export type HYPageStatus = 'new' | 'mounted' | 'suspended' | 'faulted' | 'disposed';
|
|
3
|
+
export interface HYPageOptions {
|
|
4
|
+
readonly onEvent?: (event: HYUIEvent) => void;
|
|
5
|
+
/** Scoped key from hyKey(). If unavailable the root receives focus. */
|
|
6
|
+
readonly defaultFocus?: string;
|
|
7
|
+
}
|
|
8
|
+
export type HYBinding<S> = {
|
|
9
|
+
[K in HYComponentKind]: {
|
|
10
|
+
readonly key: string;
|
|
11
|
+
readonly kind: K;
|
|
12
|
+
readonly select: (state: Readonly<S>) => Readonly<HYComponentProps[K]>;
|
|
13
|
+
readonly equal?: (a: Readonly<HYComponentProps[K]>, b: Readonly<HYComponentProps[K]>) => boolean;
|
|
14
|
+
};
|
|
15
|
+
}[HYComponentKind];
|
|
16
|
+
/** A synchronous, bounded page reconciler. It owns bindings/rendering, never the application store. */
|
|
17
|
+
export declare class HYPage {
|
|
18
|
+
#private;
|
|
19
|
+
private readonly options;
|
|
20
|
+
constructor(renderer: HYRenderer, options?: HYPageOptions);
|
|
21
|
+
get status(): HYPageStatus;
|
|
22
|
+
get revision(): number;
|
|
23
|
+
get capabilities(): HYCapabilities;
|
|
24
|
+
mount(root: HYNode): void;
|
|
25
|
+
render(root: HYNode): void;
|
|
26
|
+
setProps<K extends HYComponentKind>(key: string, kind: K, props: HYComponentProps[K]): void;
|
|
27
|
+
transaction(run: () => void): void;
|
|
28
|
+
/** One store subscription: a store transaction produces one final page batch across all bindings. */
|
|
29
|
+
bind<S, A>(store: HYStore<S, A>, bindings: readonly HYBinding<S>[]): HYDispose;
|
|
30
|
+
focus(key: string): boolean;
|
|
31
|
+
/** The shell chooses this single system-back owner. A modal consumes even a non-dismissible back. */
|
|
32
|
+
back(reason?: HYReason): boolean;
|
|
33
|
+
suspend(): void;
|
|
34
|
+
resume(): void;
|
|
35
|
+
/** Replaces the failed backend, preserving latest requested data and existing store bindings. */
|
|
36
|
+
rebuild(renderer: HYRenderer): void;
|
|
37
|
+
dispose(): void;
|
|
38
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { HYUIError, hyValidateCapabilities, hyValidateNode, hyValidateProps, snapshotValidatedProps } from '../core/validation.js';
|
|
2
|
+
import { equalData, snapshot } from '../core/data.js';
|
|
3
|
+
import { diff, flatten } from './tree.js';
|
|
4
|
+
const interactive = new Set(['Button', 'Toggle', 'Select', 'Tabs', 'TextInput', 'TextArea', 'Slider', 'PlatformView']);
|
|
5
|
+
/** A synchronous, bounded page reconciler. It owns bindings/rendering, never the application store. */
|
|
6
|
+
export class HYPage {
|
|
7
|
+
options;
|
|
8
|
+
#renderer;
|
|
9
|
+
#capabilities;
|
|
10
|
+
#status = 'new';
|
|
11
|
+
#revision = 0;
|
|
12
|
+
#epoch = 0;
|
|
13
|
+
#depth = 0;
|
|
14
|
+
#committing = false;
|
|
15
|
+
#desired = new Map();
|
|
16
|
+
#rendered = new Map();
|
|
17
|
+
// Undefined means topology may have changed; otherwise only these props
|
|
18
|
+
// need comparison. Keep tree order deterministic across grouped updates.
|
|
19
|
+
#updates;
|
|
20
|
+
#order = new Map();
|
|
21
|
+
#subscriptions = new Set();
|
|
22
|
+
#focused;
|
|
23
|
+
#boundStores = new Set();
|
|
24
|
+
#openers = new Map();
|
|
25
|
+
#cancelTransaction = 0;
|
|
26
|
+
constructor(renderer, options = {}) {
|
|
27
|
+
this.options = options;
|
|
28
|
+
hyValidateCapabilities(renderer.capabilities);
|
|
29
|
+
this.#renderer = renderer;
|
|
30
|
+
this.#capabilities = snapshot(renderer.capabilities);
|
|
31
|
+
}
|
|
32
|
+
get status() { return this.#status; }
|
|
33
|
+
get revision() { return this.#revision; }
|
|
34
|
+
get capabilities() { return this.#capabilities; }
|
|
35
|
+
#assertMutable() {
|
|
36
|
+
if (this.#status === 'disposed' || this.#committing)
|
|
37
|
+
throw new HYUIError('lifecycle', 'Page disposed or inside renderer commit');
|
|
38
|
+
}
|
|
39
|
+
mount(root) {
|
|
40
|
+
if (this.#status !== 'new')
|
|
41
|
+
throw new HYUIError('lifecycle', 'Page may only mount once');
|
|
42
|
+
this.#desired = flatten(hyValidateNode(root, this.#capabilities));
|
|
43
|
+
this.#mountRenderer();
|
|
44
|
+
this.#flush();
|
|
45
|
+
}
|
|
46
|
+
#mountRenderer() {
|
|
47
|
+
const epoch = ++this.#epoch;
|
|
48
|
+
this.#committing = true;
|
|
49
|
+
try {
|
|
50
|
+
this.#renderer.mount(event => { if (epoch === this.#epoch)
|
|
51
|
+
this.#event(event); });
|
|
52
|
+
this.#status = 'mounted';
|
|
53
|
+
}
|
|
54
|
+
catch (cause) {
|
|
55
|
+
this.#fault(cause, 'mount');
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
this.#committing = false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
render(root) {
|
|
62
|
+
this.#assertMutable();
|
|
63
|
+
if (this.#status === 'new')
|
|
64
|
+
throw new HYUIError('lifecycle', 'Mount the page before render');
|
|
65
|
+
this.#desired = flatten(hyValidateNode(root, this.#capabilities));
|
|
66
|
+
this.#updates = undefined;
|
|
67
|
+
this.#flush();
|
|
68
|
+
}
|
|
69
|
+
setProps(key, kind, props) {
|
|
70
|
+
this.#assertMutable();
|
|
71
|
+
const entry = this.#desired.get(key);
|
|
72
|
+
if (!entry || entry.node.kind !== kind)
|
|
73
|
+
throw new HYUIError('invalid-props', 'Binding target missing or kind changed', { key });
|
|
74
|
+
const context = { key, ...(entry.node.source ? { source: entry.node.source } : {}) };
|
|
75
|
+
const accepted = snapshotValidatedProps(kind, props, context, this.#capabilities);
|
|
76
|
+
if (equalData(entry.node.props, props))
|
|
77
|
+
return;
|
|
78
|
+
const next = new Map(this.#desired);
|
|
79
|
+
next.set(key, { ...entry, node: Object.freeze({ ...entry.node, props: accepted }) });
|
|
80
|
+
this.#desired = next;
|
|
81
|
+
this.#updates?.add(key);
|
|
82
|
+
this.#flush();
|
|
83
|
+
}
|
|
84
|
+
transaction(run) {
|
|
85
|
+
this.#assertMutable();
|
|
86
|
+
const before = this.#desired, updates = this.#updates && new Set(this.#updates);
|
|
87
|
+
this.#depth++;
|
|
88
|
+
try {
|
|
89
|
+
const result = run();
|
|
90
|
+
if (result && typeof result.then === 'function')
|
|
91
|
+
throw new HYUIError('lifecycle', 'Page transactions must be synchronous');
|
|
92
|
+
}
|
|
93
|
+
catch (cause) {
|
|
94
|
+
this.#desired = before;
|
|
95
|
+
this.#updates = updates;
|
|
96
|
+
throw cause;
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
this.#depth--;
|
|
100
|
+
}
|
|
101
|
+
this.#flush();
|
|
102
|
+
}
|
|
103
|
+
/** One store subscription: a store transaction produces one final page batch across all bindings. */
|
|
104
|
+
bind(store, bindings) {
|
|
105
|
+
this.#assertMutable();
|
|
106
|
+
if (this.#boundStores.has(store))
|
|
107
|
+
throw new HYUIError('lifecycle', 'Group all bindings for one store in a single bind call');
|
|
108
|
+
const definitions = [...bindings];
|
|
109
|
+
if (new Set(definitions.map(b => b.key)).size !== definitions.length)
|
|
110
|
+
throw new HYUIError('duplicate-key', 'Duplicate binding target');
|
|
111
|
+
const select = (state) => definitions.map(b => b.select(state));
|
|
112
|
+
const equal = (a, b) => a.every((p, i) => (definitions[i].equal ?? equalData)(p, b[i]));
|
|
113
|
+
let applied = [];
|
|
114
|
+
const apply = (props) => {
|
|
115
|
+
if (this.#status === 'disposed')
|
|
116
|
+
return;
|
|
117
|
+
this.transaction(() => definitions.forEach((binding, i) => {
|
|
118
|
+
const node = this.#desired.get(binding.key)?.node;
|
|
119
|
+
// A grouped subscription does not make every binding dirty. Reuse a
|
|
120
|
+
// previously validated result only while its target is still unchanged.
|
|
121
|
+
// External render/setProps and failed transactions invalidate this path.
|
|
122
|
+
if (node === applied[i] && node && equalData(node.props, props[i]))
|
|
123
|
+
return;
|
|
124
|
+
this.setProps(binding.key, binding.kind, props[i]);
|
|
125
|
+
}));
|
|
126
|
+
applied = definitions.map(binding => this.#desired.get(binding.key)?.node);
|
|
127
|
+
};
|
|
128
|
+
apply(select(store.getSnapshot()));
|
|
129
|
+
let live = true;
|
|
130
|
+
const unsubscribe = store.subscribe(select, values => { if (live)
|
|
131
|
+
apply(values); }, equal);
|
|
132
|
+
const dispose = () => { if (!live)
|
|
133
|
+
return; live = false; this.#subscriptions.delete(dispose); this.#boundStores.delete(store); unsubscribe(); };
|
|
134
|
+
this.#boundStores.add(store);
|
|
135
|
+
this.#subscriptions.add(dispose);
|
|
136
|
+
// Also captures synchronous changes caused by mount/update hooks before subscription began.
|
|
137
|
+
try {
|
|
138
|
+
apply(select(store.getSnapshot()));
|
|
139
|
+
}
|
|
140
|
+
catch (cause) {
|
|
141
|
+
dispose();
|
|
142
|
+
throw cause;
|
|
143
|
+
}
|
|
144
|
+
return dispose;
|
|
145
|
+
}
|
|
146
|
+
#fault(cause, operation, key) {
|
|
147
|
+
this.#status = 'faulted';
|
|
148
|
+
const context = cause instanceof HYUIError ? cause.context : {};
|
|
149
|
+
throw new HYUIError('renderer', 'Renderer failed; rebuild required', {
|
|
150
|
+
key: key ?? this.#desired.keys().next().value ?? '<root>', operation, ...context,
|
|
151
|
+
revision: operation === 'commit' || operation === 'mount' ? this.#revision + 1 : this.#revision,
|
|
152
|
+
}, cause);
|
|
153
|
+
}
|
|
154
|
+
#flush() {
|
|
155
|
+
if (this.#depth || this.#status !== 'mounted' || this.#committing)
|
|
156
|
+
return;
|
|
157
|
+
const next = this.#desired, previous = this.#rendered;
|
|
158
|
+
if (next === previous)
|
|
159
|
+
return;
|
|
160
|
+
const topology = this.#updates === undefined;
|
|
161
|
+
const mutations = topology ? diff(previous, next)
|
|
162
|
+
: Object.freeze([...this.#updates].sort((a, b) => this.#order.get(a) - this.#order.get(b)).flatMap(key => {
|
|
163
|
+
const node = next.get(key).node;
|
|
164
|
+
return equalData(previous.get(key).node.props, node.props) ? [] : [Object.freeze({ type: 'update', node })];
|
|
165
|
+
}));
|
|
166
|
+
const focusChanged = topology || mutations.some(m => {
|
|
167
|
+
if (m.type !== 'update')
|
|
168
|
+
return true;
|
|
169
|
+
const a = previous.get(m.node.key).node.props, b = m.node.props;
|
|
170
|
+
return a.visible !== b.visible || ('disabled' in a && a.disabled) !== ('disabled' in b && b.disabled)
|
|
171
|
+
|| ('busy' in a && a.busy) !== ('busy' in b && b.busy)
|
|
172
|
+
|| (m.node.kind === 'Dialog' && ('open' in a && a.open) !== m.node.props.open)
|
|
173
|
+
|| (m.node.kind === 'Tabs' && ('value' in a && a.value) !== m.node.props.value);
|
|
174
|
+
});
|
|
175
|
+
if (!mutations.length) {
|
|
176
|
+
this.#rendered = next;
|
|
177
|
+
this.#updates = new Set();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
this.#committing = true;
|
|
181
|
+
try {
|
|
182
|
+
this.#renderer.commit(this.#revision + 1, mutations);
|
|
183
|
+
}
|
|
184
|
+
catch (cause) {
|
|
185
|
+
this.#fault(cause, 'commit');
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
this.#committing = false;
|
|
189
|
+
}
|
|
190
|
+
this.#rendered = next;
|
|
191
|
+
this.#revision++;
|
|
192
|
+
this.#updates = new Set();
|
|
193
|
+
if (topology)
|
|
194
|
+
this.#order = new Map([...next.keys()].map((key, index) => [key, index]));
|
|
195
|
+
if (focusChanged)
|
|
196
|
+
this.#restoreFocus(previous);
|
|
197
|
+
}
|
|
198
|
+
#available(key, tree = this.#rendered) {
|
|
199
|
+
if (!key)
|
|
200
|
+
return false;
|
|
201
|
+
let entry = tree.get(key);
|
|
202
|
+
if (!entry)
|
|
203
|
+
return false;
|
|
204
|
+
while (entry) {
|
|
205
|
+
const p = entry.node.props;
|
|
206
|
+
if (p.visible === false || ('disabled' in p && p.disabled) || ('busy' in p && p.busy) || (entry.node.kind === 'Dialog' && !entry.node.props.open))
|
|
207
|
+
return false;
|
|
208
|
+
const parent = entry.parent === null ? undefined : tree.get(entry.parent);
|
|
209
|
+
if (parent?.node.kind === 'Tabs' && entry.slot !== parent.node.props.value)
|
|
210
|
+
return false;
|
|
211
|
+
entry = parent;
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
#dialogs(tree) {
|
|
216
|
+
return [...tree].filter(([key, entry]) => entry.node.kind === 'Dialog' && entry.node.props.open && this.#available(key, tree)).map(([key]) => key);
|
|
217
|
+
}
|
|
218
|
+
#inside(key, parent) { return key === parent || key.startsWith(`${parent}/`); }
|
|
219
|
+
#dialogTarget(key) {
|
|
220
|
+
return [...this.#rendered].find(([id, entry]) => this.#inside(id, key) && interactive.has(entry.node.kind) && this.#available(id))?.[0] ?? key;
|
|
221
|
+
}
|
|
222
|
+
#restoreFocus(previous) {
|
|
223
|
+
const oldDialogs = this.#dialogs(previous), dialogs = this.#dialogs(this.#rendered);
|
|
224
|
+
const newDialogs = dialogs.filter(key => !oldDialogs.includes(key));
|
|
225
|
+
for (const key of newDialogs)
|
|
226
|
+
this.#openers.set(key, this.#focused);
|
|
227
|
+
const top = dialogs.at(-1);
|
|
228
|
+
const closed = oldDialogs.filter(key => !dialogs.includes(key));
|
|
229
|
+
if (newDialogs.length || (top && (!this.#focused || !this.#inside(this.#focused, top))))
|
|
230
|
+
this.focus(this.#dialogTarget(top));
|
|
231
|
+
else if (this.#focused && !this.#available(this.#focused)) {
|
|
232
|
+
const opener = closed.slice().reverse().map(key => this.#openers.get(key)).find(key => this.#available(key));
|
|
233
|
+
const target = top ? this.#dialogTarget(top) : opener ?? (this.#available(this.options.defaultFocus) ? this.options.defaultFocus : this.#rendered.keys().next().value);
|
|
234
|
+
if (target)
|
|
235
|
+
this.focus(target);
|
|
236
|
+
else
|
|
237
|
+
this.#focused = undefined;
|
|
238
|
+
}
|
|
239
|
+
for (const key of closed)
|
|
240
|
+
this.#openers.delete(key);
|
|
241
|
+
}
|
|
242
|
+
focus(key) {
|
|
243
|
+
if (this.#status !== 'mounted' || this.#committing || !this.#available(key))
|
|
244
|
+
return false;
|
|
245
|
+
const top = this.#dialogs(this.#rendered).at(-1);
|
|
246
|
+
if (top && !this.#inside(key, top))
|
|
247
|
+
return false;
|
|
248
|
+
this.#committing = true;
|
|
249
|
+
try {
|
|
250
|
+
this.#renderer.focus(key);
|
|
251
|
+
this.#focused = key;
|
|
252
|
+
}
|
|
253
|
+
catch (cause) {
|
|
254
|
+
this.#fault(cause, 'focus', key);
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
this.#committing = false;
|
|
258
|
+
}
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
#event(event) {
|
|
262
|
+
if (this.#committing || this.#status !== 'mounted' || !this.#available(event.key))
|
|
263
|
+
return;
|
|
264
|
+
const top = this.#dialogs(this.#rendered).at(-1);
|
|
265
|
+
if (top && !this.#inside(event.key, top))
|
|
266
|
+
return;
|
|
267
|
+
if (event.type === 'focus')
|
|
268
|
+
this.#focused = event.key;
|
|
269
|
+
this.options.onEvent?.(Object.freeze({ ...event }));
|
|
270
|
+
}
|
|
271
|
+
/** The shell chooses this single system-back owner. A modal consumes even a non-dismissible back. */
|
|
272
|
+
back(reason = 'keyboard') {
|
|
273
|
+
if (this.#status !== 'mounted' || this.#committing)
|
|
274
|
+
return false;
|
|
275
|
+
const key = this.#dialogs(this.#rendered).at(-1);
|
|
276
|
+
if (!key)
|
|
277
|
+
return false;
|
|
278
|
+
const node = this.#rendered.get(key).node;
|
|
279
|
+
if (node.kind === 'Dialog' && node.props.dismissible !== false)
|
|
280
|
+
this.#event({ key, type: 'cancel', reason, transaction: ++this.#cancelTransaction });
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
suspend() {
|
|
284
|
+
if (this.#status !== 'mounted' || this.#committing)
|
|
285
|
+
return;
|
|
286
|
+
this.#status = 'suspended';
|
|
287
|
+
try {
|
|
288
|
+
this.#renderer.suspend();
|
|
289
|
+
}
|
|
290
|
+
catch (cause) {
|
|
291
|
+
this.#fault(cause, 'suspend');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
resume() {
|
|
295
|
+
if (this.#status !== 'suspended')
|
|
296
|
+
return;
|
|
297
|
+
try {
|
|
298
|
+
this.#renderer.resume();
|
|
299
|
+
}
|
|
300
|
+
catch (cause) {
|
|
301
|
+
this.#fault(cause, 'resume');
|
|
302
|
+
}
|
|
303
|
+
this.#status = 'mounted';
|
|
304
|
+
this.#flush();
|
|
305
|
+
}
|
|
306
|
+
/** Replaces the failed backend, preserving latest requested data and existing store bindings. */
|
|
307
|
+
rebuild(renderer) {
|
|
308
|
+
if (this.#status !== 'faulted')
|
|
309
|
+
throw new HYUIError('lifecycle', 'Only a faulted page may rebuild');
|
|
310
|
+
if (renderer === this.#renderer)
|
|
311
|
+
throw new HYUIError('lifecycle', 'Rebuild needs a fresh renderer');
|
|
312
|
+
hyValidateCapabilities(renderer.capabilities);
|
|
313
|
+
for (const [key, entry] of this.#desired) {
|
|
314
|
+
hyValidateProps(entry.node.kind, entry.node.props, { key }, renderer.capabilities);
|
|
315
|
+
for (const feature of entry.node.requires ?? [])
|
|
316
|
+
if (!renderer.capabilities.features.includes(feature))
|
|
317
|
+
throw new HYUIError('capability', `Missing capability ${feature}`, { key });
|
|
318
|
+
}
|
|
319
|
+
++this.#epoch;
|
|
320
|
+
this.#renderer.dispose();
|
|
321
|
+
this.#renderer = renderer;
|
|
322
|
+
this.#capabilities = snapshot(renderer.capabilities);
|
|
323
|
+
this.#rendered = new Map();
|
|
324
|
+
this.#updates = undefined;
|
|
325
|
+
this.#order.clear();
|
|
326
|
+
this.#focused = undefined;
|
|
327
|
+
this.#openers.clear();
|
|
328
|
+
this.#mountRenderer();
|
|
329
|
+
this.#flush();
|
|
330
|
+
}
|
|
331
|
+
dispose() {
|
|
332
|
+
if (this.#status === 'disposed')
|
|
333
|
+
return;
|
|
334
|
+
this.#status = 'disposed';
|
|
335
|
+
++this.#epoch;
|
|
336
|
+
const errors = [];
|
|
337
|
+
for (const unsubscribe of this.#subscriptions)
|
|
338
|
+
try {
|
|
339
|
+
unsubscribe();
|
|
340
|
+
}
|
|
341
|
+
catch (cause) {
|
|
342
|
+
errors.push(cause);
|
|
343
|
+
}
|
|
344
|
+
this.#subscriptions.clear();
|
|
345
|
+
try {
|
|
346
|
+
this.#renderer.dispose();
|
|
347
|
+
}
|
|
348
|
+
catch (cause) {
|
|
349
|
+
errors.push(cause);
|
|
350
|
+
}
|
|
351
|
+
this.#updates = undefined;
|
|
352
|
+
this.#order.clear();
|
|
353
|
+
this.#desired = new Map();
|
|
354
|
+
this.#rendered = new Map();
|
|
355
|
+
this.#openers.clear();
|
|
356
|
+
this.#focused = undefined;
|
|
357
|
+
if (errors.length)
|
|
358
|
+
throw new AggregateError(errors, 'Page cleanup failed');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
//# sourceMappingURL=page.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Platform-independent logical-pixel layout decisions. */
|
|
2
|
+
export interface HYResponsiveGridOptions {
|
|
3
|
+
readonly minCardWidth?: number;
|
|
4
|
+
readonly maxColumns?: number;
|
|
5
|
+
readonly gap?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function hyResponsiveGrid(width: number, options?: HYResponsiveGridOptions): Readonly<{
|
|
8
|
+
columns: number;
|
|
9
|
+
gap: number;
|
|
10
|
+
cardWidth: number;
|
|
11
|
+
}>;
|
|
12
|
+
export interface HYResponsiveWorkspaceOptions {
|
|
13
|
+
readonly minPaneWidth?: number;
|
|
14
|
+
readonly gap?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function hyResponsiveWorkspace(width: number, height: number, options?: HYResponsiveWorkspaceOptions): Readonly<{
|
|
17
|
+
split: boolean;
|
|
18
|
+
gap: number;
|
|
19
|
+
referenceHeight: number;
|
|
20
|
+
}>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function hyResponsiveGrid(width, options = {}) {
|
|
2
|
+
const { minCardWidth = 260, maxColumns = 4, gap = 16 } = options;
|
|
3
|
+
if (!Number.isFinite(width) || width < 0 || !Number.isFinite(minCardWidth) || minCardWidth <= 0 || !Number.isInteger(maxColumns) || maxColumns < 1 || !Number.isFinite(gap) || gap < 0)
|
|
4
|
+
throw new RangeError('Invalid responsive grid dimensions');
|
|
5
|
+
const columns = Math.max(1, Math.min(maxColumns, Math.floor((width + gap) / (minCardWidth + gap))));
|
|
6
|
+
return Object.freeze({ columns, gap, cardWidth: Math.max(0, (width - gap * (columns - 1)) / columns) });
|
|
7
|
+
}
|
|
8
|
+
export function hyResponsiveWorkspace(width, height, options = {}) {
|
|
9
|
+
const { minPaneWidth = 420, gap = 16 } = options;
|
|
10
|
+
if (![width, height, gap].every(v => Number.isFinite(v) && v >= 0) || !Number.isFinite(minPaneWidth) || minPaneWidth <= 0)
|
|
11
|
+
throw new RangeError('Invalid responsive workspace dimensions');
|
|
12
|
+
const split = width >= minPaneWidth * 2 + gap && height >= 420;
|
|
13
|
+
return Object.freeze({ split, gap, referenceHeight: Math.min(280, Math.max(0, height - 120), Math.max(120, Math.round(height * .38))) });
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=responsive.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { HYTheme } from '../core/types.js';
|
|
2
|
+
export declare const HY_THEME_TOKENS: Readonly<{
|
|
3
|
+
readonly text: "--hy-text-color";
|
|
4
|
+
readonly muted: "--hy-secondary-text-color";
|
|
5
|
+
readonly surface: "--hy-surface-color";
|
|
6
|
+
readonly input: "--hy-input-bg-color";
|
|
7
|
+
readonly accent: "--hy-accent-color";
|
|
8
|
+
readonly danger: "--hy-invalid-border-color";
|
|
9
|
+
readonly border: "--hy-border-color";
|
|
10
|
+
readonly focus: "--hy-focus-ring-color";
|
|
11
|
+
readonly selected: "--hy-selected-bg-color";
|
|
12
|
+
readonly pressed: "--hy-pressed-bg-color";
|
|
13
|
+
readonly disabled: "--hy-disabled-text-color";
|
|
14
|
+
readonly elevated: "--hy-surface-elevated-color";
|
|
15
|
+
readonly onAccent: "--hy-on-accent-color";
|
|
16
|
+
readonly fontSize: "--hy-font-size";
|
|
17
|
+
readonly lineHeight: "--hy-line-height";
|
|
18
|
+
readonly gap: "--hy-gap";
|
|
19
|
+
readonly radius: "--hy-radius";
|
|
20
|
+
}>;
|
|
21
|
+
export type HYThemeVariables = Readonly<Record<(typeof HY_THEME_TOKENS)['text' | 'muted' | 'surface' | 'input' | 'accent' | 'danger' | 'border' | 'focus' | 'fontSize' | 'lineHeight' | 'gap' | 'radius'], string> & Partial<Record<(typeof HY_THEME_TOKENS)[keyof typeof HY_THEME_TOKENS], string>> & Record<string, string>>;
|
|
22
|
+
export declare function hyNativeTheme(theme: HYTheme, fontScale?: number): HYTheme;
|
|
23
|
+
/** Returns data for an adapter to apply; importing it never creates a stylesheet. */
|
|
24
|
+
export declare function hyThemeVariables(theme: HYTheme, fontScale?: number): HYThemeVariables;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { HYUIError } from '../core/validation.js';
|
|
2
|
+
import { snapshot } from '../core/data.js';
|
|
3
|
+
export const HY_THEME_TOKENS = Object.freeze({
|
|
4
|
+
text: '--hy-text-color', muted: '--hy-secondary-text-color', surface: '--hy-surface-color', input: '--hy-input-bg-color',
|
|
5
|
+
accent: '--hy-accent-color', danger: '--hy-invalid-border-color', border: '--hy-border-color', focus: '--hy-focus-ring-color',
|
|
6
|
+
selected: '--hy-selected-bg-color', pressed: '--hy-pressed-bg-color', disabled: '--hy-disabled-text-color', elevated: '--hy-surface-elevated-color', onAccent: '--hy-on-accent-color',
|
|
7
|
+
fontSize: '--hy-font-size', lineHeight: '--hy-line-height', gap: '--hy-gap', radius: '--hy-radius',
|
|
8
|
+
});
|
|
9
|
+
export function hyNativeTheme(theme, fontScale = 1) {
|
|
10
|
+
const bad = (property) => { throw new HYUIError('invalid-props', 'Invalid portable theme value', { property }); };
|
|
11
|
+
if (!theme || typeof theme !== 'object')
|
|
12
|
+
bad('theme');
|
|
13
|
+
for (const key of Object.keys(theme))
|
|
14
|
+
if (!['color', 'fontSize', 'lineHeight', 'gap', 'radius', 'typography'].includes(key))
|
|
15
|
+
bad(key);
|
|
16
|
+
if (!Number.isFinite(fontScale) || fontScale <= 0)
|
|
17
|
+
bad('fontScale');
|
|
18
|
+
for (const key of ['fontSize', 'lineHeight', 'gap', 'radius']) {
|
|
19
|
+
if (!Number.isFinite(theme[key]) || theme[key] < 0 || ((key === 'fontSize' || key === 'lineHeight') && theme[key] === 0))
|
|
20
|
+
bad(key);
|
|
21
|
+
}
|
|
22
|
+
for (const key of ['text', 'muted', 'surface', 'input', 'accent', 'danger', 'border', 'focus']) {
|
|
23
|
+
if (typeof theme.color?.[key] !== 'string' || !/^#[\da-f]{6}([\da-f]{2})?$/i.test(theme.color[key]))
|
|
24
|
+
bad(`color.${key}`);
|
|
25
|
+
}
|
|
26
|
+
for (const key of Object.keys(theme.color))
|
|
27
|
+
if (!['text', 'muted', 'surface', 'input', 'accent', 'danger', 'border', 'focus', 'selected', 'pressed', 'disabled', 'elevated', 'onAccent'].includes(key))
|
|
28
|
+
bad(`color.${key}`);
|
|
29
|
+
for (const [key, value] of Object.entries(theme.color))
|
|
30
|
+
if (typeof value !== 'string' || !/^#[\da-f]{6}([\da-f]{2})?$/i.test(value))
|
|
31
|
+
bad(`color.${key}`);
|
|
32
|
+
const typography = {};
|
|
33
|
+
if (theme.typography !== undefined) {
|
|
34
|
+
if (!theme.typography || typeof theme.typography !== 'object' || Array.isArray(theme.typography))
|
|
35
|
+
bad('typography');
|
|
36
|
+
for (const [key, value] of Object.entries(theme.typography)) {
|
|
37
|
+
if (!['title', 'body', 'caption'].includes(key) || !value || typeof value !== 'object')
|
|
38
|
+
bad(`typography.${key}`);
|
|
39
|
+
for (const field of Object.keys(value))
|
|
40
|
+
if (!['fontSize', 'lineHeight', 'weight'].includes(field))
|
|
41
|
+
bad(`typography.${key}.${field}`);
|
|
42
|
+
if (![value.fontSize, value.lineHeight].every(v => Number.isFinite(v) && v > 0 && Number.isFinite(v * fontScale)) || (value.weight !== undefined && !['normal', 'medium', 'bold'].includes(value.weight)))
|
|
43
|
+
bad(`typography.${key}`);
|
|
44
|
+
typography[key] = { ...value, fontSize: value.fontSize * fontScale, lineHeight: value.lineHeight * fontScale };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!Number.isFinite(theme.fontSize * fontScale) || !Number.isFinite(theme.lineHeight * fontScale))
|
|
48
|
+
bad('fontScale');
|
|
49
|
+
return snapshot({ color: { ...theme.color }, fontSize: theme.fontSize * fontScale, lineHeight: theme.lineHeight * fontScale, gap: theme.gap, radius: theme.radius, ...(theme.typography ? { typography } : {}) });
|
|
50
|
+
}
|
|
51
|
+
/** Returns data for an adapter to apply; importing it never creates a stylesheet. */
|
|
52
|
+
export function hyThemeVariables(theme, fontScale = 1) {
|
|
53
|
+
const data = hyNativeTheme(theme, fontScale);
|
|
54
|
+
return Object.freeze(Object.fromEntries([
|
|
55
|
+
...Object.entries(data.color).map(([key, value]) => [HY_THEME_TOKENS[key], value]),
|
|
56
|
+
...['fontSize', 'lineHeight', 'gap', 'radius'].map(key => [HY_THEME_TOKENS[key], `${data[key]}px`]),
|
|
57
|
+
...Object.entries(data.typography ?? {}).flatMap(([key, value]) => [[`--hy-${key}-font-size`, `${value.fontSize}px`], [`--hy-${key}-line-height`, `${value.lineHeight}px`], [`--hy-${key}-font-weight`, value.weight === 'bold' ? '700' : value.weight === 'medium' ? '500' : '400']]),
|
|
58
|
+
]));
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=theme.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HYMutation, HYNode } from '../core/types.js';
|
|
2
|
+
export interface Entry {
|
|
3
|
+
readonly node: HYNode;
|
|
4
|
+
readonly parent: string | null;
|
|
5
|
+
readonly slot?: string;
|
|
6
|
+
}
|
|
7
|
+
export type Tree = ReadonlyMap<string, Entry>;
|
|
8
|
+
export declare function flatten(root: HYNode): Tree;
|
|
9
|
+
/** Returns a deterministic batch. Removing an ancestor releases descendants exactly once. */
|
|
10
|
+
export declare function diff(previous: Tree, next: Tree): readonly HYMutation[];
|