@haiyue/ui 0.1.1 → 0.2.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 (99) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +504 -1
  3. package/dist/adapters/web/controls.d.ts +19 -0
  4. package/dist/adapters/web/controls.js +284 -0
  5. package/dist/adapters/web/dom.d.ts +10 -0
  6. package/dist/adapters/web/dom.js +98 -0
  7. package/dist/adapters/web/index.d.ts +3 -0
  8. package/dist/adapters/web/index.js +3 -0
  9. package/dist/adapters/web/renderer.d.ts +18 -0
  10. package/dist/adapters/web/renderer.js +545 -0
  11. package/dist/adapters/web/styles.d.ts +3 -0
  12. package/dist/adapters/web/styles.js +62 -0
  13. package/dist/adapters/web/types.d.ts +46 -0
  14. package/dist/adapters/web/types.js +2 -0
  15. package/dist/affix.d.ts +35 -0
  16. package/dist/affix.js +137 -0
  17. package/dist/badge.d.ts +45 -0
  18. package/dist/badge.js +168 -0
  19. package/dist/border-beam.js +3 -1
  20. package/dist/breadcrumb.d.ts +37 -0
  21. package/dist/breadcrumb.js +210 -0
  22. package/dist/button-group.d.ts +12 -0
  23. package/dist/button-group.js +72 -0
  24. package/dist/button.d.ts +15 -1
  25. package/dist/button.js +101 -12
  26. package/dist/cascader.d.ts +65 -0
  27. package/dist/cascader.js +479 -0
  28. package/dist/collapse.d.ts +55 -0
  29. package/dist/collapse.js +261 -0
  30. package/dist/core/actions.d.ts +32 -0
  31. package/dist/core/actions.js +145 -0
  32. package/dist/core/data.d.ts +4 -0
  33. package/dist/core/data.js +27 -0
  34. package/dist/core/index.d.ts +7 -0
  35. package/dist/core/index.js +6 -0
  36. package/dist/core/platform-view.d.ts +17 -0
  37. package/dist/core/platform-view.js +84 -0
  38. package/dist/core/selection.d.ts +14 -0
  39. package/dist/core/selection.js +93 -0
  40. package/dist/core/text.d.ts +31 -0
  41. package/dist/core/text.js +154 -0
  42. package/dist/core/types.d.ts +250 -0
  43. package/dist/core/types.js +2 -0
  44. package/dist/core/validation.d.ts +24 -0
  45. package/dist/core/validation.js +238 -0
  46. package/dist/date-picker.d.ts +60 -0
  47. package/dist/date-picker.js +355 -0
  48. package/dist/dialog.d.ts +4 -0
  49. package/dist/dialog.js +23 -5
  50. package/dist/divider.d.ts +33 -0
  51. package/dist/divider.js +113 -0
  52. package/dist/draggable.d.ts +78 -0
  53. package/dist/draggable.js +255 -0
  54. package/dist/expandable.d.ts +32 -0
  55. package/dist/expandable.js +136 -0
  56. package/dist/index.d.ts +47 -1
  57. package/dist/index.js +69 -0
  58. package/dist/input.d.ts +30 -1
  59. package/dist/input.js +166 -54
  60. package/dist/portable/index.d.ts +4 -0
  61. package/dist/portable/index.js +3 -0
  62. package/dist/portable/page.d.ts +38 -0
  63. package/dist/portable/page.js +361 -0
  64. package/dist/portable/theme.d.ts +19 -0
  65. package/dist/portable/theme.js +40 -0
  66. package/dist/portable/tree.d.ts +10 -0
  67. package/dist/portable/tree.js +96 -0
  68. package/dist/range.d.ts +3 -0
  69. package/dist/range.js +16 -5
  70. package/dist/rate.d.ts +46 -0
  71. package/dist/rate.js +193 -0
  72. package/dist/resizable.d.ts +97 -0
  73. package/dist/resizable.js +426 -0
  74. package/dist/skeleton.d.ts +41 -0
  75. package/dist/skeleton.js +141 -0
  76. package/dist/steps.d.ts +45 -0
  77. package/dist/steps.js +203 -0
  78. package/dist/table.d.ts +106 -0
  79. package/dist/table.js +689 -0
  80. package/dist/tabs.d.ts +7 -0
  81. package/dist/tabs.js +90 -16
  82. package/dist/tag.d.ts +55 -0
  83. package/dist/tag.js +225 -0
  84. package/dist/time-picker.d.ts +62 -0
  85. package/dist/time-picker.js +298 -0
  86. package/dist/timeline.d.ts +48 -0
  87. package/dist/timeline.js +201 -0
  88. package/dist/toast.d.ts +55 -0
  89. package/dist/toast.js +224 -0
  90. package/dist/tour.d.ts +89 -0
  91. package/dist/tour.js +306 -0
  92. package/dist/virtual-keyboard.d.ts +95 -0
  93. package/dist/virtual-keyboard.js +432 -0
  94. package/dist/virtual-list.js +12 -10
  95. package/dist/virtual-mouse.d.ts +82 -0
  96. package/dist/virtual-mouse.js +358 -0
  97. package/dist/watermark.d.ts +44 -0
  98. package/dist/watermark.js +148 -0
  99. package/package.json +135 -7
