@hypen-space/web 0.5.2 → 0.5.3

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 (104) hide show
  1. package/dist/canvas/accessibility.d.ts +118 -11
  2. package/dist/canvas/accessibility.js +470 -108
  3. package/dist/canvas/accessibility.js.map +1 -1
  4. package/dist/canvas/dispatch.d.ts +24 -0
  5. package/dist/canvas/dispatch.js +62 -0
  6. package/dist/canvas/dispatch.js.map +1 -0
  7. package/dist/canvas/editing.d.ts +178 -0
  8. package/dist/canvas/editing.js +590 -0
  9. package/dist/canvas/editing.js.map +1 -0
  10. package/dist/canvas/events.d.ts +28 -23
  11. package/dist/canvas/events.js +76 -128
  12. package/dist/canvas/events.js.map +1 -1
  13. package/dist/canvas/focus.d.ts +78 -0
  14. package/dist/canvas/focus.js +182 -0
  15. package/dist/canvas/focus.js.map +1 -0
  16. package/dist/canvas/index.d.ts +3 -1
  17. package/dist/canvas/index.js +3 -1
  18. package/dist/canvas/index.js.map +1 -1
  19. package/dist/canvas/paint.d.ts +16 -3
  20. package/dist/canvas/paint.js +283 -41
  21. package/dist/canvas/paint.js.map +1 -1
  22. package/dist/canvas/renderer.d.ts +20 -1
  23. package/dist/canvas/renderer.js +181 -50
  24. package/dist/canvas/renderer.js.map +1 -1
  25. package/dist/canvas/selection.js +31 -177
  26. package/dist/canvas/selection.js.map +1 -1
  27. package/dist/canvas/text-geometry.d.ts +72 -0
  28. package/dist/canvas/text-geometry.js +244 -0
  29. package/dist/canvas/text-geometry.js.map +1 -0
  30. package/dist/canvas/text.js +42 -9
  31. package/dist/canvas/text.js.map +1 -1
  32. package/dist/canvas/types.d.ts +7 -1
  33. package/dist/canvas/variants.d.ts +4 -1
  34. package/dist/canvas/variants.js +22 -3
  35. package/dist/canvas/variants.js.map +1 -1
  36. package/dist/dom/a11y-styles.d.ts +20 -0
  37. package/dist/dom/a11y-styles.js +61 -0
  38. package/dist/dom/a11y-styles.js.map +1 -0
  39. package/dist/dom/applicators/aria.d.ts +19 -0
  40. package/dist/dom/applicators/aria.js +36 -0
  41. package/dist/dom/applicators/aria.js.map +1 -0
  42. package/dist/dom/applicators/events.d.ts +7 -0
  43. package/dist/dom/applicators/events.js +35 -8
  44. package/dist/dom/applicators/events.js.map +1 -1
  45. package/dist/dom/applicators/index.js +4 -0
  46. package/dist/dom/applicators/index.js.map +1 -1
  47. package/dist/dom/components/hypenapp.d.ts +25 -1
  48. package/dist/dom/components/hypenapp.js +213 -245
  49. package/dist/dom/components/hypenapp.js.map +1 -1
  50. package/dist/dom/components/index.js +7 -0
  51. package/dist/dom/components/index.js.map +1 -1
  52. package/dist/dom/components/spinner.js +24 -10
  53. package/dist/dom/components/spinner.js.map +1 -1
  54. package/dist/dom/components/tabs.d.ts +27 -0
  55. package/dist/dom/components/tabs.js +69 -0
  56. package/dist/dom/components/tabs.js.map +1 -0
  57. package/dist/dom/components/visuallyhidden.d.ts +9 -0
  58. package/dist/dom/components/visuallyhidden.js +26 -0
  59. package/dist/dom/components/visuallyhidden.js.map +1 -0
  60. package/dist/dom/operability.d.ts +100 -0
  61. package/dist/dom/operability.js +298 -0
  62. package/dist/dom/operability.js.map +1 -0
  63. package/dist/dom/renderer.d.ts +98 -5
  64. package/dist/dom/renderer.js +398 -63
  65. package/dist/dom/renderer.js.map +1 -1
  66. package/dist/dom/route-focus.d.ts +42 -0
  67. package/dist/dom/route-focus.js +88 -0
  68. package/dist/dom/route-focus.js.map +1 -0
  69. package/dist/dom/semantics.d.ts +35 -0
  70. package/dist/dom/semantics.js +184 -0
  71. package/dist/dom/semantics.js.map +1 -0
  72. package/package.json +2 -2
  73. package/src/canvas/QUICKSTART.md +4 -4
  74. package/src/canvas/README.md +55 -29
  75. package/src/canvas/accessibility.ts +507 -115
  76. package/src/canvas/dispatch.ts +78 -0
  77. package/src/canvas/editing.ts +697 -0
  78. package/src/canvas/events.ts +89 -142
  79. package/src/canvas/focus.ts +216 -0
  80. package/src/canvas/index.ts +8 -1
  81. package/src/canvas/paint.ts +339 -42
  82. package/src/canvas/renderer.ts +218 -70
  83. package/src/canvas/selection.ts +52 -210
  84. package/src/canvas/text-geometry.ts +311 -0
  85. package/src/canvas/text.ts +43 -9
  86. package/src/canvas/types.ts +14 -1
  87. package/src/canvas/variants.ts +24 -5
  88. package/src/dom/a11y-styles.ts +65 -0
  89. package/src/dom/applicators/aria.ts +41 -0
  90. package/src/dom/applicators/events.ts +38 -7
  91. package/src/dom/applicators/index.ts +5 -0
  92. package/src/dom/components/hypenapp.ts +244 -272
  93. package/src/dom/components/index.ts +7 -0
  94. package/src/dom/components/spinner.ts +31 -13
  95. package/src/dom/components/tabs.ts +76 -0
  96. package/src/dom/components/visuallyhidden.ts +30 -0
  97. package/src/dom/operability.ts +312 -0
  98. package/src/dom/renderer.ts +455 -63
  99. package/src/dom/route-focus.ts +98 -0
  100. package/src/dom/semantics.ts +199 -0
  101. package/dist/canvas/input.d.ts +0 -76
  102. package/dist/canvas/input.js +0 -207
  103. package/dist/canvas/input.js.map +0 -1
  104. package/src/canvas/input.ts +0 -251
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Route-change focus management.
3
+ *
4
+ * When the Router swaps routes, keyboard and screen-reader focus is left on
5
+ * an element that just left the document — the user is stranded and AT
6
+ * announces nothing. This module defines the focus contract for navigation
7
+ * (design Open Question #4) and the DOM renderer applies it after each patch
8
+ * batch:
9
+ *
10
+ * 1. A batch is a **navigation** when it contains at least one `detach` (the
11
+ * leaving route, unlinked but kept alive by the Router cache) and at least
12
+ * one incoming subtree root (an `attach` of a cached route, or a fresh
13
+ * `create`+`insert` route build).
14
+ * 2. On navigation, focus moves to — in order of preference:
15
+ * a. the element that held focus when the incoming subtree was detached
16
+ * (**focus restore** on cached re-entry), if it is still inside the
17
+ * subtree;
18
+ * b. the subtree's first heading (`h1`–`h6` / `role="heading"`) or `main`
19
+ * landmark — the "start of the new page" for AT users;
20
+ * c. the subtree root itself.
21
+ * Non-natively-focusable targets get `tabindex="-1"` (programmatic focus
22
+ * only — they do not join the Tab order).
23
+ * 3. Focus-restore memory is keyed by the detached subtree's root NodeId and
24
+ * is dropped the moment a `remove` arrives for that id (Router LRU
25
+ * eviction) — **focus restore never targets an evicted NodeId**, the
26
+ * invariant this contract exists to guarantee.
27
+ *
28
+ * The contract is opt-out: `DOMRendererOptions.routeFocus: "off"` disables it
29
+ * for apps that own focus management themselves.
30
+ */
31
+
32
+ /** Tags that are natively focusable without a tabindex. */
33
+ const NATIVELY_FOCUSABLE = new Set(["A", "BUTTON", "INPUT", "TEXTAREA", "SELECT"]);
34
+
35
+ /** Heading hosts (`<h1>`–`<h6>`). */
36
+ const HEADING_TAGS = new Set(["H1", "H2", "H3", "H4", "H5", "H6"]);
37
+
38
+ type ElementLike = {
39
+ tagName?: string;
40
+ children?: ArrayLike<unknown>;
41
+ getAttribute?(name: string): string | null;
42
+ };
43
+
44
+ const matches = (el: ElementLike, pred: (el: ElementLike) => boolean): boolean => {
45
+ try {
46
+ return pred(el);
47
+ } catch {
48
+ return false;
49
+ }
50
+ };
51
+
52
+ const isHeading = (el: ElementLike): boolean =>
53
+ HEADING_TAGS.has(el.tagName?.toUpperCase() ?? "") ||
54
+ el.getAttribute?.("role") === "heading";
55
+
56
+ const isMain = (el: ElementLike): boolean =>
57
+ el.tagName?.toUpperCase() === "MAIN" || el.getAttribute?.("role") === "main";
58
+
59
+ /**
60
+ * Depth-first search over `children` (works on both real DOM collections and
61
+ * test-double arrays) for the first element matching `pred`.
62
+ */
63
+ function findFirst(root: ElementLike, pred: (el: ElementLike) => boolean): ElementLike | null {
64
+ const children = Array.from((root.children ?? []) as ArrayLike<ElementLike>);
65
+ for (const child of children) {
66
+ if (matches(child, pred)) return child;
67
+ const nested = findFirst(child, pred);
68
+ if (nested) return nested;
69
+ }
70
+ return null;
71
+ }
72
+
73
+ /**
74
+ * Pick the element that should receive focus inside a freshly-shown route
75
+ * subtree: first heading, else first `main` landmark, else the root itself.
76
+ */
77
+ export function findRouteFocusTarget(root: HTMLElement): HTMLElement {
78
+ const heading = findFirst(root as ElementLike, isHeading);
79
+ if (heading) return heading as HTMLElement;
80
+ const main = findFirst(root as ElementLike, isMain);
81
+ if (main) return main as HTMLElement;
82
+ return root;
83
+ }
84
+
85
+ /**
86
+ * Move focus to `target`, granting it programmatic focusability
87
+ * (`tabindex="-1"`) when it is neither natively focusable nor already
88
+ * carrying a tabindex. `-1` keeps it out of the Tab order — this is a
89
+ * navigation landing point, not a new tab stop.
90
+ */
91
+ export function focusRouteTarget(target: HTMLElement): void {
92
+ const tag = target.tagName?.toUpperCase() ?? "";
93
+ const hasTabindex = target.getAttribute?.("tabindex") != null;
94
+ if (!NATIVELY_FOCUSABLE.has(tag) && !hasTabindex) {
95
+ target.setAttribute?.("tabindex", "-1");
96
+ }
97
+ target.focus?.();
98
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Accessibility semantics → DOM.
3
+ *
4
+ * The engine derives a platform-neutral {@link Semantics} block (role, heading
5
+ * level, busy, and in later phases name/state/hidden/…) and ships it on
6
+ * `create` patches. This module is the DOM translation: it maps that block
7
+ * onto ARIA / native HTML.
8
+ *
9
+ * Guiding rule: **let native HTML do the work; never emit a redundant or
10
+ * misplaced role.** Most Hypen components render a semantic native element
11
+ * whose tag already conveys the correct role (`<button>`, `<a>`, `<p>`,
12
+ * `<img>`, `<input>`, `<select>`, `<textarea>`, `<h1>`–`<h6>`). Some render a
13
+ * wrapper around the real control (`Checkbox`/`Switch` are a `<label>` around a
14
+ * nested `<input>`), where putting the role on the wrapper would be flat wrong.
15
+ *
16
+ * So we only apply an explicit `role` when the host is a *generic* container
17
+ * (`<div>`/`<span>`) that conveys nothing on its own — which is exactly the
18
+ * handful of cases that need it (`Spinner`, `ProgressBar`). Everywhere else the
19
+ * native element is already correct, and the {@link Semantics} block still
20
+ * travels to the non-DOM renderers (Canvas shadow, iOS, Android) that lack
21
+ * native semantics.
22
+ */
23
+
24
+ import type { Semantics } from "@hypen-space/core/types";
25
+
26
+ /**
27
+ * The implicit ARIA role each native host tag already conveys. An engine
28
+ * role equal to the implicit one is never applied (redundant-role smell:
29
+ * `role="button"` on `<button>`), but a *different* engine role is an
30
+ * intentional override and IS applied — `role="tab"` on a `<button>` host,
31
+ * `role="combobox"` on an `<input>` (the ARIA 1.2 combobox pattern).
32
+ *
33
+ * `LABEL` is special-cased below: it never takes a role at all, because
34
+ * Checkbox/Switch render a `<label>` *wrapper* whose role belongs to the
35
+ * nested `<input>` — putting it on the wrapper would be flat wrong.
36
+ */
37
+ const IMPLICIT_ROLE: Record<string, string> = {
38
+ A: "link",
39
+ P: "paragraph",
40
+ IMG: "img",
41
+ INPUT: "textbox",
42
+ TEXTAREA: "textbox",
43
+ SELECT: "listbox",
44
+ BUTTON: "button",
45
+ H1: "heading",
46
+ H2: "heading",
47
+ H3: "heading",
48
+ H4: "heading",
49
+ H5: "heading",
50
+ H6: "heading",
51
+ };
52
+
53
+ /**
54
+ * Attributes this module set on an element on its previous pass, so a
55
+ * re-apply (a reactive `setSemantics` patch) can clear exactly what it — and
56
+ * only it — owns. Tracking per element instead of blanket-removing every
57
+ * possible ARIA attribute keeps the `.aria()` escape hatch's attributes (and
58
+ * anything else outside this module) untouched.
59
+ */
60
+ const appliedAttrs = new WeakMap<HTMLElement, string[]>();
61
+
62
+ /**
63
+ * Apply derived accessibility semantics to a DOM element.
64
+ *
65
+ * Called at create (first paint) and again on every `setSemantics` patch
66
+ * with the node's complete re-resolved block. Idempotent, and **clearing**:
67
+ * an attribute this module set on a previous pass that the new block no
68
+ * longer produces is removed (a name reverting from explicit to derived
69
+ * must drop its stale `aria-label`, a cleared `.expanded` its
70
+ * `aria-expanded`). No-op when `semantics` was never present, so
71
+ * non-semantic nodes pay nothing.
72
+ */
73
+ export function applySemantics(element: HTMLElement, semantics?: Semantics): void {
74
+ if (!semantics && !appliedAttrs.has(element)) return;
75
+
76
+ // Everything this pass wants set, collected first so the diff against the
77
+ // previous pass is a plain list comparison.
78
+ const next: Array<[string, string]> = [];
79
+
80
+ if (semantics) {
81
+ if (semantics.hidden) {
82
+ // Decorative: remove from the accessibility tree entirely. Nothing
83
+ // else applies once hidden.
84
+ next.push(["aria-hidden", "true"]);
85
+ } else {
86
+ // tagName is uppercase in real DOM; normalize so the lookup is robust
87
+ // against test doubles that preserve the as-created case.
88
+ const tag = element.tagName?.toUpperCase();
89
+
90
+ if (
91
+ semantics.role &&
92
+ tag !== "LABEL" &&
93
+ IMPLICIT_ROLE[tag] !== semantics.role
94
+ ) {
95
+ // Applied when the host conveys nothing (div/span) or when the
96
+ // engine role intentionally overrides the tag's implicit role
97
+ // (`role="tab"` on a <button>). Skipped when redundant, and always
98
+ // skipped on <label> wrappers (the role belongs to the nested input).
99
+ next.push(["role", semantics.role]);
100
+ }
101
+
102
+ // An explicit author label overrides visible content, so it is applied
103
+ // as aria-label even on native elements. A *derived* name is
104
+ // deliberately not applied — the browser already exposes it from the
105
+ // visible content.
106
+ if (semantics.nameExplicit && semantics.name) {
107
+ next.push(["aria-label", semantics.name]);
108
+ }
109
+
110
+ if (semantics.description) {
111
+ next.push(["aria-description", semantics.description]);
112
+ }
113
+
114
+ // Self-state relationship attributes.
115
+ if (semantics.expanded !== undefined) {
116
+ next.push(["aria-expanded", String(semantics.expanded)]);
117
+ }
118
+ if (semantics.pressed !== undefined) {
119
+ next.push(["aria-pressed", String(semantics.pressed)]);
120
+ }
121
+ if (semantics.selected !== undefined) {
122
+ next.push(["aria-selected", String(semantics.selected)]);
123
+ }
124
+ if (semantics.current) {
125
+ next.push(["aria-current", semantics.current]);
126
+ }
127
+ if (semantics.checked !== undefined) {
128
+ next.push(["aria-checked", String(semantics.checked)]);
129
+ }
130
+ if (semantics.invalid !== undefined) {
131
+ // Reactive form validity — kept live via `setSemantics` re-emits;
132
+ // clearing (block drops the field) removes the attribute below.
133
+ next.push(["aria-invalid", String(semantics.invalid)]);
134
+ }
135
+
136
+ // Cross-node relationships by author-supplied id. DOM-only /
137
+ // web-leaning: these id-reference relationships have no equivalent in
138
+ // the string-hint native accessibility APIs, so they do not travel to
139
+ // Canvas / iOS / Android. See the guide's "Platform support" section
140
+ // (hypen-docs/content/docs/guide/accessibility.mdx).
141
+ if (semantics.id) {
142
+ // The anchor the references below resolve against — a real DOM id.
143
+ next.push(["id", semantics.id]);
144
+ }
145
+ if (semantics.dir) {
146
+ // Base text direction → the native HTML dir attribute (not ARIA).
147
+ // Managed like `id`: cleared when a later block drops it.
148
+ next.push(["dir", semantics.dir]);
149
+ }
150
+ if (semantics.controls) {
151
+ next.push(["aria-controls", semantics.controls]);
152
+ }
153
+ if (semantics.describedby) {
154
+ next.push(["aria-describedby", semantics.describedby]);
155
+ }
156
+ if (semantics.labelledby) {
157
+ next.push(["aria-labelledby", semantics.labelledby]);
158
+ }
159
+ if (semantics.activeDescendant) {
160
+ // Reactive roving-focus pointer — arrives initially on `create` and
161
+ // stays live via `setSemantics` re-emits as the bound state changes.
162
+ next.push(["aria-activedescendant", semantics.activeDescendant]);
163
+ }
164
+ if (semantics.owns) {
165
+ next.push(["aria-owns", semantics.owns]);
166
+ }
167
+
168
+ if (semantics.busy) {
169
+ next.push(["aria-busy", "true"]);
170
+ }
171
+
172
+ if (semantics.live) {
173
+ // Live-region politeness — engine-validated ("polite" | "assertive").
174
+ next.push(["aria-live", semantics.live]);
175
+ }
176
+ }
177
+ }
178
+
179
+ // Clear what the previous pass set but this one doesn't.
180
+ const previous = appliedAttrs.get(element);
181
+ if (previous) {
182
+ const keep = new Set(next.map(([name]) => name));
183
+ for (const name of previous) {
184
+ if (!keep.has(name)) {
185
+ element.removeAttribute(name);
186
+ }
187
+ }
188
+ }
189
+
190
+ for (const [name, value] of next) {
191
+ element.setAttribute(name, value);
192
+ }
193
+
194
+ if (next.length > 0) {
195
+ appliedAttrs.set(element, next.map(([name]) => name));
196
+ } else {
197
+ appliedAttrs.delete(element);
198
+ }
199
+ }
@@ -1,76 +0,0 @@
1
- /**
2
- * Input Overlay System
3
- *
4
- * Handle text input using DOM overlay elements
5
- */
6
- import type { VirtualNode } from "./types.js";
7
- /** Minimal engine interface for dispatching bind actions */
8
- interface BindEngine {
9
- dispatchAction(name: string, payload?: unknown): void;
10
- }
11
- /**
12
- * Input Overlay Manager
13
- */
14
- export declare class InputOverlay {
15
- private container;
16
- private overlay;
17
- private focusedNode;
18
- private onChangeCallback;
19
- private bindPath;
20
- private engine;
21
- constructor(container: HTMLElement | null);
22
- /**
23
- * Show input overlay for a node.
24
- *
25
- * Type comparison is case-insensitive — the engine emits `"Input"` /
26
- * `"Textarea"`, but the overlay branch was looking for lowercase
27
- * `"textarea"` and always created an `<input>` (so multi-line typing
28
- * couldn't enter newlines and the visible caret was the wrong height).
29
- */
30
- showInput(node: VirtualNode, canvasBounds: DOMRect, onChange: (value: string) => void, engine?: BindEngine | null): void;
31
- /**
32
- * Hide input overlay
33
- */
34
- hideInput(): void;
35
- /**
36
- * Update overlay position
37
- */
38
- updatePosition(node: VirtualNode, canvasBounds: DOMRect): void;
39
- /**
40
- * Style overlay to match canvas node
41
- */
42
- private styleOverlay;
43
- /**
44
- * Position overlay over canvas node.
45
- *
46
- * `bounds` is in canvas-local CSS pixels (the layout coordinate space
47
- * the renderer paints into after `ctx.scale(dpr, dpr)`). The overlay is
48
- * mounted on `document.body`, so we add `canvasBounds.left/top`
49
- * (page coords from `getBoundingClientRect`) to land at the right
50
- * absolute position regardless of where the canvas sits on the page.
51
- * Adding `window.scrollX/Y` keeps the overlay pinned to the canvas
52
- * even when the page scrolls between focus and reposition.
53
- */
54
- private positionOverlay;
55
- /**
56
- * Handle input event
57
- */
58
- private onInput;
59
- /**
60
- * Handle blur event
61
- */
62
- private onBlur;
63
- /**
64
- * Handle keyboard events
65
- */
66
- private onKeyDown;
67
- /**
68
- * Check if input is currently shown
69
- */
70
- isShown(): boolean;
71
- /**
72
- * Get current focused node
73
- */
74
- getFocusedNode(): VirtualNode | null;
75
- }
76
- export {};
@@ -1,207 +0,0 @@
1
- /**
2
- * Input Overlay System
3
- *
4
- * Handle text input using DOM overlay elements
5
- */
6
- import { getAbsoluteBounds, cssLengthToPx } from "./utils.js";
7
- /**
8
- * Input Overlay Manager
9
- */
10
- export class InputOverlay {
11
- container;
12
- overlay = null;
13
- focusedNode = null;
14
- onChangeCallback = null;
15
- bindPath = null;
16
- engine = null;
17
- constructor(container) {
18
- this.container = container || {};
19
- }
20
- /**
21
- * Show input overlay for a node.
22
- *
23
- * Type comparison is case-insensitive — the engine emits `"Input"` /
24
- * `"Textarea"`, but the overlay branch was looking for lowercase
25
- * `"textarea"` and always created an `<input>` (so multi-line typing
26
- * couldn't enter newlines and the visible caret was the wrong height).
27
- */
28
- showInput(node, canvasBounds, onChange, engine) {
29
- // Skip if document is not available (non-browser environment)
30
- if (typeof document === "undefined")
31
- return;
32
- this.hideInput();
33
- // Store bind path and engine for two-way binding
34
- this.bindPath = node.props.bind || null;
35
- this.engine = engine || null;
36
- const bounds = getAbsoluteBounds(node);
37
- if (!bounds)
38
- return;
39
- const isMultiline = node.type.toLowerCase() === "textarea";
40
- this.overlay = isMultiline
41
- ? document.createElement("textarea")
42
- : document.createElement("input");
43
- // Style overlay
44
- this.styleOverlay(node, bounds, canvasBounds);
45
- // Set initial value
46
- const value = node.props.value || "";
47
- this.overlay.value = value;
48
- // Setup event handlers
49
- this.onChangeCallback = onChange;
50
- this.overlay.addEventListener("input", this.onInput.bind(this));
51
- this.overlay.addEventListener("blur", this.onBlur.bind(this));
52
- this.overlay.addEventListener("keydown", this.onKeyDown.bind(this));
53
- // Mount on document.body so absolute positioning is relative to the
54
- // viewport (`canvasBounds.left/top` are page coords too) — no
55
- // dependency on the canvas's wrapper having `position: relative`.
56
- document.body.appendChild(this.overlay);
57
- this.overlay.focus();
58
- this.focusedNode = node;
59
- }
60
- /**
61
- * Hide input overlay
62
- */
63
- hideInput() {
64
- if (this.overlay) {
65
- this.overlay.remove();
66
- this.overlay = null;
67
- }
68
- this.focusedNode = null;
69
- this.onChangeCallback = null;
70
- this.bindPath = null;
71
- this.engine = null;
72
- }
73
- /**
74
- * Update overlay position
75
- */
76
- updatePosition(node, canvasBounds) {
77
- if (!this.overlay || node !== this.focusedNode)
78
- return;
79
- const bounds = getAbsoluteBounds(node);
80
- if (!bounds)
81
- return;
82
- this.positionOverlay(bounds, canvasBounds);
83
- }
84
- /**
85
- * Style overlay to match canvas node
86
- */
87
- styleOverlay(node, bounds, canvasBounds) {
88
- if (!this.overlay)
89
- return;
90
- const props = node.props;
91
- // Position
92
- this.positionOverlay(bounds, canvasBounds);
93
- // Font and text styling — go through `cssLengthToPx` so values that
94
- // arrive in `rem` ("0.875rem" for `text-sm`) round to real pixels
95
- // instead of `parseFloat`'s bare 0.875 (which made the caret disappear).
96
- const fontSize = cssLengthToPx(props.fontSize) ?? 16;
97
- const fontWeight = props.fontWeight || "normal";
98
- const fontFamily = props.fontFamily || "system-ui, sans-serif";
99
- const color = props.color || "#000000";
100
- const padTop = cssLengthToPx(props.paddingTop ?? props.padding) ?? 0;
101
- const padRight = cssLengthToPx(props.paddingRight ?? props.padding) ?? 0;
102
- const padBottom = cssLengthToPx(props.paddingBottom ?? props.padding) ?? 0;
103
- const padLeft = cssLengthToPx(props.paddingLeft ?? props.padding) ?? 0;
104
- const borderRadius = cssLengthToPx(props.borderRadius) ?? 4;
105
- Object.assign(this.overlay.style, {
106
- fontSize: `${fontSize}px`,
107
- fontWeight: String(fontWeight),
108
- fontFamily: fontFamily,
109
- color: color,
110
- border: "none",
111
- outline: "none",
112
- backgroundColor: props.backgroundColor || "#ffffff",
113
- padding: `${padTop}px ${padRight}px ${padBottom}px ${padLeft}px`,
114
- borderRadius: `${borderRadius}px`,
115
- boxSizing: "border-box",
116
- resize: "none",
117
- // Keep the overlay above the canvas; the Bun-served stage has no
118
- // stacking context of its own so a plain z-index works.
119
- zIndex: "1000",
120
- });
121
- // Placeholder
122
- if (props.placeholder) {
123
- this.overlay.placeholder = props.placeholder;
124
- }
125
- // Input type
126
- if (this.overlay instanceof HTMLInputElement && props.type) {
127
- this.overlay.type = props.type;
128
- }
129
- }
130
- /**
131
- * Position overlay over canvas node.
132
- *
133
- * `bounds` is in canvas-local CSS pixels (the layout coordinate space
134
- * the renderer paints into after `ctx.scale(dpr, dpr)`). The overlay is
135
- * mounted on `document.body`, so we add `canvasBounds.left/top`
136
- * (page coords from `getBoundingClientRect`) to land at the right
137
- * absolute position regardless of where the canvas sits on the page.
138
- * Adding `window.scrollX/Y` keeps the overlay pinned to the canvas
139
- * even when the page scrolls between focus and reposition.
140
- */
141
- positionOverlay(bounds, canvasBounds) {
142
- if (!this.overlay)
143
- return;
144
- const sx = typeof window !== "undefined" ? window.scrollX : 0;
145
- const sy = typeof window !== "undefined" ? window.scrollY : 0;
146
- Object.assign(this.overlay.style, {
147
- position: "absolute",
148
- left: `${canvasBounds.left + sx + bounds.x}px`,
149
- top: `${canvasBounds.top + sy + bounds.y}px`,
150
- width: `${bounds.width}px`,
151
- height: `${bounds.height}px`,
152
- });
153
- }
154
- /**
155
- * Handle input event
156
- */
157
- onInput(e) {
158
- if (!this.overlay || !this.onChangeCallback)
159
- return;
160
- const value = this.overlay.value;
161
- this.onChangeCallback(value);
162
- // Dispatch __hypen_bind for two-way binding
163
- if (this.bindPath && this.engine) {
164
- this.engine.dispatchAction("__hypen_bind", {
165
- path: this.bindPath,
166
- value,
167
- });
168
- }
169
- }
170
- /**
171
- * Handle blur event
172
- */
173
- onBlur() {
174
- // Hide overlay when focus is lost
175
- this.hideInput();
176
- }
177
- /**
178
- * Handle keyboard events
179
- */
180
- onKeyDown(e) {
181
- if (!this.overlay)
182
- return;
183
- // Enter key submits (unless multiline)
184
- if (e.key === "Enter" && this.overlay instanceof HTMLInputElement) {
185
- e.preventDefault();
186
- this.overlay.blur();
187
- }
188
- // Escape cancels
189
- if (e.key === "Escape") {
190
- e.preventDefault();
191
- this.overlay.blur();
192
- }
193
- }
194
- /**
195
- * Check if input is currently shown
196
- */
197
- isShown() {
198
- return this.overlay !== null;
199
- }
200
- /**
201
- * Get current focused node
202
- */
203
- getFocusedNode() {
204
- return this.focusedNode;
205
- }
206
- }
207
- //# sourceMappingURL=input.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"input.js","sourceRoot":"","sources":["../../src/canvas/input.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAO9D;;GAEG;AACH,MAAM,OAAO,YAAY;IACf,SAAS,CAAc;IACvB,OAAO,GAAkD,IAAI,CAAC;IAC9D,WAAW,GAAuB,IAAI,CAAC;IACvC,gBAAgB,GAAqC,IAAI,CAAC;IAC1D,QAAQ,GAAkB,IAAI,CAAC;IAC/B,MAAM,GAAsB,IAAI,CAAC;IAEzC,YAAY,SAA6B;QACvC,IAAI,CAAC,SAAS,GAAG,SAAS,IAAK,EAAkB,CAAC;IACpD,CAAC;IAED;;;;;;;OAOG;IACH,SAAS,CACP,IAAiB,EACjB,YAAqB,EACrB,QAAiC,EACjC,MAA0B;QAE1B,8DAA8D;QAC9D,IAAI,OAAO,QAAQ,KAAK,WAAW;YAAE,OAAO;QAE5C,IAAI,CAAC,SAAS,EAAE,CAAC;QAEjB,iDAAiD;QACjD,IAAI,CAAC,QAAQ,GAAI,IAAI,CAAC,KAAK,CAAC,IAAe,IAAI,IAAI,CAAC;QACpD,IAAI,CAAC,MAAM,GAAG,MAAM,IAAI,IAAI,CAAC;QAE7B,MAAM,MAAM,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;QACvC,IAAI,CAAC,MAAM;YAAE,OAAO;QAEpB,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,UAAU,CAAC;QAC3D,IAAI,CAAC,OAAO,GAAG,WAAW;YACxB,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAC;YACpC,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAEpC,gBAAgB;QAChB,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC;QAE9C,oBAAoB;QACpB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC;QACrC,IAAI,CAAC,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC;QAE3B,uBAAuB;QACvB,IAAI,CAAC,gBAAgB,GAAG,QAAQ,CAAC;QACjC,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAChE,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAC9D,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAkB,CAAC,CAAC;QAErF,oEAAoE;QACpE,8DAA8D;QAC9D,kEAAkE;QAClE,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QAErB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;IAC1B,CAAC;IAED;;OAEG;IACH,SAAS;QACP,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YACtB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACtB,CAAC;QACD,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAC7B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;IACrB,CAAC;IAED;;OAEG;IACH,cAAc,CAAC,IAAiB,EAAE,YAAqB;QACrD,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,IAAI,KAAK,IAAI,CAAC,WAAW;YAAE,OAAO;QAEvD,MAAM,MAAM,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;QACvC,IAAI,CAAC,MAAM;YAAE,OAAO;QAEpB,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC7C,CAAC;IAED;;OAEG;IACK,YAAY,CAClB,IAAiB,EACjB,MAAiB,EACjB,YAAqB;QAErB,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO;QAE1B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QAEzB,WAAW;QACX,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;QAE3C,oEAAoE;QACpE,kEAAkE;QAClE,yEAAyE;QACzE,MAAM,QAAQ,GAAG,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACrD,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,IAAI,QAAQ,CAAC;QAChD,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,IAAI,uBAAuB,CAAC;QAC/D,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,SAAS,CAAC;QAEvC,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACrE,MAAM,QAAQ,GAAG,aAAa,CAAC,KAAK,CAAC,YAAY,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACzE,MAAM,SAAS,GAAG,aAAa,CAAC,KAAK,CAAC,aAAa,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC3E,MAAM,OAAO,GAAG,aAAa,CAAC,KAAK,CAAC,WAAW,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACvE,MAAM,YAAY,GAAG,aAAa,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;QAE5D,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE;YAChC,QAAQ,EAAE,GAAG,QAAQ,IAAI;YACzB,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;YAC9B,UAAU,EAAE,UAAU;YACtB,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,MAAM;YACf,eAAe,EAAE,KAAK,CAAC,eAAe,IAAI,SAAS;YACnD,OAAO,EAAE,GAAG,MAAM,MAAM,QAAQ,MAAM,SAAS,MAAM,OAAO,IAAI;YAChE,YAAY,EAAE,GAAG,YAAY,IAAI;YACjC,SAAS,EAAE,YAAY;YACvB,MAAM,EAAE,MAAM;YACd,iEAAiE;YACjE,wDAAwD;YACxD,MAAM,EAAE,MAAM;SACf,CAAC,CAAC;QAEH,cAAc;QACd,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC;YACtB,IAAI,CAAC,OAAO,CAAC,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC;QAC/C,CAAC;QAED,aAAa;QACb,IAAI,IAAI,CAAC,OAAO,YAAY,gBAAgB,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;YAC3D,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACjC,CAAC;IACH,CAAC;IAED;;;;;;;;;;OAUG;IACK,eAAe,CAAC,MAAiB,EAAE,YAAqB;QAC9D,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO;QAE1B,MAAM,EAAE,GAAG,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9D,MAAM,EAAE,GAAG,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QAE9D,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE;YAChC,QAAQ,EAAE,UAAU;YACpB,IAAI,EAAE,GAAG,YAAY,CAAC,IAAI,GAAG,EAAE,GAAG,MAAM,CAAC,CAAC,IAAI;YAC9C,GAAG,EAAE,GAAG,YAAY,CAAC,GAAG,GAAG,EAAE,GAAG,MAAM,CAAC,CAAC,IAAI;YAC5C,KAAK,EAAE,GAAG,MAAM,CAAC,KAAK,IAAI;YAC1B,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,IAAI;SAC7B,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACK,OAAO,CAAC,CAAQ;QACtB,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,gBAAgB;YAAE,OAAO;QAEpD,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC;QACjC,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAE7B,4CAA4C;QAC5C,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACjC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,cAAc,EAAE;gBACzC,IAAI,EAAE,IAAI,CAAC,QAAQ;gBACnB,KAAK;aACN,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED;;OAEG;IACK,MAAM;QACZ,kCAAkC;QAClC,IAAI,CAAC,SAAS,EAAE,CAAC;IACnB,CAAC;IAED;;OAEG;IACK,SAAS,CAAC,CAAgB;QAChC,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO;QAE1B,uCAAuC;QACvC,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,IAAI,CAAC,OAAO,YAAY,gBAAgB,EAAE,CAAC;YAClE,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QACtB,CAAC;QAED,iBAAiB;QACjB,IAAI,CAAC,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;YACvB,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QACtB,CAAC;IACH,CAAC;IAED;;OAEG;IACH,OAAO;QACL,OAAO,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC;IAC/B,CAAC;IAED;;OAEG;IACH,cAAc;QACZ,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;CACF"}