@zeyos/zx 2.3.0 → 4.0.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.
Files changed (150) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/DESIGN-SYSTEM.md +96 -0
  3. package/README.md +29 -39
  4. package/dist/types/components/account-menu/account-menu.d.ts +144 -0
  5. package/dist/types/components/app-sidebar/app-sidebar.d.ts +243 -0
  6. package/dist/types/components/avatar/avatar.d.ts +100 -0
  7. package/dist/types/components/card/card.d.ts +128 -0
  8. package/dist/types/components/chart/chart.d.ts +322 -0
  9. package/dist/types/components/date-picker/month-picker.d.ts +6 -0
  10. package/dist/types/components/dialog/dialog.d.ts +9 -0
  11. package/dist/types/components/dock/dock.d.ts +495 -0
  12. package/dist/types/components/dropdown/dropdown.d.ts +2 -2
  13. package/dist/types/components/field/field.d.ts +14 -0
  14. package/dist/types/components/field-upload/field-upload.d.ts +37 -4
  15. package/dist/types/components/form/form.d.ts +25 -3
  16. package/dist/types/components/grid/grid.d.ts +172 -0
  17. package/dist/types/components/launcher/launcher.d.ts +359 -0
  18. package/dist/types/components/layout/layout.d.ts +4 -4
  19. package/dist/types/components/menu-button/menu-button.d.ts +10 -2
  20. package/dist/types/components/modal/modal.d.ts +25 -0
  21. package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +7 -1
  22. package/dist/types/components/questionnaire/items.d.ts +327 -0
  23. package/dist/types/components/questionnaire/questionnaire.d.ts +449 -0
  24. package/dist/types/components/select/select.d.ts +51 -0
  25. package/dist/types/components/sheet/sheet-stack.d.ts +167 -0
  26. package/dist/types/components/sheet/sheet.d.ts +294 -0
  27. package/dist/types/components/split-view/split-view.d.ts +2 -21
  28. package/dist/types/components/table/table.d.ts +223 -15
  29. package/dist/types/components/tooltip/tooltip.d.ts +2 -2
  30. package/dist/types/components/value-list/value-list.d.ts +24 -3
  31. package/dist/types/core/dom.d.ts +122 -0
  32. package/dist/types/core/drag-axis.d.ts +111 -0
  33. package/dist/types/core/position.d.ts +4 -2
  34. package/dist/types/index.d.ts +13 -2
  35. package/dist/types/internal/app-rail.d.ts +265 -0
  36. package/dist/types/zeyos/icons.d.ts +5 -0
  37. package/dist/types/zeyos/index.d.ts +2 -1
  38. package/dist/types/zeyos/launcher.d.ts +147 -0
  39. package/dist/types/zeyos/select.d.ts +39 -0
  40. package/dist/zx-zeyos.esm.js +1870 -133
  41. package/dist/zx-zeyos.esm.min.js +1 -1
  42. package/dist/zx.css +2006 -267
  43. package/dist/zx.esm.js +8987 -2757
  44. package/dist/zx.esm.min.js +3 -3
  45. package/dist/zx.global.js +8987 -2757
  46. package/dist/zx.global.min.js +3 -3
  47. package/dist/zx.min.css +1 -1
  48. package/docs/api.json +4396 -2344
  49. package/docs/llms.md +463 -86
  50. package/docs/llms.txt +32 -19
  51. package/package.json +10 -17
  52. package/src/components/account-menu/account-menu.css +70 -0
  53. package/src/components/account-menu/account-menu.js +208 -0
  54. package/src/components/app-rail/app-rail.css +107 -0
  55. package/src/components/app-sidebar/app-sidebar.css +80 -0
  56. package/src/components/app-sidebar/app-sidebar.js +588 -0
  57. package/src/components/avatar/avatar.css +61 -0
  58. package/src/components/avatar/avatar.js +222 -0
  59. package/src/components/button/button.css +0 -1
  60. package/src/components/card/card.css +149 -0
  61. package/src/components/card/card.js +285 -0
  62. package/src/components/chart/chart.css +61 -0
  63. package/src/components/chart/chart.js +523 -0
  64. package/src/components/date-picker/month-picker.js +9 -0
  65. package/src/components/dialog/dialog.js +11 -4
  66. package/src/components/dock/dock.css +228 -0
  67. package/src/components/dock/dock.js +977 -0
  68. package/src/components/dropdown/dropdown.js +1 -1
  69. package/src/components/field/field.js +2 -0
  70. package/src/components/field-upload/field-upload.js +9 -4
  71. package/src/components/form/form.js +6 -3
  72. package/src/components/grid/grid.css +6 -0
  73. package/src/components/grid/grid.js +220 -0
  74. package/src/components/launcher/launcher.css +210 -0
  75. package/src/components/launcher/launcher.js +765 -0
  76. package/src/components/layout/layout.js +2 -2
  77. package/src/components/menu-button/menu-button.js +27 -2
  78. package/src/components/modal/modal.css +5 -1
  79. package/src/components/modal/modal.js +37 -2
  80. package/src/components/multi-value-editor/multi-value-editor.js +2 -1
  81. package/src/components/questionnaire/items.js +370 -0
  82. package/src/components/questionnaire/questionnaire.css +387 -0
  83. package/src/components/questionnaire/questionnaire.js +1033 -0
  84. package/src/components/select/field-adapter.js +9 -4
  85. package/src/components/select/select.css +144 -11
  86. package/src/components/select/select.js +259 -27
  87. package/src/components/sheet/sheet-stack.js +304 -0
  88. package/src/components/sheet/sheet.css +351 -0
  89. package/src/components/sheet/sheet.js +551 -0
  90. package/src/components/split-view/split-view.js +76 -161
  91. package/src/components/table/table.css +43 -0
  92. package/src/components/table/table.js +531 -52
  93. package/src/components/tooltip/tooltip.js +5 -2
  94. package/src/components/value-list/value-list.js +6 -3
  95. package/src/core/dom.js +322 -0
  96. package/src/core/drag-axis.js +203 -0
  97. package/src/core/position.js +68 -22
  98. package/src/index.js +20 -2
  99. package/src/internal/app-rail.js +718 -0
  100. package/src/zeyos/icons.js +8 -3
  101. package/src/zeyos/index.js +7 -1
  102. package/src/zeyos/launcher.js +338 -0
  103. package/src/zeyos/modules.js +1 -0
  104. package/src/zeyos/select.js +115 -1
  105. package/styles/tokens/dark.css +2 -3
  106. package/styles/tokens/global.css +6 -6
  107. package/styles/tokens/modules.css +1 -0
  108. package/styles/tokens/semantic.css +5 -3
  109. package/styles/tokens/themes.css +1 -1
  110. package/styles/zx.css +11 -1
  111. package/MIGRATION.md +0 -598
  112. package/dist/types/compat/base.d.ts +0 -156
  113. package/dist/types/compat/globals.d.ts +0 -20
  114. package/dist/types/compat/index.d.ts +0 -128
  115. package/dist/types/compat/map/basic.d.ts +0 -175
  116. package/dist/types/compat/map/data.d.ts +0 -102
  117. package/dist/types/compat/map/date-time.d.ts +0 -80
  118. package/dist/types/compat/map/factory.d.ts +0 -9
  119. package/dist/types/compat/map/form.d.ts +0 -98
  120. package/dist/types/compat/map/helpers.d.ts +0 -12
  121. package/dist/types/compat/map/http.d.ts +0 -36
  122. package/dist/types/compat/map/layout.d.ts +0 -106
  123. package/dist/types/compat/map/options.d.ts +0 -80
  124. package/dist/types/compat/map/select.d.ts +0 -95
  125. package/dist/types/compat/map/stubs.d.ts +0 -32
  126. package/dist/types/compat/map/utilities.d.ts +0 -37
  127. package/dist/types/compat-entry.d.ts +0 -5
  128. package/dist/types/components/permission/permission.d.ts +0 -96
  129. package/dist/zx-compat.esm.js +0 -17484
  130. package/dist/zx-compat.esm.min.js +0 -1
  131. package/dist/zx-compat.global.js +0 -23635
  132. package/dist/zx-compat.global.min.js +0 -3
  133. package/src/compat/base.js +0 -291
  134. package/src/compat/globals.js +0 -165
  135. package/src/compat/index.js +0 -84
  136. package/src/compat/map/basic.js +0 -464
  137. package/src/compat/map/data.js +0 -232
  138. package/src/compat/map/date-time.js +0 -177
  139. package/src/compat/map/factory.js +0 -45
  140. package/src/compat/map/form.js +0 -330
  141. package/src/compat/map/helpers.js +0 -46
  142. package/src/compat/map/http.js +0 -117
  143. package/src/compat/map/layout.js +0 -256
  144. package/src/compat/map/options.js +0 -267
  145. package/src/compat/map/select.js +0 -198
  146. package/src/compat/map/stubs.js +0 -26
  147. package/src/compat/map/utilities.js +0 -113
  148. package/src/compat-entry.js +0 -11
  149. package/src/components/permission/permission.css +0 -47
  150. package/src/components/permission/permission.js +0 -201
