@hypen-space/web 0.4.82 → 0.4.83

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 (71) hide show
  1. package/dist/canvas/events.d.ts +30 -2
  2. package/dist/canvas/events.js +112 -30
  3. package/dist/canvas/events.js.map +1 -1
  4. package/dist/canvas/input.d.ts +15 -2
  5. package/dist/canvas/input.js +43 -24
  6. package/dist/canvas/input.js.map +1 -1
  7. package/dist/canvas/layout.js +479 -96
  8. package/dist/canvas/layout.js.map +1 -1
  9. package/dist/canvas/paint.d.ts +4 -0
  10. package/dist/canvas/paint.js +141 -29
  11. package/dist/canvas/paint.js.map +1 -1
  12. package/dist/canvas/props.d.ts +55 -0
  13. package/dist/canvas/props.js +121 -0
  14. package/dist/canvas/props.js.map +1 -0
  15. package/dist/canvas/renderer.d.ts +13 -0
  16. package/dist/canvas/renderer.js +109 -11
  17. package/dist/canvas/renderer.js.map +1 -1
  18. package/dist/canvas/scroll.d.ts +27 -3
  19. package/dist/canvas/scroll.js +66 -26
  20. package/dist/canvas/scroll.js.map +1 -1
  21. package/dist/canvas/selection.js +3 -4
  22. package/dist/canvas/selection.js.map +1 -1
  23. package/dist/canvas/utils.d.ts +23 -3
  24. package/dist/canvas/utils.js +90 -30
  25. package/dist/canvas/utils.js.map +1 -1
  26. package/dist/dom/applicators/border.js +9 -0
  27. package/dist/dom/applicators/border.js.map +1 -1
  28. package/dist/dom/applicators/font.js +18 -1
  29. package/dist/dom/applicators/font.js.map +1 -1
  30. package/dist/dom/applicators/layout.js +44 -8
  31. package/dist/dom/applicators/layout.js.map +1 -1
  32. package/dist/dom/applicators/margin.js +18 -53
  33. package/dist/dom/applicators/margin.js.map +1 -1
  34. package/dist/dom/applicators/padding.js +21 -33
  35. package/dist/dom/applicators/padding.js.map +1 -1
  36. package/dist/dom/applicators/typography.js +13 -1
  37. package/dist/dom/applicators/typography.js.map +1 -1
  38. package/dist/dom/components/column.d.ts +11 -2
  39. package/dist/dom/components/column.js +12 -4
  40. package/dist/dom/components/column.js.map +1 -1
  41. package/dist/dom/components/image.js +9 -3
  42. package/dist/dom/components/image.js.map +1 -1
  43. package/dist/dom/components/input.js +11 -0
  44. package/dist/dom/components/input.js.map +1 -1
  45. package/dist/dom/components/textarea.js +8 -0
  46. package/dist/dom/components/textarea.js.map +1 -1
  47. package/dist/dom/renderer.d.ts +18 -0
  48. package/dist/dom/renderer.js +87 -3
  49. package/dist/dom/renderer.js.map +1 -1
  50. package/package.json +2 -2
  51. package/src/canvas/PARITY.md +254 -0
  52. package/src/canvas/events.ts +113 -32
  53. package/src/canvas/input.ts +44 -25
  54. package/src/canvas/layout.ts +492 -91
  55. package/src/canvas/paint.ts +143 -29
  56. package/src/canvas/props.ts +117 -0
  57. package/src/canvas/renderer.ts +132 -11
  58. package/src/canvas/scroll.ts +62 -29
  59. package/src/canvas/selection.ts +3 -4
  60. package/src/canvas/utils.ts +83 -31
  61. package/src/dom/applicators/border.ts +9 -0
  62. package/src/dom/applicators/font.ts +16 -1
  63. package/src/dom/applicators/layout.ts +34 -7
  64. package/src/dom/applicators/margin.ts +16 -43
  65. package/src/dom/applicators/padding.ts +19 -27
  66. package/src/dom/applicators/typography.ts +11 -1
  67. package/src/dom/components/column.ts +12 -4
  68. package/src/dom/components/image.ts +8 -3
  69. package/src/dom/components/input.ts +11 -0
  70. package/src/dom/components/textarea.ts +8 -0
  71. package/src/dom/renderer.ts +92 -3
@@ -1,8 +1,17 @@
1
1
  /**
2
2
  * Column Component - Vertical Stack
3
3
  *
4
- * Children wrap to content by default (matching Android/iOS behavior).
5
- * Use .fillMaxWidth() on children or .horizontalAlignment("stretch") to stretch.
4
+ * Children stretch to the Column's cross-axis (width) by default matches
5
+ * Tailwind / CSS authors' expectations (`mx-4` on a button leaves 16px
6
+ * gaps on each side, not a content-hugging pill). Override per-child with
7
+ * `.alignSelf("flex-start")` or `.horizontalAlignment(...)`.
8
+ *
9
+ * This was `flex-start` originally to mirror iOS/Android "wrap to content"
10
+ * defaults, but that made every `.tw("mx-4")`-padded child (e.g. the
11
+ * social Edit Profile button) render at its content width and look wrong
12
+ * against the native renderers — which DO stretch because SwiftUI /
13
+ * Compose treat horizontal margin-only as a "pad the edges of a wide
14
+ * element" intent.
6
15
  */
