@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.
- package/dist/canvas/events.d.ts +30 -2
- package/dist/canvas/events.js +112 -30
- package/dist/canvas/events.js.map +1 -1
- package/dist/canvas/input.d.ts +15 -2
- package/dist/canvas/input.js +43 -24
- package/dist/canvas/input.js.map +1 -1
- package/dist/canvas/layout.js +479 -96
- package/dist/canvas/layout.js.map +1 -1
- package/dist/canvas/paint.d.ts +4 -0
- package/dist/canvas/paint.js +141 -29
- package/dist/canvas/paint.js.map +1 -1
- package/dist/canvas/props.d.ts +55 -0
- package/dist/canvas/props.js +121 -0
- package/dist/canvas/props.js.map +1 -0
- package/dist/canvas/renderer.d.ts +13 -0
- package/dist/canvas/renderer.js +109 -11
- package/dist/canvas/renderer.js.map +1 -1
- package/dist/canvas/scroll.d.ts +27 -3
- package/dist/canvas/scroll.js +66 -26
- package/dist/canvas/scroll.js.map +1 -1
- package/dist/canvas/selection.js +3 -4
- package/dist/canvas/selection.js.map +1 -1
- package/dist/canvas/utils.d.ts +23 -3
- package/dist/canvas/utils.js +90 -30
- package/dist/canvas/utils.js.map +1 -1
- package/dist/dom/applicators/border.js +9 -0
- package/dist/dom/applicators/border.js.map +1 -1
- package/dist/dom/applicators/font.js +18 -1
- package/dist/dom/applicators/font.js.map +1 -1
- package/dist/dom/applicators/layout.js +44 -8
- package/dist/dom/applicators/layout.js.map +1 -1
- package/dist/dom/applicators/margin.js +18 -53
- package/dist/dom/applicators/margin.js.map +1 -1
- package/dist/dom/applicators/padding.js +21 -33
- package/dist/dom/applicators/padding.js.map +1 -1
- package/dist/dom/applicators/typography.js +13 -1
- package/dist/dom/applicators/typography.js.map +1 -1
- package/dist/dom/components/column.d.ts +11 -2
- package/dist/dom/components/column.js +12 -4
- package/dist/dom/components/column.js.map +1 -1
- package/dist/dom/components/image.js +9 -3
- package/dist/dom/components/image.js.map +1 -1
- package/dist/dom/components/input.js +11 -0
- package/dist/dom/components/input.js.map +1 -1
- package/dist/dom/components/textarea.js +8 -0
- package/dist/dom/components/textarea.js.map +1 -1
- package/dist/dom/renderer.d.ts +18 -0
- package/dist/dom/renderer.js +87 -3
- package/dist/dom/renderer.js.map +1 -1
- package/package.json +2 -2
- package/src/canvas/PARITY.md +254 -0
- package/src/canvas/events.ts +113 -32
- package/src/canvas/input.ts +44 -25
- package/src/canvas/layout.ts +492 -91
- package/src/canvas/paint.ts +143 -29
- package/src/canvas/props.ts +117 -0
- package/src/canvas/renderer.ts +132 -11
- package/src/canvas/scroll.ts +62 -29
- package/src/canvas/selection.ts +3 -4
- package/src/canvas/utils.ts +83 -31
- package/src/dom/applicators/border.ts +9 -0
- package/src/dom/applicators/font.ts +16 -1
- package/src/dom/applicators/layout.ts +34 -7
- package/src/dom/applicators/margin.ts +16 -43
- package/src/dom/applicators/padding.ts +19 -27
- package/src/dom/applicators/typography.ts +11 -1
- package/src/dom/components/column.ts +12 -4
- package/src/dom/components/image.ts +8 -3
- package/src/dom/components/input.ts +11 -0
- package/src/dom/components/textarea.ts +8 -0
- package/src/dom/renderer.ts +92 -3
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Column Component - Vertical Stack
|
|
3
3
|
*
|
|
4
|
-
* Children
|
|
5
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
|
package/src/dom/renderer.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
*/
|