@@ -4,7 +4,10 @@ import { position } from '../../core/position.js';
4
4
  import { uid } from '../../core/util.js';
5
5
 
6
6
  /** Placements accepted by the option and understood by `position()`. */
7
- const PLACEMENTS = new Set(['top', 'bottom', 'top-start', 'top-end', 'bottom-start', 'bottom-end']);
7
+ const PLACEMENTS = new Set([
8
+ 'top', 'bottom', 'top-start', 'top-end', 'bottom-start', 'bottom-end',
9
+ 'left', 'right', 'left-start', 'left-end', 'right-start', 'right-end'
10
+ ]);
8
11
  /** Trigger modes. `manual` installs no listeners at all. */
9
12
  const TRIGGERS = new Set(['hover', 'focus', 'both', 'manual']);
10
13
 
@@ -21,7 +24,7 @@ const TRIGGERS = new Set(['hover', 'focus', 'both', 'manual']);
21
24
  /**
22
25
  * @typedef {Object} TooltipOptions
23
26
  * @property {TooltipContent} [content=''] Bubble content, re-evaluated on each open when a function.
24
- * @property {'top'|'bottom'|'top-start'|'top-end'|'bottom-start'|'bottom-end'} [placement='top'] Preferred side; flips when it would overflow.
27
+ * @property {'top'|'bottom'|'top-start'|'top-end'|'bottom-start'|'bottom-end'|'left'|'right'|'left-start'|'left-end'|'right-start'|'right-end'} [placement='top'] Preferred side; flips when it would overflow.
25
28
  * @property {number} [offset=6] Gap between anchor and bubble in pixels.
26
29
  * @property {number} [delay=400] Delay before a hover opens the tooltip, in milliseconds.
27
30
  * @property {number} [hideDelay=80] Delay before a leave or blur closes it, in milliseconds.
@@ -10,6 +10,9 @@ import { uid } from '../../core/util.js';
10
10
  * @property {boolean} [sortable=true] Whether values can be reordered.
11
11
  * @property {boolean} [unique=true] Whether duplicate strings are rejected.
12
12
  * @property {((value: string) => boolean|string)|null} [validate=null] Value validator.
13
+ * @property {(event: CustomEvent<{values: string[]}>) => void} [onchange] Values change listener.
14
+ * @property {(event: CustomEvent<{value: string}>) => void} [onadd] Value added listener.
15
+ * @property {(event: CustomEvent<{value: string}>) => void} [onremove] Value removed listener.
13
16
  */