@@ -0,0 +1,261 @@
1
+ let collapseInstance = 0;
2
+ function parseKeys(value) {
3
+ if (!value)
4
+ return [];
5
+ try {
6
+ const parsed = JSON.parse(value);
7
+ return Array.isArray(parsed) ? [...new Set(parsed.map(String))] : [String(parsed)];
8
+ }
9
+ catch {
10
+ return [...new Set(value.split(',').map(key => key.trim()).filter(Boolean))];
11
+ }
12
+ }
13
+ /** Accessible accordion and multi-panel disclosure group. */
14
+ export class HYCollapse extends HTMLElement {
15
+ _container = document.createElement('div');
16
+ _uid = `hy-collapse-${++collapseInstance}`;
17
+ _items = [];
18
+ _itemsAssigned = false;
19
+ _activeKeys = [];
20
+ _panels = new Map();
21
+ static get observedAttributes() {
22
+ return ['items', 'active-keys', 'default-active-keys', 'accordion', 'bordered', 'ghost', 'icon-position', 'collapsible', 'destroy-on-hidden', 'aria-label'];
23
+ }
24
+ constructor() {
25
+ super();
26
+ const root = this.attachShadow({ mode: 'open' });
27
+ const style = document.createElement('style');
28
+ style.textContent = `
29
+ :host { display: block; min-width: 0; color: var(--hy-text-color, #eef3ff); font: 12px/1.5 system-ui, sans-serif; }
30
+ .collapse { overflow: hidden; border: 1px solid var(--hy-border-color, #3d4654); border-radius: var(--hy-collapse-radius, 8px); background: var(--hy-surface-color, #111722); }
31
+ .panel + .panel { border-top: 1px solid var(--hy-border-color, #3d4654); }
32
+ .header { display: flex; min-height: var(--hy-collapse-header-height, 44px); align-items: center; background: var(--hy-collapse-header-bg, color-mix(in srgb, var(--hy-surface-elevated-color, #171d27) 82%, transparent)); }
33
+ .trigger { display: flex; min-width: 0; min-height: inherit; flex: 1 1 auto; align-items: center; gap: 10px; padding: 9px 14px; border: 0; color: inherit; background: transparent; cursor: pointer; font: inherit; text-align: start; }
34
+ .trigger:hover:not(:disabled) { background: var(--hy-hover-bg-color, #273241); }
35
+ .trigger:focus-visible { position: relative; z-index: 1; outline: 2px solid var(--hy-focus-border-color, #79a8ff); outline-offset: -2px; }
36
+ .trigger:disabled { color: var(--hy-muted-color, #788697); cursor: not-allowed; }
37
+ .arrow { display: grid; width: 16px; height: 16px; flex: 0 0 auto; place-items: center; color: var(--hy-muted-color, #788697); transition: transform 160ms ease; }
38
+ .arrow::before { content: ''; width: 6px; height: 6px; border-block-start: 1.5px solid currentColor; border-inline-end: 1.5px solid currentColor; transform: rotate(45deg); }
39
+ .panel.expanded .arrow { transform: rotate(90deg); }
40
+ .title { min-width: 0; flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
41
+ .extra { flex: 0 0 auto; padding-inline: var(--hy-collapse-extra-gap, 10px) 14px; color: var(--hy-muted-color, #788697); font-size: 10px; }
42
+ .region[hidden] { display: none; }
43
+ .body { padding: var(--hy-collapse-content-padding, 16px); border-top: 1px solid var(--hy-border-color, #3d4654); color: var(--hy-secondary-text-color, #aebbd0); background: var(--hy-collapse-content-bg, transparent); }
44
+ :host([icon-position="end"]) .arrow { order: 2; }
45
+ :host([bordered="false"]) .collapse { border: 0; border-radius: 0; }
46
+ :host([ghost]) .collapse { border: 0; background: transparent; }
47
+ :host([ghost]) .panel + .panel { border-top-color: transparent; }
48
+ :host([ghost]) .header, :host([ghost]) .body { border: 0; background: transparent; }
49
+ @media (prefers-reduced-motion: reduce) { .arrow { transition: none; } }
50
+ `;
51
+ this._container.className = 'collapse';
52
+ this._container.part.add('collapse');
53
+ this._container.setAttribute('role', 'group');
54
+ root.append(style, this._container);
55
+ }
56
+ connectedCallback() {
57
+ if (!this._itemsAssigned && this.hasAttribute('items'))
58
+ this._readItemsAttribute();
59
+ this._activeKeys = this.hasAttribute('active-keys') ? parseKeys(this.getAttribute('active-keys')) : parseKeys(this.getAttribute('default-active-keys'));
60
+ this._normalizeActiveKeys();
61
+ this._syncItems();
62
+ this._syncLabel();
63
+ }
64
+ attributeChangedCallback(name, oldValue, newValue) {
65
+ if (oldValue === newValue)
66
+ return;
67
+ if (name === 'items' && !this._itemsAssigned) {
68
+ this._readItemsAttribute();
69
+ this._syncItems();
70
+ return;
71
+ }
72
+ if (name === 'active-keys') {
73
+ this._activeKeys = parseKeys(newValue);
74
+ this._normalizeActiveKeys();
75
+ this._syncExpanded();
76
+ return;
77
+ }
78
+ if (name === 'default-active-keys' && !this.hasAttribute('active-keys')) {
79
+ this._activeKeys = parseKeys(newValue);
80
+ this._normalizeActiveKeys();
81
+ this._syncExpanded();
82
+ return;
83
+ }
84
+ if (name === 'aria-label') {
85
+ this._syncLabel();
86
+ return;
87
+ }
88
+ if (name === 'accordion') {
89
+ this._normalizeActiveKeys();
90
+ this._reflectActiveKeys();
91
+ }
92
+ this._syncExpanded();
93
+ }
94
+ get items() { return this._items; }
95
+ set items(value) {
96
+ this._itemsAssigned = true;
97
+ this._items = Array.isArray(value) ? value.flatMap(item => item && item.key != null && item.title != null ? [{ ...item, key: String(item.key), title: String(item.title) }] : []) : [];
98
+ this._normalizeActiveKeys();
99
+ this._syncItems();
100
+ }
101
+ get activeKeys() { return [...this._activeKeys]; }
102
+ set activeKeys(value) {
103
+ this._activeKeys = Array.isArray(value) ? [...new Set(value.map(String))] : [];
104
+ this._normalizeActiveKeys();
105
+ this._reflectActiveKeys();
106
+ this._syncExpanded();
107
+ }
108
+ get accordion() { return this.hasAttribute('accordion'); }
109
+ set accordion(value) { this.toggleAttribute('accordion', Boolean(value)); }
110
+ get bordered() { return this.getAttribute('bordered') !== 'false'; }
111
+ set bordered(value) { this.setAttribute('bordered', String(Boolean(value))); }
112
+ get ghost() { return this.hasAttribute('ghost'); }
113
+ set ghost(value) { this.toggleAttribute('ghost', Boolean(value)); }
114
+ get iconPosition() { return this.getAttribute('icon-position') === 'end' ? 'end' : 'start'; }
115
+ set iconPosition(value) { this.setAttribute('icon-position', value === 'end' ? 'end' : 'start'); }
116
+ get collapsible() {
117
+ const value = this.getAttribute('collapsible');
118
+ return value === 'icon' || value === 'disabled' ? value : 'header';
119
+ }
120
+ set collapsible(value) { this.setAttribute('collapsible', value === 'icon' || value === 'disabled' ? value : 'header'); }
121
+ get destroyOnHidden() { return this.hasAttribute('destroy-on-hidden'); }
122
+ set destroyOnHidden(value) { this.toggleAttribute('destroy-on-hidden', Boolean(value)); }
123
+ toggle(key, expanded) {
124
+ const normalized = String(key);
125
+ const panel = this._panels.get(normalized);
126
+ if (!panel || panel.item.disabled || this.collapsible === 'disabled')
127
+ return;
128
+ const wasExpanded = this._activeKeys.includes(normalized);
129
+ const nextExpanded = expanded ?? !wasExpanded;
130
+ if (wasExpanded === nextExpanded)
131
+ return;
132
+ this._activeKeys = nextExpanded ? (this.accordion ? [normalized] : [...this._activeKeys, normalized]) : this._activeKeys.filter(activeKey => activeKey !== normalized);
133
+ this._reflectActiveKeys();
134
+ this._syncExpanded();
135
+ this.dispatchEvent(new CustomEvent('collapse-change', { bubbles: true, composed: true, detail: { activeKeys: [...this._activeKeys], changedKey: normalized, expanded: nextExpanded } }));
136
+ }
137
+ _readItemsAttribute() {
138
+ try {
139
+ const parsed = JSON.parse(this.getAttribute('items') ?? '[]');
140
+ this._items = Array.isArray(parsed) ? parsed.flatMap(value => {
141
+ if (!value || typeof value !== 'object')
142
+ return [];
143
+ const item = value;
144
+ if (item.key == null || item.title == null)
145
+ return [];
146
+ return [{ key: String(item.key), title: String(item.title), ...(item.content != null ? { content: String(item.content) } : {}), ...(item.extra != null ? { extra: String(item.extra) } : {}), ...(typeof item.disabled === 'boolean' ? { disabled: item.disabled } : {}), ...(typeof item.showArrow === 'boolean' ? { showArrow: item.showArrow } : {}) }];
147
+ }) : [];
148
+ }
149
+ catch {
150
+ this._items = [];
151
+ }
152
+ }
153
+ _syncItems() {
154
+ if (!this.isConnected)
155
+ return;
156
+ this._panels.clear();
157
+ const sections = [];
158
+ this._items.forEach((item, index) => {
159
+ const section = document.createElement('section');
160
+ const header = document.createElement('div');
161
+ const button = document.createElement('button');
162
+ const arrow = document.createElement('span');
163
+ const title = document.createElement('span');
164
+ const extra = document.createElement('span');
165
+ const region = document.createElement('div');
166
+ const body = document.createElement('div');
167
+ const buttonId = `${this._uid}-button-${index}`;
168
+ const regionId = `${this._uid}-panel-${index}`;
169
+ section.className = 'panel';
170
+ section.part.add('panel');
171
+ header.className = 'header';
172
+ header.part.add('header');
173
+ button.type = 'button';
174
+ button.className = 'trigger';
175
+ button.part.add('trigger');
176
+ button.id = buttonId;
177
+ button.setAttribute('aria-controls', regionId);
178
+ arrow.className = 'arrow';
179
+ arrow.part.add('arrow');
180
+ arrow.setAttribute('aria-hidden', 'true');
181
+ arrow.hidden = item.showArrow === false;
182
+ title.className = 'title';
183
+ title.part.add('title');
184
+ title.textContent = item.title;
185
+ button.append(arrow, title);
186
+ extra.className = 'extra';
187
+ extra.part.add('extra');
188
+ extra.textContent = item.extra ?? '';
189
+ extra.hidden = !item.extra;
190
+ header.append(button, extra);
191
+ region.className = 'region';
192
+ region.part.add('region');
193
+ region.id = regionId;
194
+ region.setAttribute('role', 'region');
195
+ region.setAttribute('aria-labelledby', buttonId);
196
+ body.className = 'body';
197
+ body.part.add('body');
198
+ region.append(body);
199
+ section.append(header, region);
200
+ const record = { item, section, button, arrow, region, body, rendered: false };
201
+ this._panels.set(item.key, record);
202
+ sections.push(section);
203
+ button.addEventListener('click', event => { if (this.collapsible === 'icon' && event.detail !== 0 && !event.composedPath()[0]?.closest?.('.arrow'))
204
+ return; this.toggle(item.key); });
205
+ });
206
+ this._container.replaceChildren(...sections);
207
+ this._syncExpanded();
208
+ }
209
+ _syncExpanded() {
210
+ if (!this.isConnected)
211
+ return;
212
+ for (const [key, panel] of this._panels) {
213
+ const expanded = this._activeKeys.includes(key);
214
+ const disabled = Boolean(panel.item.disabled) || this.collapsible === 'disabled';
215
+ panel.section.classList.toggle('expanded', expanded);
216
+ panel.section.toggleAttribute('data-disabled', disabled);
217
+ panel.button.disabled = disabled;
218
+ panel.button.setAttribute('aria-expanded', String(expanded));
219
+ panel.region.hidden = !expanded;
220
+ if (expanded && !panel.rendered)
221
+ this._renderContent(panel);
222
+ else if (!expanded && this.destroyOnHidden && panel.rendered) {
223
+ panel.body.replaceChildren();
224
+ panel.rendered = false;
225
+ }
226
+ }
227
+ }
228
+ _renderContent(panel) {
229
+ const source = typeof panel.item.content === 'function' ? panel.item.content() : panel.item.content;
230
+ if (source instanceof Node)
231
+ panel.body.replaceChildren(source);
232
+ else
233
+ panel.body.textContent = source == null ? '' : String(source);
234
+ panel.rendered = true;
235
+ }
236
+ _normalizeActiveKeys() {
237
+ if (this._items.length) {
238
+ const valid = new Set(this._items.map(item => item.key));
239
+ this._activeKeys = this._activeKeys.filter(key => valid.has(key));
240
+ }
241
+ if (this.accordion)
242
+ this._activeKeys = this._activeKeys.slice(0, 1);
243
+ }
244
+ _reflectActiveKeys() {
245
+ const value = JSON.stringify(this._activeKeys);
246
+ if (this.getAttribute('active-keys') !== value)
247
+ this.setAttribute('active-keys', value);
248
+ }
249
+ _syncLabel() {
250
+ const label = this.getAttribute('aria-label');
251
+ if (label)
252
+ this._container.setAttribute('aria-label', label);
253
+ else
254
+ this._container.removeAttribute('aria-label');
255
+ }
256
+ }
257
+ export function defineCollapseComponents() {
258
+ if (!customElements.get('hy-collapse'))
259
+ customElements.define('hy-collapse', HYCollapse);
260
+ }
261
+ //# sourceMappingURL=collapse.js.map
@@ -0,0 +1,32 @@
1
+ import type { HYComponentProps, HYReason, HYUIEvent } from './types.js';
2
+ export declare class HYButtonController {
3
+ #private;
4
+ readonly key: string;
5
+ private readonly emit;
6
+ constructor(key: string, props: HYComponentProps['Button'], emit: (event: HYUIEvent) => void);
7
+ update(props: HYComponentProps['Button']): void;
8
+ /** Adapters pass the same gesture id for overlapping tap/click/accessibility callbacks. */
9
+ activate(reason?: HYReason, gesture?: string | number): boolean;
10
+ dispose(): void;
11
+ }
12
+ export declare class HYToggleController {
13
+ #private;
14
+ readonly key: string;
15
+ private readonly emit;
16
+ constructor(key: string, props: HYComponentProps['Toggle'], emit: (event: HYUIEvent) => void);
17
+ update(props: HYComponentProps['Toggle']): void;
18
+ choose(checked: boolean, reason?: HYReason): boolean;
19
+ dispose(): void;
20
+ }
21
+ export declare class HYSliderController {
22
+ #private;
23
+ readonly key: string;
24
+ private readonly emit;
25
+ constructor(key: string, props: HYComponentProps['Slider'], emit: (event: HYUIEvent) => void);
26
+ get value(): number;
27
+ update(props: HYComponentProps['Slider']): void;
28
+ change(value: number, reason?: HYReason): boolean;
29
+ commit(reason?: HYReason): boolean;
30
+ cancel(reason?: HYReason): void;
31
+ dispose(): void;
32
+ }
@@ -0,0 +1,145 @@
1
+ import { snapshot } from './data.js';
2
+ import { hyKey, hyValidateProps } from './validation.js';
3
+ export class HYButtonController {
4
+ key;
5
+ emit;
6
+ #props;
7
+ #disposed = false;
8
+ #emitting = false;
9
+ #transaction = 0;
10
+ #gestures = new Set();
11
+ constructor(key, props, emit) {
12
+ this.key = key;
13
+ this.emit = emit;
14
+ hyKey(key);
15
+ this.update(props);
16
+ }
17
+ update(props) {
18
+ if (this.#disposed)
19
+ return;
20
+ hyValidateProps('Button', props, { key: this.key });
21
+ this.#props = snapshot(props);
22
+ }
23
+ /** Adapters pass the same gesture id for overlapping tap/click/accessibility callbacks. */
24
+ activate(reason = 'pointer', gesture) {
25
+ if (this.#disposed || this.#emitting || this.#props.disabled || this.#props.busy || this.#props.visible === false)
26
+ return false;
27
+ if (gesture !== undefined) {
28
+ if (this.#gestures.has(gesture))
29
+ return false;
30
+ this.#gestures.add(gesture);
31
+ if (this.#gestures.size > 64)
32
+ this.#gestures.delete(this.#gestures.values().next().value);
33
+ }
34
+ this.#emitting = true;
35
+ try {
36
+ this.emit({ key: this.key, type: 'activate', reason, transaction: ++this.#transaction });
37
+ return true;
38
+ }
39
+ finally {
40
+ this.#emitting = false;
41
+ }
42
+ }
43
+ dispose() { this.#disposed = true; this.#gestures.clear(); }
44
+ }
45
+ export class HYToggleController {
46
+ key;
47
+ emit;
48
+ #props;
49
+ #disposed = false;
50
+ #emitting = false;
51
+ #transaction = 0;
52
+ constructor(key, props, emit) {
53
+ this.key = key;
54
+ this.emit = emit;
55
+ hyKey(key);
56
+ this.update(props);
57
+ }
58
+ update(props) {
59
+ if (this.#disposed)
60
+ return;
61
+ hyValidateProps('Toggle', props, { key: this.key });
62
+ this.#props = snapshot(props);
63
+ }
64
+ choose(checked, reason = 'pointer') {
65
+ if (typeof checked !== 'boolean' || this.#disposed || this.#emitting || this.#props.disabled || this.#props.visible === false || checked === this.#props.checked)
66
+ return false;
67
+ this.#emitting = true;
68
+ const event = { key: this.key, value: checked, reason, transaction: ++this.#transaction };
69
+ try {
70
+ this.emit({ ...event, type: 'valueChange' });
71
+ if (this.#disposed)
72
+ return false;
73
+ this.emit({ ...event, type: 'commit' });
74
+ return true;
75
+ }
76
+ finally {
77
+ this.#emitting = false;
78
+ }
79
+ }
80
+ dispose() { this.#disposed = true; }
81
+ }
82
+ export class HYSliderController {
83
+ key;
84
+ emit;
85
+ #props;
86
+ #draft = 0;
87
+ #transaction = 0;
88
+ #active = 0;
89
+ #disposed = false;
90
+ constructor(key, props, emit) {
91
+ this.key = key;
92
+ this.emit = emit;
93
+ hyKey(key);
94
+ this.update(props);
95
+ }
96
+ get value() { return this.#draft; }
97
+ update(props) {
98
+ if (this.#disposed)
99
+ return;
100
+ hyValidateProps('Slider', props, { key: this.key });
101
+ if (!this.#props || props.value !== this.#props.value || props.min !== this.#props.min || props.max !== this.#props.max || props.step !== this.#props.step || props.disabled || props.visible === false) {
102
+ this.#draft = props.value;
103
+ this.#active = 0;
104
+ }
105
+ this.#props = snapshot(props);
106
+ }
107
+ change(value, reason = 'pointer') {
108
+ if (this.#disposed || this.#props.disabled || this.#props.visible === false || !Number.isFinite(value))
109
+ return false;
110
+ const { min, max, step } = this.#props;
111
+ const quantized = min + Math.round((Math.max(min, Math.min(max, value)) - min) / step) * step;
112
+ const next = Math.max(min, Math.min(max, Number(quantized.toPrecision(14))));
113
+ if (next === this.#draft)
114
+ return false;
115
+ this.#draft = next;
116
+ this.#active ||= ++this.#transaction;
117
+ this.emit({ key: this.key, type: 'valueChange', value: next, reason, transaction: this.#active });
118
+ return true;
119
+ }
120
+ commit(reason = 'pointer') {
121
+ if (this.#disposed || this.#props.disabled || !this.#active)
122
+ return false;
123
+ const transaction = this.#active;
124
+ this.#active = 0;
125
+ if (this.#draft === this.#props.value)
126
+ return false;
127
+ try {
128
+ this.emit({ key: this.key, type: 'commit', value: this.#draft, reason, transaction });
129
+ return true;
130
+ }
131
+ finally {
132
+ this.#draft = this.#props.value;
133
+ }
134
+ }
135
+ cancel(reason = 'pointer') {
136
+ if (this.#disposed || !this.#active)
137
+ return;
138
+ const transaction = this.#active;
139
+ this.#active = 0;
140
+ this.#draft = this.#props.value;
141
+ this.emit({ key: this.key, type: 'cancel', reason, transaction });
142
+ }
143
+ dispose() { this.#disposed = true; this.#active = 0; }
144
+ }
145
+ //# sourceMappingURL=actions.js.map
@@ -0,0 +1,4 @@
1
+ /** Internal structural comparison for immutable protocol data; never serializes props. */
2
+ export declare function equalData(a: unknown, b: unknown): boolean;
3
+ export declare function isSnapshot(value: object): boolean;
4
+ export declare function snapshot<T>(value: T): T;
@@ -0,0 +1,27 @@
1
+ /** Internal structural comparison for immutable protocol data; never serializes props. */
2
+ export function equalData(a, b) {
3
+ if (Object.is(a, b))
4
+ return true;
5
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object')
6
+ return false;
7
+ if (Array.isArray(a) !== Array.isArray(b))
8
+ return false;
9
+ const left = a, right = b;
10
+ const keys = Object.keys(left);
11
+ return keys.length === Object.keys(right).length
12
+ && keys.every(key => Object.hasOwn(right, key) && equalData(left[key], right[key]));
13
+ }
14
+ /** Called only after schema validation, so cycles and platform handles are impossible. */
15
+ const snapshots = new WeakSet();
16
+ export function isSnapshot(value) { return snapshots.has(value); }
17
+ export function snapshot(value) {
18
+ if (!value || typeof value !== 'object')
19
+ return value;
20
+ if (snapshots.has(value))
21
+ return value;
22
+ const result = Array.isArray(value) ? Object.freeze(value.map(snapshot))
23
+ : Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, snapshot(item)])));
24
+ snapshots.add(result);
25
+ return result;
26
+ }
27
+ //# sourceMappingURL=data.js.map
@@ -0,0 +1,7 @@
1
+ export type * from './types.js';
2
+ export { HYUIError, HY_COMPONENTS, HY_FEATURES, hyValidateCapabilities, hyValidateProps, hyValidateNode, hySupports, hyKey, hyNode } from './validation.js';
3
+ export { HYSelectionController } from './selection.js';
4
+ export { HYButtonController, HYToggleController, HYSliderController } from './actions.js';
5
+ export { HYTextController } from './text.js';
6
+ export type { HYTextView, HYTextViewState } from './text.js';
7
+ export { HYPlatformViewController } from './platform-view.js';
@@ -0,0 +1,6 @@
1
+ export { HYUIError, HY_COMPONENTS, HY_FEATURES, hyValidateCapabilities, hyValidateProps, hyValidateNode, hySupports, hyKey, hyNode } from './validation.js';
2
+ export { HYSelectionController } from './selection.js';
3
+ export { HYButtonController, HYToggleController, HYSliderController } from './actions.js';
4
+ export { HYTextController } from './text.js';
5
+ export { HYPlatformViewController } from './platform-view.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,17 @@
1
+ import type { HYBounds, HYPlatformView } from './types.js';
2
+ /** Backend-owned resource lifetime. No GPU acquire/present or platform handles in page state. */
3
+ export declare class HYPlatformViewController {
4
+ #private;
5
+ private readonly factory;
6
+ private resourceId;
7
+ private shown;
8
+ constructor(factory: () => HYPlatformView, resourceId: string, shown?: boolean);
9
+ mount(): void;
10
+ update(resourceId: string): void;
11
+ layout(bounds: HYBounds): void;
12
+ visible(value: boolean): void;
13
+ focus(): void;
14
+ suspend(): void;
15
+ resume(): void;
16
+ dispose(): void;
17
+ }
@@ -0,0 +1,84 @@
1
+ import { equalData, snapshot } from './data.js';
2
+ import { HYUIError } from './validation.js';
3
+ /** Backend-owned resource lifetime. No GPU acquire/present or platform handles in page state. */
4
+ export class HYPlatformViewController {
5
+ factory;
6
+ resourceId;
7
+ shown;
8
+ #view;
9
+ #disposed = false;
10
+ #suspended = false;
11
+ #bounds;
12
+ constructor(factory, resourceId, shown = true) {
13
+ this.factory = factory;
14
+ this.resourceId = resourceId;
15
+ this.shown = shown;
16
+ this.#validateId(resourceId);
17
+ }
18
+ #validateId(id) { if (typeof id !== 'string' || !id.trim())
19
+ throw new HYUIError('invalid-props', 'PlatformView needs a resourceId'); }
20
+ mount() {
21
+ if (this.#disposed || this.#view)
22
+ throw new HYUIError('lifecycle', 'PlatformView disposed or already mounted');
23
+ this.#view = this.factory();
24
+ try {
25
+ this.#view.mount();
26
+ this.#view.update(this.resourceId);
27
+ if (this.#bounds)
28
+ this.#view.layout(this.#bounds);
29
+ this.#view.visible(this.shown && !this.#suspended);
30
+ }
31
+ catch (error) {
32
+ this.dispose();
33
+ throw error;
34
+ }
35
+ }
36
+ update(resourceId) {
37
+ if (this.#disposed)
38
+ return;
39
+ this.#validateId(resourceId);
40
+ if (resourceId === this.resourceId)
41
+ return;
42
+ this.resourceId = resourceId;
43
+ this.#view?.update(resourceId);
44
+ }
45
+ layout(bounds) {
46
+ if (this.#disposed)
47
+ return;
48
+ if (![bounds.x, bounds.y, bounds.width, bounds.height, bounds.scale].every(Number.isFinite)
49
+ || bounds.width < 0 || bounds.height < 0 || bounds.scale <= 0)
50
+ throw new HYUIError('invalid-props', 'Invalid PlatformView bounds');
51
+ if (equalData(bounds, this.#bounds))
52
+ return;
53
+ this.#bounds = snapshot(bounds);
54
+ this.#view?.layout(this.#bounds);
55
+ }
56
+ visible(value) {
57
+ if (this.#disposed || this.shown === value)
58
+ return;
59
+ this.shown = value;
60
+ if (!this.#suspended)
61
+ this.#view?.visible(value);
62
+ }
63
+ focus() { if (!this.#disposed && !this.#suspended && this.shown)
64
+ this.#view?.focus(); }
65
+ suspend() { if (!this.#disposed && !this.#suspended) {
66
+ this.#suspended = true;
67
+ if (this.shown)
68
+ this.#view?.visible(false);
69
+ } }
70
+ resume() { if (!this.#disposed && this.#suspended) {
71
+ this.#suspended = false;
72
+ if (this.shown)
73
+ this.#view?.visible(true);
74
+ } }
75
+ dispose() {
76
+ if (this.#disposed)
77
+ return;
78
+ this.#disposed = true;
79
+ const view = this.#view;
80
+ this.#view = undefined;
81
+ view?.dispose();
82
+ }
83
+ }
84
+ //# sourceMappingURL=platform-view.js.map
@@ -0,0 +1,14 @@
1
+ import type { HYReason, HYSelectionEvent, HYSelectionProps, HYSelectionView } from './types.js';
2
+ /** Controlled Select/Tabs semantics. Views report user actions, never delayed setter echoes. */
3
+ export declare class HYSelectionController {
4
+ #private;
5
+ readonly key: string;
6
+ private readonly emit;
7
+ constructor(key: string, props: HYSelectionProps, emit: (event: HYSelectionEvent) => void);
8
+ update(props: HYSelectionProps): void;
9
+ attach(view: HYSelectionView): void;
10
+ choose(value: string, reason?: HYReason): boolean;
11
+ pick(open: () => Promise<string | null>, reason?: HYReason): Promise<boolean>;
12
+ cancel(): void;
13
+ dispose(): void;
14
+ }