7
16
 
8
17
  import type { ComponentHandler } from "./index.js";
@@ -12,8 +21,7 @@ export const columnHandler: ComponentHandler = {
12
21
  const el = document.createElement("div");
13
22
  el.style.display = "flex";
14
23
  el.style.flexDirection = "column";
15
- // Wrap children to content by default (match iOS/Android behavior)
16
- el.style.alignItems = "flex-start";
24
+ el.style.alignItems = "stretch";
17
25
  el.dataset.hypenType = "column";
18
26
  return el;
19
27
  },
@@ -14,10 +14,15 @@ export const imageHandler: ComponentHandler = {
14
14
  applyProps(el: HTMLElement, props: Record<string, any>): void {
15
15
  const img = el as HTMLImageElement;
16
16
 
17
- // Support url, src, or first positional argument
18
- const src = props["0"] || props.url || props.src;
19
- if (src !== undefined) {
17
+ // Support url, src, or first positional argument.
18
+ // Skip null/empty initial state often has src=null before the real
19
+ // URL arrives in a follow-up SetProp patch, and `<img src="null">`
20
+ // would trigger a spurious GET /null and block paint.
21
+ const src = props["0"] ?? props.url ?? props.src;
22
+ if (src != null && src !== "") {
20
23
  img.src = String(src);
24
+ } else if (img.src) {
25
+ img.removeAttribute("src");
21
26
  }
22
27
 
23
28
  if (props.alt !== undefined) {
@@ -8,6 +8,17 @@ export const inputHandler: ComponentHandler = {
8
8
  create(): HTMLElement {
9
9
  const el = document.createElement("input");
10
10
  el.dataset.hypenType = "input";
11
+ // Reset the default user-agent chrome so `.tw("bg-gray-100 rounded-lg")`
12
+ // on an Input reads the same as any other styled element. Without
13
+ // this the native black 2px inset border shows through the Tailwind
14
+ // background (the social Search bar rendered as a dark-bordered
15
+ // rectangle instead of a pill). The author can still opt back in
16
+ // via explicit `border-*` applicators.
17
+ el.style.border = "none";
18
+ el.style.outline = "none";
19
+ el.style.background = "transparent";
20
+ el.style.font = "inherit";
21
+ el.style.color = "inherit";
11
22
  return el as any as HTMLElement;
12
23
  },
13
24
 
@@ -8,6 +8,14 @@ export const textareaHandler: ComponentHandler = {
8
8
  create(): HTMLElement {
9
9
  const el = document.createElement("textarea");
10
10
  el.dataset.hypenType = "textarea";
11
+ // Same user-agent reset as Input — otherwise `.tw(...)` styling draws
12
+ // over a native dark border and inset shadow.
13
+ el.style.border = "none";
14
+ el.style.outline = "none";
15
+ el.style.background = "transparent";
16
+ el.style.font = "inherit";
17
+ el.style.color = "inherit";
18
+ el.style.resize = "none";
11
19
  return el as HTMLElement;
12
20
  },
13
21
 
@@ -15,6 +15,24 @@ const log = frameworkLoggers.renderer;
15
15
  /** Element types that treat the "action" prop as an onClick handler */
16
16
  const ACTIONABLE_TYPES = new Set(["button", "link", "card"]);
17
17
 
18
+ /**
19
+ * Per-component-type props that map to HTML element attributes (not CSS).
20
+ * SetProp for these names must go through the component's `applyProps`
21
+ * (which writes the attribute), otherwise the applicator's CSS fallback
22
+ * tries `el.style.<name>` and the update silently no-ops — most visibly
23
+ * for `Image.src` updates that depend on async-loaded state (the social
24
+ * `Your story` avatar that fills in once `state.currentUser` arrives).
25
+ */
26
+ const COMPONENT_HTML_ATTRS: Record<string, Set<string>> = {
27
+ image: new Set(["src", "alt", "url", "0", "srcset"]),
28
+ input: new Set(["type", "placeholder", "value", "disabled", "readonly", "name", "checked"]),
29
+ textarea: new Set(["placeholder", "value", "rows", "cols", "disabled", "readonly", "name"]),
30
+ select: new Set(["name", "multiple", "disabled", "value"]),
31
+ checkbox: new Set(["checked", "disabled", "name"]),
32
+ radio: new Set(["checked", "disabled", "name", "value"]),
33
+ link: new Set(["href", "target", "rel"]),
34
+ };
35
+
18
36
  import { ComponentRegistry } from "./components/index.js";
19
37
  import { ApplicatorRegistry } from "./applicators/index.js";
20
38
  import { canvasHandler, canvasApplicators } from "./canvas/index.js";
@@ -134,8 +152,11 @@ export class DOMRenderer {
134
152
 
135
153
  switch (patch.type) {
136
154
  case "insert":
137
- case "move": {
138
- // Route by parent — if parent is a canvas root or inside one
155
+ case "move":
156
+ case "attach": {
157
+ // Route by parent — if parent is a canvas root or inside one.
158
+ // `attach` behaves like `insert`: it puts the (already-alive)
159
+ // id into a new parent, so it routes exactly the same way.
139
160
  if (!parentId) return undefined;
140
161
  if (this.canvasRenderers.has(parentId)) return parentId;
141
162
  return this.canvasSubtreeMap.get(parentId);
@@ -146,7 +167,7 @@ export class DOMRenderer {
146
167
  return this.canvasSubtreeMap.get(id);
147
168
  }
148
169
  default: {
149
- // setProp, removeProp, setText, remove — route by node id
170
+ // setProp, removeProp, setText, remove, detach — route by node id
150
171
  if (!id) return undefined;
151
172
  return this.canvasSubtreeMap.get(id);
152
173
  }
@@ -241,6 +262,12 @@ export class DOMRenderer {
241
262
  case "remove":
242
263
  this.onRemove(id!);
243
264
  break;
265
+ case "detach":
266
+ this.onDetach(id!);
267
+ break;
268
+ case "attach":
269
+ this.onInsert(parentId!, id!, beforeId);
270
+ break;
244
271
  }
245
272
  }
246
273
 
@@ -323,6 +350,20 @@ export class DOMRenderer {
323
350
  return;
324
351
  }
325
352
 
353
+ // For element types where the positional `0` is a non-text prop
354
+ // (e.g. `Image(src)` accepts the URL as `0`), let the component's
355
+ // `applyProps` handle it instead of force-setting `textContent`.
356
+ // Without this an `Image` whose URL arrived in a later SetProp
357
+ // patch ended up with `<img>Your story</img>`-style text contents
358
+ // and an empty `src`.
359
+ if (elementType && COMPONENT_HTML_ATTRS[elementType]?.has(name)) {
360
+ const handler = this.components.get(elementType);
361
+ if (handler?.applyProps) {
362
+ handler.applyProps(element, { [name]: value });
363
+ return;
364
+ }
365
+ }
366
+
326
367
  const nextText = String(value);
327
368
  element.textContent = nextText;
328
369
 
@@ -352,6 +393,22 @@ export class DOMRenderer {
352
393
  return;
353
394
  }
354
395
 
396
+ // Element-specific HTML attributes (img.src, input.placeholder, etc.)
397
+ // need to go through the component handler's `applyProps`. Without
398
+ // this, a follow-up SetProp(src=…) for an `Image` whose initial
399
+ // Create had no src — the `Your story` avatar that depends on
400
+ // `state.currentUser`, which loads after the first paint — was
401
+ // routed to the CSS fallback (`el.style.src`, a no-op) and the
402
+ // image stayed blank forever.
403
+ const elementType = element.dataset.hypenType;
404
+ if (elementType && COMPONENT_HTML_ATTRS[elementType]?.has(name)) {
405
+ const handler = this.components.get(elementType);
406
+ if (handler?.applyProps) {
407
+ handler.applyProps(element, { [name]: value });
408
+ return;
409
+ }
410
+ }
411
+
355
412
  this.applicators.apply(element, name, value);
356
413
 
357
414
  // Forward canvas dimension changes to its CanvasRenderer
@@ -423,6 +480,38 @@ export class DOMRenderer {
423
480
  this.onInsert(parentId, id, beforeId);
424
481
  }
425
482
 
483
+ /**
484
+ * Detach an element from its parent without destroying it.
485
+ *
486
+ * The HTMLElement and its children stay alive in `this.nodes` (and
487
+ * anywhere else they're referenced); only the DOM parent link is
488
+ * broken. A subsequent `attach` patch reinserts the same reference
489
+ * into a parent, preserving scroll position, form state, focus,
490
+ * inline styles, and any attached event listeners.
491
+ *
492
+ * Event listeners and other disposables are intentionally NOT
493
+ * disposed — the subtree is expected to come back. If the engine
494
+ * follows up with a `remove` for this id instead, full teardown
495
+ * happens there.
496
+ *
497
+ * Used by the engine's Router cache to keep previously-visited
498
+ * route subtrees alive between navigations.
499
+ */
500
+ private onDetach(id: string): void {
501
+ const element = this.nodes.get(id);
502
+ if (!element) return;
503
+
504
+ if (element.parentNode) {
505
+ element.parentNode.removeChild(element);
506
+ }
507
+
508
+ // If the root gets detached (unlikely but defensible), clear
509
+ // rootId so a later attach can re-root cleanly.
510
+ if (this.rootId === id) {
511
+ this.rootId = null;
512
+ }
513
+ }
514
+
426
515
  /**
427
516
  * Remove an element from the tree
428
517
  */