14
17
 
15
18
  /**
@@ -320,17 +323,17 @@ function normalizeValues(values, unique) {
320
323
  /**
321
324
  * Values changed event.
322
325
  * @event ValueList#change
323
- * @type {CustomEvent<{values: string[]}>
326
+ * @type {CustomEvent<{values: string[]}>}
324
327
  */
325
328
 
326
329
  /**
327
330
  * Value added event.
328
331
  * @event ValueList#add
329
- * @type {CustomEvent<{value: string}>
332
+ * @type {CustomEvent<{value: string}>}
330
333
  */
331
334
 
332
335
  /**
333
336
  * Value removed event.
334
337
  * @event ValueList#remove
335
- * @type {CustomEvent<{value: string}>
338
+ * @type {CustomEvent<{value: string}>}
336
339
  */
package/src/core/dom.js CHANGED
@@ -3,6 +3,7 @@ import { escapeRegExp, isElement } from './util.js';
3
3
 
4
4
  /** @typedef {{toElement: () => Node|null}} ElementProvider */
5
5
  /** @typedef {Node|string|number|ElementProvider|null|undefined|Array<DomChild>} DomChild */
6
+ /** @typedef {Record<string, any>} DomBuilderProperties */
6
7
  /**
7
8
  * @typedef {Object} HProps
8
9
  * @property {string|string[]} [class] Class name or names.
@@ -14,6 +15,193 @@ import { escapeRegExp, isElement } from './util.js';
14
15
  /** @type {Record<string, Element>|null} */
15
16
  let activeRefs = null;
16
17
 
18
+ /** Properties the ZeyOS builder historically assigns through the DOM property rather than an attribute. */
19
+ const DOM_BUILDER_PROPERTIES = new Set([
20
+ 'href', 'id', 'lang', 'name', 'rel', 'src', 'target', 'title', 'value'
21
+ ]);
22
+
23
+ /** @type {WeakMap<EventTarget, Map<string, {handlers: Function[], listener: EventListener}>>} */
24
+ const compactEvents = new WeakMap();
25
+
26
+ /**
27
+ * @typedef {Object} CompactTag
28
+ * @property {string} tag Element tag, defaulting to `div`.
29
+ * @property {string} id Element id.
30
+ * @property {string} className Space-delimited class string.
31
+ */
32
+
33
+ /**
34
+ * Parses the compact ZeyOS builder grammar: `tag#id.class names`. The order intentionally matches
35
+ * the established function—class text begins at the first dot, then id is read from the remaining
36
+ * tag portion—so thousands of existing call sites retain the same meaning.
37
+ * @param {string} compact Compact element description.
38
+ * @returns {CompactTag}
39
+ */
40
+ export function parseCompactTag(compact) {
41
+ if (typeof compact !== 'string') throw new TypeError('DOM builder tag must be a string');
42
+ let tag = compact;
43
+ let className = '';
44
+ let id = '';
45
+ let index = tag.indexOf('.');
46
+ if (index !== -1) {
47
+ className = tag.slice(index + 1);
48
+ tag = tag.slice(0, index);
49
+ }
50
+ index = tag.indexOf('#');
51
+ if (index !== -1) {
52
+ id = tag.slice(index + 1);
53
+ tag = tag.slice(0, index);
54
+ }
55
+ return { tag: tag || 'div', id, className };
56
+ }
57
+
58
+ /**
59
+ * ZeyOS's compact DOM builder, now a first-class Zx primitive. Primitive content is assigned as
60
+ * text and object content is appended as a Node; HTML strings are never parsed.
61
+ *
62
+ * Existing syntax is preserved (`__('button.primary', {Daction: 'save'}, 'Save')`). Zx components
63
+ * exposing `toElement()` are the one additive extension, so a component may replace a UI factory
64
+ * without changing the surrounding builder call.
65
+ * @param {string} compact `tag#id.class names`; an omitted tag creates a `div`.
66
+ * @param {DomBuilderProperties|null} [properties=null] ZeyOS builder properties.
67
+ * @param {unknown} [content=null] Text-safe content.
68
+ * @returns {HTMLElement}
69
+ */
70
+ export function compactElement(compact, properties = null, content = null) {
71
+ const description = parseCompactTag(compact);
72
+ const element = document.createElement(description.tag);
73
+ if (description.id) element.id = description.id;
74
+ if (description.className) element.className = description.className;
75
+
76
+ if (description.tag === 'a') {
77
+ const anchor = /** @type {HTMLAnchorElement} */ (element);
78
+ anchor.rel = 'noopener';
79
+ anchor.tabIndex = -1;
80
+ } else if (description.tag === 'button') {
81
+ /** @type {HTMLButtonElement} */ (element).type = 'button';
82
+ } else if (description.tag === 'input' || description.tag === 'textarea') {
83
+ const control = /** @type {any} */ (element);
84
+ control.autocapitalize = 'none';
85
+ control.autocomplete = 'off';
86
+ control.autocorrect = 'off';
87
+ control.spellcheck = false;
88
+ }
89
+
90
+ if (properties) applyCompactProperties(element, properties);
91
+ setCompactContent(element, content);
92
+ return element;
93
+ }
94
+
95
+ /** Exact migration-facing alias retained for existing ZeyOS builder calls. */
96
+ export const __ = compactElement;
97
+
98
+ /**
99
+ * Returns a trimmed native-link destination unless its parsed scheme can execute document code.
100
+ * Relative URLs, fragments, and ordinary application protocols remain valid; malformed and
101
+ * control-obfuscated script/data schemes are rejected.
102
+ * @param {unknown} value Candidate href.
103
+ * @returns {string|null}
104
+ */
105
+ export function safeHref(value) {
106
+ if (typeof value !== 'string') return null;
107
+ const href = value.trim();
108
+ if (!href) return null;
109
+ try {
110
+ const protocol = new URL(href, globalThis.location?.href ?? 'https://zx.invalid/').protocol;
111
+ if (['javascript:', 'data:', 'vbscript:'].includes(protocol.toLowerCase())) return null;
112
+ } catch {
113
+ return null;
114
+ }
115
+ return href;
116
+ }
117
+
118
+ /**
119
+ * Applies the established ZeyOS property rules to an existing element:
120
+ * `Dname` → `data-name`, `SfontSize` → `style.fontSize`, `on*` → a legacy-compatible listener,
121
+ * booleans/built-ins → DOM properties, and everything else → attributes.
122
+ * @param {HTMLElement} element Target element.
123
+ * @param {DomBuilderProperties} properties Properties to apply.
124
+ * @returns {HTMLElement} The target, for chaining.
125
+ */
126
+ export function applyCompactProperties(element, properties) {
127
+ // `for…in` is intentional: the established builder also applies enumerable properties inherited
128
+ // from a shared descriptor prototype. Changing that during migration would silently drop them.
129
+ for (const name in properties ?? {}) {
130
+ const value = properties[name];
131
+ if (value == null) continue;
132
+ if (name[0] === 'D') {
133
+ element.setAttribute(`data-${name.slice(1)}`, String(value));
134
+ } else if (name[0] === 'S') {
135
+ element.style[name.slice(1)] = value;
136
+ } else if (name.startsWith('on')) {
137
+ if (typeof value !== 'function') {
138
+ throw new TypeError(`Compact DOM event ${name} must be a function`);
139
+ }
140
+ onCompactEvent(element, name.slice(2), value);
141
+ } else if (typeof value === 'boolean' || DOM_BUILDER_PROPERTIES.has(name)) {
142
+ element[name] = value;
143
+ } else {
144
+ element.setAttribute(name, String(value));
145
+ }
146
+ }
147
+ return element;
148
+ }
149
+
150
+ /**
151
+ * Appends one child and returns the parent: the pure-function equivalent of `parent._(child)`.
152
+ * Primitive values become text; a component exposing `toElement()` is accepted additively.
153
+ * @param {Node} parent Parent node.
154
+ * @param {unknown} child Child content.
155
+ * @returns {Node}
156
+ */
157
+ export function appendContent(parent, child) {
158
+ if (child == null) return parent;
159
+ const resolved = resolveCompactProvider(child);
160
+ if (resolved == null) return parent;
161
+ if (typeof resolved === 'object') parent.appendChild(/** @type {Node} */ (resolved));
162
+ else parent.appendChild(document.createTextNode(String(resolved)));
163
+ return parent;
164
+ }
165
+
166
+ /**
167
+ * Creates and appends a compact element, returning the parent: the pure equivalent of `parent.__`.
168
+ * @param {Node} parent Parent node.
169
+ * @param {string} compact Compact element description.
170
+ * @param {DomBuilderProperties|null} [properties=null] Builder properties.
171
+ * @param {unknown} [content=null] Text-safe content.
172
+ * @returns {Node}
173
+ */
174
+ export function appendCompact(parent, compact, properties = null, content = null) {
175
+ parent.appendChild(compactElement(compact, properties, content));
176
+ return parent;
177
+ }
178
+
179
+ /**
180
+ * Creates and appends a compact element, returning the child: the pure equivalent of `parent.$__`.
181
+ * @param {Node} parent Parent node.
182
+ * @param {string} compact Compact element description.
183
+ * @param {DomBuilderProperties|null} [properties=null] Builder properties.
184
+ * @param {unknown} [content=null] Text-safe content.
185
+ * @returns {HTMLElement}
186
+ */
187
+ export function appendCompactChild(parent, compact, properties = null, content = null) {
188
+ const child = compactElement(compact, properties, content);
189
+ parent.appendChild(child);
190
+ return child;
191
+ }
192
+
193
+ /**
194
+ * Creates a document fragment with normalized modern Zx children. Unlike the exact compact
195
+ * builder, this convenience accepts arrays and flattens them like `h()`.
196
+ * @param {...(DomChild|boolean)} children Fragment children.
197
+ * @returns {DocumentFragment}
198
+ */
199
+ export function fragment(...children) {
200
+ const result = document.createDocumentFragment();
201
+ appendChildren(result, /** @type {DomChild[]} */ (children));
202
+ return result;
203
+ }
204
+
17
205
  /**
18
206
  * Creates an element and appends normalized children.
19
207
  * @param {string} tag Element tag name.
@@ -258,3 +446,137 @@ function appendChildren(parent, children) {
258
446
  }
259
447
  }
260
448
  }
449
+
450
+ /**
451
+ * Applies the compact builder's exact single-content contract. Objects are passed to native
452
+ * `appendChild`, while primitives replace textContent. Providers are the explicit Zx extension.
453
+ * @param {HTMLElement} element Target element.
454
+ * @param {unknown} content Builder content.
455
+ * @returns {void}
456
+ */
457
+ function setCompactContent(element, content) {
458
+ if (content == null) return;
459
+ const resolved = resolveCompactProvider(content);
460
+ if (resolved == null) return;
461
+ if (typeof resolved === 'object') element.appendChild(/** @type {Node} */ (resolved));
462
+ else element.textContent = String(resolved);
463
+ }
464
+
465
+ /**
466
+ * Resolves the one additive component-provider extension without normalizing any other object.
467
+ * Arrays and plain objects therefore retain the native `appendChild` failure contract.
468
+ * @param {unknown} value Candidate content.
469
+ * @returns {unknown}
470
+ */
471
+ function resolveCompactProvider(value) {
472
+ if (value && typeof value === 'object') {
473
+ // A native Node always wins, even if application code attached a `toElement()` method to it.
474
+ // Classic passes every object straight to appendChild; provider resolution is additive only
475
+ // for component-like objects that are not already DOM nodes.
476
+ if (typeof (/** @type {any} */ (value)).nodeType === 'number') return value;
477
+ if (typeof (/** @type {any} */ (value)).toElement === 'function') {
478
+ return (/** @type {ElementProvider} */ (value)).toElement();
479
+ }
480
+ }
481
+ return value;
482
+ }
483
+
484
+ /**
485
+ * Registers an event with the established ZeyOS callback contract: handlers run newest-first,
486
+ * receive `(event, event.target)` (or the key code for keyboard events), and may return false to
487
+ * prevent the default action. This exported registry is shared by compact properties, removal,
488
+ * and synthetic firing so the legacy `DOM` facade can delegate all three operations to it.
489
+ * @param {EventTarget} target Event target.
490
+ * @param {string} type Event name.
491
+ * @param {Function} handler Listener.
492
+ * @param {boolean} [passive=false] Native passive-listener hint.
493
+ * @returns {EventTarget}
494
+ */
495
+ export function onCompactEvent(target, type, handler, passive = false) {
496
+ if (typeof handler !== 'function') throw new TypeError('Compact DOM event handler must be a function');
497
+ const eventType = String(type);
498
+ let events = compactEvents.get(target);
499
+ if (!events) {
500
+ events = new Map();
501
+ compactEvents.set(target, events);
502
+ }
503
+ const registered = events.get(eventType);
504
+ if (registered) {
505
+ registered.handlers.push(handler);
506
+ return target;
507
+ }
508
+ const handlers = [handler];
509
+ /** @type {EventListener} */
510
+ const listener = (event) => dispatchCompactEvent(target, handlers, event);
511
+ events.set(eventType, { handlers, listener });
512
+ try {
513
+ target.addEventListener(eventType, listener, { passive });
514
+ } catch {
515
+ target.addEventListener(eventType, listener);
516
+ }
517
+ return target;
518
+ }
519
+
520
+ /**
521
+ * Removes one registered handler, matching the established duplicate-registration behavior.
522
+ * @param {EventTarget} target Event target.
523
+ * @param {string} type Event name.
524
+ * @param {Function} handler Handler to remove.
525
+ * @returns {EventTarget}
526
+ */
527
+ export function offCompactEvent(target, type, handler) {
528
+ const eventType = String(type);
529
+ const events = compactEvents.get(target);
530
+ const registered = events?.get(eventType);
531
+ if (!registered) return target;
532
+ const index = registered.handlers.indexOf(handler);
533
+ if (index === -1) return target;
534
+ registered.handlers.splice(index, 1);
535
+ if (registered.handlers.length === 0) {
536
+ target.removeEventListener(eventType, registered.listener);
537
+ events.delete(eventType);
538
+ if (events.size === 0) compactEvents.delete(target);
539
+ }
540
+ return target;
541
+ }
542
+
543
+ /**
544
+ * Invokes compact handlers without dispatching unrelated native listeners.
545
+ * @param {EventTarget} target Event target.
546
+ * @param {string} type Event name.
547
+ * @param {Event|Record<string, any>|null} [event=null] Event-like object.
548
+ * @returns {EventTarget}
549
+ */
550
+ export function fireCompactEvent(target, type, event = null) {
551
+ const eventType = String(type);
552
+ const events = compactEvents.get(target);
553
+ // Match DOM.fireEvt's guard: a requested type must be registered before an event is considered.
554
+ if (!events?.has(eventType)) return target;
555
+ const actual = event ?? compactSyntheticEvent(target, eventType);
556
+ // DOM.handleEvt then dispatches by the supplied event's own type. This distinction matters for
557
+ // hand-built event objects even though normal ZeyOS callers omit the optional event argument.
558
+ const registered = events.get(String((/** @type {any} */ (actual)).type));
559
+ if (registered) dispatchCompactEvent(target, registered.handlers, actual);
560
+ return target;
561
+ }
562
+
563
+ /** @param {EventTarget} target @param {Function[]} handlers @param {Event|Record<string, any>} event */
564
+ function dispatchCompactEvent(target, handlers, event) {
565
+ const value = /** @type {any} */ (event);
566
+ const secondary = String(value.type).startsWith('key') ? value.which : value.target;
567
+ for (let index = handlers.length - 1; index >= 0; index -= 1) {
568
+ if (handlers[index].call(target, event, secondary) === false) value.preventDefault();
569
+ }
570
+ }
571
+
572
+ /** @param {EventTarget} target @param {string} type @returns {Record<string, any>} */
573
+ function compactSyntheticEvent(target, type) {
574
+ const event = {
575
+ target,
576
+ type,
577
+ which: -1,
578
+ defaultPrevented: false,
579
+ preventDefault() { event.defaultPrevented = true; }
580
+ };
581
+ return event;
582
+ }
@@ -0,0 +1,203 @@
1
+ // @ts-check
2
+
3
+ /** Pixels an arrow key moves a handle. */
4
+ export const STEP = 16;
5
+ /** Pixels a shifted arrow key moves a handle. */
6
+ export const STEP_LARGE = 64;
7
+
8
+ /**
9
+ * @typedef {Object} DragAxisOptions
10
+ * @property {'horizontal'|'vertical'} orientation Axis the handle moves along. `horizontal` reads
11
+ * `clientX`, `vertical` reads `clientY`.
12
+ * @property {(event: PointerEvent) => boolean|void} [onStart] Called on pointer-down before the
13
+ * drag begins. Return `false` to refuse it — the usual reason being that there is nothing to
14
+ * resize on either side of the handle.
15
+ * @property {(delta: number) => void} onMove Called at most once per frame with the signed pixel
16
+ * distance travelled along the axis since pointer-down.
17
+ * @property {(delta: number, moved: boolean) => void} [onEnd] Called once the pointer is released.
18
+ * `moved` is false for a press that never travelled, which is a click rather than a drag.
19
+ * @property {(direction: -1|1, large: boolean) => void} [onStep] Called for an arrow key along the
20
+ * axis. `large` is true while Shift is held.
21
+ * @property {(edge: 'min'|'max') => void} [onJump] Called for Home and End.
22
+ * @property {() => void} [onActivate] Called for Enter and Space, which components use for the
23
+ * collapse affordance a splitter is expected to offer.
24
+ * @property {() => boolean} [disabled] Consulted before every interaction.
25
+ */
26
+
27
+ /**
28
+ * @typedef {Object} DragAxisController
29
+ * @property {() => boolean} isDragging Whether a drag is in flight.
30
+ * @property {() => void} cancel Ends any drag in flight without reporting it.
31
+ * @property {() => void} destroy Removes listeners and cancels any drag in flight.
32
+ */
33
+
34
+ /**
35
+ * Pointer and keyboard mechanics for anything dragged along one axis — a split divider, a dock
36
+ * divider, the resizable edge of a sheet.
37
+ *
38
+ * Deliberately knows nothing about sizes. It reports the signed distance travelled and leaves
39
+ * every consumer to do its own arithmetic, because the three that need it disagree about what a
40
+ * drag means: a split view moves one boundary, a dock moves size between two neighbours, and a
41
+ * sheet grows in the opposite direction depending on which edge it is attached to. What they do
42
+ * share is the fiddly part — pointer capture that survives the pointer leaving the handle, one
43
+ * write per frame however fast the pointer reports, telling a click apart from a zero-distance
44
+ * drag, and the WAI-ARIA keyboard map.
45
+ *
46
+ * @param {Element} handle Element the pointer grabs; also the keyboard target.
47
+ * @param {DragAxisOptions} options Axis and callbacks.
48
+ * @returns {DragAxisController}
49
+ */
50
+ export function dragAxis(handle, options) {
51
+ if (!handle) throw new TypeError('dragAxis requires a handle element');
52
+ if (typeof options?.onMove !== 'function') throw new TypeError('dragAxis requires an onMove callback');
53
+
54
+ const vertical = options.orientation === 'vertical';
55
+ const controller = new AbortController();
56
+ const { signal } = controller;
57
+
58
+ let dragging = false;
59
+ /** @type {number|null} */
60
+ let pointerId = null;
61
+ let startClient = 0;
62
+ let client = 0;
63
+ let moved = false;
64
+ /** Handle of the frame a pending update is scheduled in; `0` if none. */
65
+ let frame = 0;
66
+
67
+ /** @param {PointerEvent} event @returns {number} */
68
+ const axis = (event) => (vertical ? event.clientY : event.clientX);
69
+
70
+ /** @returns {boolean} */
71
+ const blocked = () => Boolean(options.disabled?.());
72
+
73
+ /** @returns {void} */
74
+ const releasePointer = () => {
75
+ const id = pointerId;
76
+ pointerId = null;
77
+ if (id === null) return;
78
+ try {
79
+ /** @type {HTMLElement} */ (handle).releasePointerCapture(id);
80
+ } catch {
81
+ // The capture is already gone once the pointer was cancelled or the node was detached.
82
+ }
83
+ };
84
+
85
+ /** @returns {void} */
86
+ const cancelFrame = () => {
87
+ if (frame === 0) return;
88
+ cancelAnimationFrame(frame);
89
+ frame = 0;
90
+ };
91
+
92
+ /** @returns {void} */
93
+ const finish = () => {
94
+ dragging = false;
95
+ moved = false;
96
+ cancelFrame();
97
+ releasePointer();
98
+ };
99
+
100
+ handle.addEventListener('pointerdown', (event) => {
101
+ const pointer = /** @type {PointerEvent} */ (event);
102
+ if (blocked() || dragging || pointer.button !== 0) return;
103
+ if (options.onStart?.(pointer) === false) return;
104
+ /*
105
+ * No preventDefault(): cancelling pointerdown suppresses the compatibility mouse events a
106
+ * double-click reset is built on. Handles set `user-select: none` in CSS to stop the text
107
+ * selection this would otherwise be guarding against.
108
+ */
109
+ dragging = true;
110
+ pointerId = pointer.pointerId;
111
+ startClient = axis(pointer);
112
+ client = startClient;
113
+ moved = false;
114
+ try {
115
+ /** @type {HTMLElement} */ (handle).setPointerCapture(pointer.pointerId);
116
+ } catch {
117
+ // A pointer that ended between the event and this call cannot be captured; the drag simply
118
+ // finishes on the next pointerup.
119
+ }
120
+ }, { signal });
121
+
122
+ handle.addEventListener('pointermove', (event) => {
123
+ const pointer = /** @type {PointerEvent} */ (event);
124
+ if (!dragging || pointer.pointerId !== pointerId) return;
125
+ client = axis(pointer);
126
+ if (client !== startClient) moved = true;
127
+ if (frame !== 0) return;
128
+ frame = requestAnimationFrame(() => {
129
+ frame = 0;
130
+ if (dragging) options.onMove(client - startClient);
131
+ });
132
+ }, { signal });
133
+
134
+ /** @param {Event} event @returns {void} */
135
+ const end = (event) => {
136
+ const pointer = /** @type {PointerEvent} */ (event);
137
+ if (!dragging || pointer.pointerId !== pointerId) return;
138
+ cancelFrame();
139
+ client = axis(pointer);
140
+ const travelled = moved;
141
+ const delta = client - startClient;
142
+ finish();
143
+ options.onEnd?.(delta, travelled);
144
+ };
145
+ handle.addEventListener('pointerup', end, { signal });
146
+ handle.addEventListener('pointercancel', end, { signal });
147
+
148
+ handle.addEventListener('keydown', (event) => {
149
+ const key = /** @type {KeyboardEvent} */ (event);
150
+ if (blocked()) return;
151
+ if ((key.key === 'Enter' || key.key === ' ') && options.onActivate) {
152
+ key.preventDefault();
153
+ options.onActivate();
154
+ return;
155
+ }
156
+ if (key.key === 'Home' || key.key === 'End') {
157
+ if (!options.onJump) return;
158
+ key.preventDefault();
159
+ options.onJump(key.key === 'Home' ? 'min' : 'max');
160
+ return;
161
+ }
162
+ const decrease = vertical ? 'ArrowUp' : 'ArrowLeft';
163
+ const increase = vertical ? 'ArrowDown' : 'ArrowRight';
164
+ if (key.key !== decrease && key.key !== increase) return;
165
+ if (!options.onStep) return;
166
+ key.preventDefault();
167
+ options.onStep(key.key === increase ? 1 : -1, key.shiftKey);
168
+ }, { signal });
169
+
170
+ return {
171
+ isDragging: () => dragging,
172
+ cancel() {
173
+ if (!dragging) return;
174
+ finish();
175
+ },
176
+ destroy() {
177
+ finish();
178
+ controller.abort();
179
+ }
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Resolves a size option to pixels without touching the DOM.
185
+ *
186
+ * Numbers, bare numeric strings, and `px` lengths are pixels; a percentage resolves against
187
+ * `total`. Every other CSS length — `rem`, `vh`, `calc()` — needs the browser, so it returns `NaN`
188
+ * and the caller hands the value to CSS instead of guessing at it.
189
+ * @param {number|string} value Size option.
190
+ * @param {number} total Container size along the split axis, in pixels.
191
+ * @returns {number} Pixels, or `NaN` when only the browser can resolve the value.
192
+ */
193
+ export function resolveSize(value, total) {
194
+ if (typeof value === 'number') return Number.isFinite(value) ? value : Number.NaN;
195
+ if (typeof value !== 'string') return Number.NaN;
196
+ const match = /^([+-]?(?:\d+\.?\d*|\.\d+))(px|%)?$/i.exec(value.trim());
197
+ if (!match) return Number.NaN;
198
+ const amount = Number(match[1]);
199
+ if (!Number.isFinite(amount)) return Number.NaN;
200
+ if ((match[2] ?? '').toLowerCase() !== '%') return amount;
201
+ const span = Number(total);
202
+ return Number.isFinite(span) ? (amount / 100) * span : Number.NaN;
203
+ }