@hypen-space/web 0.4.82 → 0.4.84
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
package/src/canvas/scroll.ts
CHANGED
|
@@ -58,10 +58,36 @@ export function createScrollState(): ScrollState {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
/** Returns true if the node is configured as a scrollable container.
|
|
61
|
+
/** Returns true if the node is configured as a scrollable container.
|
|
62
|
+
*
|
|
63
|
+
* Accepts both forms the engine and user code emit:
|
|
64
|
+
* - `overflow: "scroll" | "auto"` — CSS-style (DOM parity)
|
|
65
|
+
* - `scrollable: true | "vertical" | "horizontal" | "both"` — Hypen DSL form
|
|
66
|
+
*/
|
|
62
67
|
export function isScrollable(node: VirtualNode): boolean {
|
|
68
|
+
const axes = getScrollAxes(node);
|
|
69
|
+
return axes.x || axes.y;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve which axes a node is allowed to scroll on.
|
|
74
|
+
*
|
|
75
|
+
* `scrollable: "horizontal"` means a horizontal strip — the user expects
|
|
76
|
+
* vertical drags / wheel events to bubble out, and content overflowing the
|
|
77
|
+
* Y axis must NOT inflate scrollHeight (otherwise the strip jitters
|
|
78
|
+
* vertically on touch).
|
|
79
|
+
*/
|
|
80
|
+
export function getScrollAxes(node: VirtualNode): { x: boolean; y: boolean } {
|
|
63
81
|
const overflow = node.props.overflow;
|
|
64
|
-
|
|
82
|
+
if (overflow === "scroll" || overflow === "auto") return { x: true, y: true };
|
|
83
|
+
const scrollable = node.props.scrollable;
|
|
84
|
+
if (scrollable === true) return { x: true, y: true };
|
|
85
|
+
if (typeof scrollable === "string") {
|
|
86
|
+
if (scrollable === "horizontal") return { x: true, y: false };
|
|
87
|
+
if (scrollable === "vertical") return { x: false, y: true };
|
|
88
|
+
if (scrollable === "both") return { x: true, y: true };
|
|
89
|
+
}
|
|
90
|
+
return { x: false, y: false };
|
|
65
91
|
}
|
|
66
92
|
|
|
67
93
|
/** Ensure a scroll state is attached; returns the (possibly new) state. */
|
|
@@ -165,12 +191,13 @@ export class ScrollManager {
|
|
|
165
191
|
// -------------------------------------------------------------------------
|
|
166
192
|
|
|
167
193
|
private canvasPoint(clientX: number, clientY: number): Point {
|
|
194
|
+
// Logical CSS pixels — layout is in logical units (the renderer scales the
|
|
195
|
+
// ctx by dpr once at setup), so hit tests must match. See the same note in
|
|
196
|
+
// `events.ts#getCanvasCoordinates`.
|
|
168
197
|
const rect = this.canvas.getBoundingClientRect();
|
|
169
|
-
const scaleX = this.canvas.width / rect.width;
|
|
170
|
-
const scaleY = this.canvas.height / rect.height;
|
|
171
198
|
return {
|
|
172
|
-
x:
|
|
173
|
-
y:
|
|
199
|
+
x: clientX - rect.left,
|
|
200
|
+
y: clientY - rect.top,
|
|
174
201
|
};
|
|
175
202
|
}
|
|
176
203
|
|
|
@@ -234,12 +261,13 @@ export class ScrollManager {
|
|
|
234
261
|
): boolean {
|
|
235
262
|
const ss = node.scrollState;
|
|
236
263
|
if (!ss) return false;
|
|
264
|
+
const axes = getScrollAxes(node);
|
|
237
265
|
const { maxX, maxY } = maxScroll(node);
|
|
238
266
|
|
|
239
|
-
if (dy < 0 && ss.scrollY > 0) return true;
|
|
240
|
-
if (dy > 0 && ss.scrollY < maxY) return true;
|
|
241
|
-
if (dx < 0 && ss.scrollX > 0) return true;
|
|
242
|
-
if (dx > 0 && ss.scrollX < maxX) return true;
|
|
267
|
+
if (axes.y && dy < 0 && ss.scrollY > 0) return true;
|
|
268
|
+
if (axes.y && dy > 0 && ss.scrollY < maxY) return true;
|
|
269
|
+
if (axes.x && dx < 0 && ss.scrollX > 0) return true;
|
|
270
|
+
if (axes.x && dx > 0 && ss.scrollX < maxX) return true;
|
|
243
271
|
|
|
244
272
|
return false;
|
|
245
273
|
}
|
|
@@ -264,10 +292,11 @@ export class ScrollManager {
|
|
|
264
292
|
if (!target) return;
|
|
265
293
|
|
|
266
294
|
const ss = ensureScrollState(target);
|
|
295
|
+
const axes = getScrollAxes(target);
|
|
267
296
|
const { maxX, maxY } = maxScroll(target);
|
|
268
297
|
|
|
269
|
-
const canScrollX = maxX > 0;
|
|
270
|
-
const canScrollY = maxY > 0;
|
|
298
|
+
const canScrollX = axes.x && maxX > 0;
|
|
299
|
+
const canScrollY = axes.y && maxY > 0;
|
|
271
300
|
if (!canScrollX && !canScrollY) return;
|
|
272
301
|
|
|
273
302
|
if (canScrollY) {
|
|
@@ -364,16 +393,14 @@ export class ScrollManager {
|
|
|
364
393
|
if (!node) return;
|
|
365
394
|
|
|
366
395
|
const ss = ensureScrollState(node);
|
|
396
|
+
const axes = getScrollAxes(node);
|
|
367
397
|
const { maxX, maxY } = maxScroll(node);
|
|
368
398
|
const now = performance.now();
|
|
369
399
|
const dt = now - this.lastPointerTime;
|
|
370
400
|
|
|
371
|
-
|
|
372
|
-
const
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
const dx = -(clientX - this.lastPointerX) * scaleX;
|
|
376
|
-
const dy = -(clientY - this.lastPointerY) * scaleY;
|
|
401
|
+
// Logical pixel deltas — layout (and scroll bounds) live in logical units.
|
|
402
|
+
const dx = axes.x ? -(clientX - this.lastPointerX) : 0;
|
|
403
|
+
const dy = axes.y ? -(clientY - this.lastPointerY) : 0;
|
|
377
404
|
|
|
378
405
|
ss.scrollX = clampScroll(ss.scrollX + dx, maxX, true);
|
|
379
406
|
ss.scrollY = clampScroll(ss.scrollY + dy, maxY, true);
|
|
@@ -626,7 +653,8 @@ export class ScrollManager {
|
|
|
626
653
|
ScrollManager.updateScrollBounds(child);
|
|
627
654
|
}
|
|
628
655
|
|
|
629
|
-
|
|
656
|
+
const axes = getScrollAxes(node);
|
|
657
|
+
if (!axes.x && !axes.y) return;
|
|
630
658
|
|
|
631
659
|
const ss = ensureScrollState(node);
|
|
632
660
|
let maxRight = 0;
|
|
@@ -641,8 +669,11 @@ export class ScrollManager {
|
|
|
641
669
|
maxBottom = Math.max(maxBottom, relY + child.layout.height + child.layout.margin.bottom);
|
|
642
670
|
}
|
|
643
671
|
|
|
644
|
-
|
|
645
|
-
|
|
672
|
+
// Cap content size to the viewport on disabled axes — otherwise a
|
|
673
|
+
// horizontal strip with tall children would report a phantom scrollHeight
|
|
674
|
+
// and the touch handler would let users drag vertically into nothing.
|
|
675
|
+
ss.scrollWidth = axes.x ? maxRight : Math.min(maxRight, node.layout.contentWidth);
|
|
676
|
+
ss.scrollHeight = axes.y ? maxBottom : Math.min(maxBottom, node.layout.contentHeight);
|
|
646
677
|
|
|
647
678
|
// Clamp current scroll to new bounds
|
|
648
679
|
const { maxX, maxY } = maxScroll(node);
|
|
@@ -677,8 +708,15 @@ export class ScrollManager {
|
|
|
677
708
|
// ---------------------------------------------------------------------------
|
|
678
709
|
|
|
679
710
|
/**
|
|
680
|
-
* Get
|
|
681
|
-
* ancestors.
|
|
711
|
+
* Get the on-screen bounds for a node, accounting for scroll offsets of all
|
|
712
|
+
* ancestors. `node.layout.{x,y}` is already absolute (computed in
|
|
713
|
+
* `writeLayout` for the Taffy path and the fallback equivalent), so we only
|
|
714
|
+
* need to subtract each scrollable ancestor's offset — the same translation
|
|
715
|
+
* the paint pipeline applies via `ctx.translate(-scrollX, -scrollY)`.
|
|
716
|
+
*
|
|
717
|
+
* NOTE: an earlier version walked up to add `current.layout.contentX`,
|
|
718
|
+
* which double-counted the parent's padding+border. That made hit testing
|
|
719
|
+
* land on the wrong node any time content was nested more than a level deep.
|
|
682
720
|
*/
|
|
683
721
|
export function getScrollAwareBounds(node: VirtualNode): Rectangle | null {
|
|
684
722
|
if (!node.layout) return null;
|
|
@@ -687,16 +725,11 @@ export function getScrollAwareBounds(node: VirtualNode): Rectangle | null {
|
|
|
687
725
|
let y = node.layout.y;
|
|
688
726
|
|
|
689
727
|
let current = node.parent;
|
|
690
|
-
while (current
|
|
691
|
-
x += current.layout.contentX;
|
|
692
|
-
y += current.layout.contentY;
|
|
693
|
-
|
|
694
|
-
// Apply ancestor scroll offset
|
|
728
|
+
while (current) {
|
|
695
729
|
if (current.scrollState) {
|
|
696
730
|
x -= current.scrollState.scrollX;
|
|
697
731
|
y -= current.scrollState.scrollY;
|
|
698
732
|
}
|
|
699
|
-
|
|
700
733
|
current = current.parent;
|
|
701
734
|
}
|
|
702
735
|
|
package/src/canvas/selection.ts
CHANGED
|
@@ -101,12 +101,11 @@ export class SelectionManager {
|
|
|
101
101
|
// -------------------------------------------------------------------------
|
|
102
102
|
|
|
103
103
|
private canvasPoint(e: MouseEvent): Point {
|
|
104
|
+
// Logical CSS pixels — same convention as `events.ts#getCanvasCoordinates`.
|
|
104
105
|
const rect = this.canvas.getBoundingClientRect();
|
|
105
|
-
const scaleX = this.canvas.width / rect.width;
|
|
106
|
-
const scaleY = this.canvas.height / rect.height;
|
|
107
106
|
return {
|
|
108
|
-
x:
|
|
109
|
-
y:
|
|
107
|
+
x: e.clientX - rect.left,
|
|
108
|
+
y: e.clientY - rect.top,
|
|
110
109
|
};
|
|
111
110
|
}
|
|
112
111
|
|
package/src/canvas/utils.ts
CHANGED
|
@@ -6,6 +6,65 @@
|
|
|
6
6
|
|
|
7
7
|
import type { BoxSpacing, Rectangle, Point, VirtualNode } from "./types.js";
|
|
8
8
|
|
|
9
|
+
// Pixel equivalent for 1rem (and 1em, which we approximate the same without a
|
|
10
|
+
// real inheritance chain). The engine emits Tailwind values — `0.75rem`,
|
|
11
|
+
// `1rem`, `3.5rem` — and any earlier `parseFloat` path was stripping the unit
|
|
12
|
+
// and treating `3.5rem` as 3.5px, collapsing whole layouts to 1/16 size.
|
|
13
|
+
const ROOT_FONT_PX = 16;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse a CSS length string (or number) into pixels.
|
|
17
|
+
* Returns `null` for `"auto"` or unparseable input; returns `null` for `%`
|
|
18
|
+
* values so callers can decide whether to keep the percentage verbatim or
|
|
19
|
+
* fall back to 0.
|
|
20
|
+
*/
|
|
21
|
+
export function cssLengthToPx(value: any): number | null {
|
|
22
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
23
|
+
if (typeof value !== "string") return null;
|
|
24
|
+
const s = value.trim();
|
|
25
|
+
if (s === "" || s === "auto") return null;
|
|
26
|
+
if (s.endsWith("%")) return null;
|
|
27
|
+
const match = s.match(/^(-?\d*\.?\d+)\s*([a-zA-Z]+)?$/);
|
|
28
|
+
if (!match) return null;
|
|
29
|
+
const n = parseFloat(match[1]);
|
|
30
|
+
if (!Number.isFinite(n)) return null;
|
|
31
|
+
const unit = (match[2] || "px").toLowerCase();
|
|
32
|
+
switch (unit) {
|
|
33
|
+
case "px": return n;
|
|
34
|
+
case "rem":
|
|
35
|
+
case "em": return n * ROOT_FONT_PX;
|
|
36
|
+
// We don't have a viewport reference here; treat vw/vh as unitless px
|
|
37
|
+
// fallback so the layout gets _some_ numeric value rather than zero.
|
|
38
|
+
case "vw":
|
|
39
|
+
case "vh": return n;
|
|
40
|
+
// Point/pica — rarely seen from the engine but honoured for completeness.
|
|
41
|
+
case "pt": return n * (96 / 72);
|
|
42
|
+
case "pc": return n * 16;
|
|
43
|
+
case "in": return n * 96;
|
|
44
|
+
case "cm": return n * (96 / 2.54);
|
|
45
|
+
case "mm": return n * (96 / 25.4);
|
|
46
|
+
default: return n;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse a CSS length string for Taffy's Dimension type. Keeps `%` values as
|
|
52
|
+
* the tagged percentage string, turns `"auto"`/empty/invalid into `"auto"`,
|
|
53
|
+
* converts everything else (including rem/em) through `cssLengthToPx`.
|
|
54
|
+
*/
|
|
55
|
+
export function cssLengthToDimension(value: any): "auto" | number | `${number}%` {
|
|
56
|
+
if (value === undefined || value === null || value === "auto") return "auto";
|
|
57
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : "auto";
|
|
58
|
+
if (typeof value === "string") {
|
|
59
|
+
const s = value.trim();
|
|
60
|
+
if (s === "" || s === "auto") return "auto";
|
|
61
|
+
if (s.endsWith("%")) return s as `${number}%`;
|
|
62
|
+
const px = cssLengthToPx(s);
|
|
63
|
+
return px !== null ? px : "auto";
|
|
64
|
+
}
|
|
65
|
+
return "auto";
|
|
66
|
+
}
|
|
67
|
+
|
|
9
68
|
/**
|
|
10
69
|
* Parse spacing value (margin, padding).
|
|
11
70
|
*
|
|
@@ -22,7 +81,7 @@ export function parseSpacing(value: any): BoxSpacing {
|
|
|
22
81
|
}
|
|
23
82
|
|
|
24
83
|
if (typeof value === "string") {
|
|
25
|
-
const parts = value.split(/\s+/).map((v) =>
|
|
84
|
+
const parts = value.split(/\s+/).map((v) => cssLengthToPx(v) ?? 0);
|
|
26
85
|
return spacingFromArray(parts);
|
|
27
86
|
}
|
|
28
87
|
|
|
@@ -35,17 +94,17 @@ export function parseSpacing(value: any): BoxSpacing {
|
|
|
35
94
|
value.left !== undefined
|
|
36
95
|
) {
|
|
37
96
|
return {
|
|
38
|
-
top:
|
|
39
|
-
right:
|
|
40
|
-
bottom:
|
|
41
|
-
left:
|
|
97
|
+
top: cssLengthToPx(value.top) ?? 0,
|
|
98
|
+
right: cssLengthToPx(value.right) ?? 0,
|
|
99
|
+
bottom: cssLengthToPx(value.bottom) ?? 0,
|
|
100
|
+
left: cssLengthToPx(value.left) ?? 0,
|
|
42
101
|
};
|
|
43
102
|
}
|
|
44
103
|
|
|
45
104
|
// Positional form: walk contiguous "0", "1", … keys.
|
|
46
105
|
const args: number[] = [];
|
|
47
106
|
for (let i = 0; value[String(i)] !== undefined; i++) {
|
|
48
|
-
args.push(
|
|
107
|
+
args.push(cssLengthToPx(value[String(i)]) ?? 0);
|
|
49
108
|
}
|
|
50
109
|
if (args.length > 0) {
|
|
51
110
|
return spacingFromArray(args);
|
|
@@ -71,17 +130,12 @@ function spacingFromArray(parts: number[]): BoxSpacing {
|
|
|
71
130
|
}
|
|
72
131
|
|
|
73
132
|
/**
|
|
74
|
-
* Parse size value (width, height)
|
|
75
|
-
* Returns
|
|
133
|
+
* Parse size value (width, height) into pixels.
|
|
134
|
+
* Returns `null` for `"auto"`, percentages, or unparseable input — callers
|
|
135
|
+
* that want to keep the percentage form should use `cssLengthToDimension`.
|
|
76
136
|
*/
|
|
77
137
|
export function parseSize(value: any): number | null {
|
|
78
|
-
|
|
79
|
-
if (typeof value === "string") {
|
|
80
|
-
if (value === "auto") return null;
|
|
81
|
-
const num = parseFloat(value);
|
|
82
|
-
return isNaN(num) ? null : num;
|
|
83
|
-
}
|
|
84
|
-
return null;
|
|
138
|
+
return cssLengthToPx(value);
|
|
85
139
|
}
|
|
86
140
|
|
|
87
141
|
/**
|
|
@@ -109,8 +163,12 @@ export function isPointInRoundedRect(
|
|
|
109
163
|
// Quick reject if outside bounding box
|
|
110
164
|
if (!isPointInRect(point, rect)) return false;
|
|
111
165
|
|
|
112
|
-
// No radius means simple rectangle
|
|
166
|
+
// No radius means simple rectangle. Clamp the radius to the box, since
|
|
167
|
+
// Tailwind's `rounded-full` arrives as 9999 — without clamping, the
|
|
168
|
+
// corner-test math thinks the corners are far outside the box and
|
|
169
|
+
// mis-classifies hits.
|
|
113
170
|
if (radius <= 0) return true;
|
|
171
|
+
radius = Math.min(radius, width / 2, height / 2);
|
|
114
172
|
|
|
115
173
|
const px = point.x;
|
|
116
174
|
const py = point.y;
|
|
@@ -212,25 +270,19 @@ export function findNodeById(root: VirtualNode, id: string): VirtualNode | null
|
|
|
212
270
|
}
|
|
213
271
|
|
|
214
272
|
/**
|
|
215
|
-
* Get absolute bounds of node
|
|
273
|
+
* Get absolute bounds of a node. `node.layout.{x,y}` is already absolute
|
|
274
|
+
* (the layout pass writes parent.absX + child.x in both the Taffy and JS
|
|
275
|
+
* fallback paths), so this is a thin wrapper for callers that work in
|
|
276
|
+
* `Rectangle` shape.
|
|
277
|
+
*
|
|
278
|
+
* Use `getScrollAwareBounds` (in `scroll.ts`) when ancestor scroll offsets
|
|
279
|
+
* matter — e.g. for hit testing.
|
|
216
280
|
*/
|
|
217
281
|
export function getAbsoluteBounds(node: VirtualNode): Rectangle | null {
|
|
218
282
|
if (!node.layout) return null;
|
|
219
|
-
|
|
220
|
-
let x = node.layout.x;
|
|
221
|
-
let y = node.layout.y;
|
|
222
|
-
|
|
223
|
-
// Walk up tree to accumulate offsets
|
|
224
|
-
let current = node.parent;
|
|
225
|
-
while (current && current.layout) {
|
|
226
|
-
x += current.layout.contentX;
|
|
227
|
-
y += current.layout.contentY;
|
|
228
|
-
current = current.parent;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
283
|
return {
|
|
232
|
-
x,
|
|
233
|
-
y,
|
|
284
|
+
x: node.layout.x,
|
|
285
|
+
y: node.layout.y,
|
|
234
286
|
width: node.layout.width,
|
|
235
287
|
height: node.layout.height,
|
|
236
288
|
};
|
|
@@ -44,6 +44,15 @@ export const borderHandlers: Record<string, ApplicatorHandler> = {
|
|
|
44
44
|
|
|
45
45
|
borderWidth: (el, value) => {
|
|
46
46
|
el.style.borderWidth = typeof value === "number" ? `${value}px` : String(value);
|
|
47
|
+
// CSS `border-style` default is `none`, so a width-only border draws
|
|
48
|
+
// nothing. The Hypen DSL writes `.borderWidth(2).borderColor(...)`
|
|
49
|
+
// expecting a visible stroke (the social Stories ring + the Profile
|
|
50
|
+
// page Edit button). Override `none` too — the Button component
|
|
51
|
+
// resets `border: none` at create time, so the inherited
|
|
52
|
+
// border-style is "none" by the time this applicator lands.
|
|
53
|
+
if (!el.style.borderStyle || el.style.borderStyle === "none") {
|
|
54
|
+
el.style.borderStyle = "solid";
|
|
55
|
+
}
|
|
47
56
|
},
|
|
48
57
|
|
|
49
58
|
borderStyle: (el, value) => {
|
|
@@ -89,7 +89,22 @@ export const fontHandlers: Record<string, ApplicatorHandler> = {
|
|
|
89
89
|
},
|
|
90
90
|
|
|
91
91
|
textAlign: (el, value) => {
|
|
92
|
-
|
|
92
|
+
const v = String(value);
|
|
93
|
+
el.style.textAlign = v;
|
|
94
|
+
// `text-align: center | right | end | justify` only takes visible
|
|
95
|
+
// effect when the box is wider than the text. Hypen's Text defaults
|
|
96
|
+
// to `inline-block` (shrink-to-fit), so without this an author
|
|
97
|
+
// writing `Text("Edit Profile").tw("text-center")` saw the same
|
|
98
|
+
// left-flush text whether they set `text-center` or not (the
|
|
99
|
+
// social Profile page Edit button). Stretch the box across its
|
|
100
|
+
// flex parent and switch to block so the alignment has room to act.
|
|
101
|
+
if (v === "center" || v === "right" || v === "end" || v === "justify") {
|
|
102
|
+
if (!el.style.alignSelf) el.style.alignSelf = "stretch";
|
|
103
|
+
if (!el.style.width) el.style.width = "100%";
|
|
104
|
+
if (el.style.display === "inline-block" || !el.style.display) {
|
|
105
|
+
el.style.display = "block";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
93
108
|
},
|
|
94
109
|
|
|
95
110
|
lineHeight: (el, value) => {
|
|
@@ -104,14 +104,24 @@ export const layoutHandlers: Record<string, ApplicatorHandler> = {
|
|
|
104
104
|
// Use .weight(1) to make element take remaining space in Row/Column
|
|
105
105
|
weight: (el, value) => {
|
|
106
106
|
el.style.flex = String(value);
|
|
107
|
-
//
|
|
107
|
+
// CSS sets `min-width/min-height: auto` (= min-content) on flex items by
|
|
108
|
+
// default, so a flex-1 child can never shrink below its intrinsic
|
|
109
|
+
// content size — meaning a 20,000-tall feed inside a 900px viewport
|
|
110
|
+
// overflowed because HomePage's `flex-1` couldn't shrink. The classic
|
|
111
|
+
// CSS workaround is `min-*: 0`. Apply it here so authors don't need to
|
|
112
|
+
// remember; explicit user `minWidth`/`minHeight` overrides it (the
|
|
113
|
+
// applicator runs after `weight`/`flex`).
|
|
114
|
+
if (!el.style.minWidth) el.style.minWidth = "0";
|
|
115
|
+
if (!el.style.minHeight) el.style.minHeight = "0";
|
|
108
116
|
el.dataset.hypenFlex = "true";
|
|
109
117
|
},
|
|
110
118
|
|
|
111
119
|
// flex: CSS flex shorthand (kept for CSS compatibility)
|
|
112
120
|
flex: (el, value) => {
|
|
113
121
|
el.style.flex = String(value);
|
|
114
|
-
//
|
|
122
|
+
// See note on `weight` above — flex children need `min-*: 0` to shrink.
|
|
123
|
+
if (!el.style.minWidth) el.style.minWidth = "0";
|
|
124
|
+
if (!el.style.minHeight) el.style.minHeight = "0";
|
|
115
125
|
el.dataset.hypenFlex = "true";
|
|
116
126
|
},
|
|
117
127
|
|
|
@@ -132,18 +142,35 @@ export const layoutHandlers: Record<string, ApplicatorHandler> = {
|
|
|
132
142
|
},
|
|
133
143
|
|
|
134
144
|
scrollable: (el, value) => {
|
|
135
|
-
|
|
145
|
+
// CSS spec: setting `overflow-x` to a non-visible value forces
|
|
146
|
+
// `overflow-y: auto` (and vice versa) — you can't mix `visible` with
|
|
147
|
+
// a scrolling axis. That combo makes the element a scroll container
|
|
148
|
+
// on both axes, and flex items with overflow get `min-height: auto`
|
|
149
|
+
// collapsed to 0, so a `scrollable("horizontal")` row inside a
|
|
150
|
+
// flex-column parent (the social Stories carousel) shrank to just
|
|
151
|
+
// its padding (18 tall) instead of hugging its 92-tall avatars. We
|
|
152
|
+
// pin `min-height`/`min-width` to `fit-content` on the non-scroll
|
|
153
|
+
// axis so the intrinsic size survives.
|
|
154
|
+
if (value === true || value === "true" || value === "both") {
|
|
136
155
|
el.style.overflow = "auto";
|
|
137
156
|
} else if (value === false || value === "false") {
|
|
138
157
|
el.style.overflow = "hidden";
|
|
139
158
|
} else if (value === "vertical") {
|
|
140
|
-
el.style.overflowX = "hidden";
|
|
141
159
|
el.style.overflowY = "auto";
|
|
160
|
+
if (!el.style.minWidth) el.style.minWidth = "fit-content";
|
|
142
161
|
} else if (value === "horizontal") {
|
|
143
162
|
el.style.overflowX = "auto";
|
|
144
|
-
el.style.
|
|
145
|
-
|
|
146
|
-
|
|
163
|
+
if (!el.style.minHeight) el.style.minHeight = "fit-content";
|
|
164
|
+
// A flex-column parent with `align-items: flex-start` (our Column
|
|
165
|
+
// default, matching iOS/Android "wrap to content") leaves a child
|
|
166
|
+
// row at its natural width — so a 5×96 carousel was 498 wide inside
|
|
167
|
+
// a 470 viewport and the whole page scrolled horizontally. Force
|
|
168
|
+
// the scroll container to fit the parent's cross-axis and clamp at
|
|
169
|
+
// that width so the overflow is trapped inside.
|
|
170
|
+
if (!el.style.alignSelf) el.style.alignSelf = "stretch";
|
|
171
|
+
if (!el.style.width) el.style.width = "100%";
|
|
172
|
+
if (!el.style.maxWidth) el.style.maxWidth = "100%";
|
|
173
|
+
if (!el.style.minWidth) el.style.minWidth = "0";
|
|
147
174
|
} else {
|
|
148
175
|
el.style.overflow = String(value);
|
|
149
176
|
}
|
|
@@ -4,33 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ApplicatorHandler } from "./types.js";
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (typeof value === "object" && value["0"] !== undefined) return Number(value["0"]);
|
|
11
|
-
if (typeof value === "string") return parseFloat(value);
|
|
12
|
-
return null;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
// Extract a CSS length value, supporting numbers, strings ("auto", "10px",
|
|
16
|
-
// percentages), or `{0: value}` positional form. Returns null if no value.
|
|
17
|
-
const getCssLengthValue = (value: any): string | null => {
|
|
18
|
-
if (value === null || value === undefined) return null;
|
|
19
|
-
if (typeof value === "number") return `${value}px`;
|
|
20
|
-
if (typeof value === "string") return value;
|
|
21
|
-
if (typeof value === "object") {
|
|
22
|
-
if (value["0"] !== undefined) {
|
|
23
|
-
const inner = value["0"];
|
|
24
|
-
if (typeof inner === "number") return `${inner}px`;
|
|
25
|
-
if (typeof inner === "string") return inner;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return null;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// Format a value as a CSS length: numbers become "Npx", strings pass through
|
|
7
|
+
// Format a value as a CSS length: numbers become "Npx", strings pass through.
|
|
8
|
+
// Same caveat as `padding.ts#toCssLength`: rem/em/% values must survive,
|
|
9
|
+
// otherwise `parseFloat("0.5rem")` collapses to literal `0.5px`.
|
|
32
10
|
const toCssLength = (v: any): string => {
|
|
11
|
+
if (v == null) return "";
|
|
33
12
|
if (typeof v === "number") return `${v}px`;
|
|
13
|
+
if (typeof v === "object" && v["0"] !== undefined) return toCssLength(v["0"]);
|
|
34
14
|
return String(v);
|
|
35
15
|
};
|
|
36
16
|
|
|
@@ -77,38 +57,31 @@ export const marginHandler: ApplicatorHandler = (el, value) => {
|
|
|
77
57
|
};
|
|
78
58
|
|
|
79
59
|
// Directional margin handlers for .marginTop(8), .marginBottom(8), etc.
|
|
60
|
+
// All routed through `toCssLength` so `rem` / `em` / `%` units survive.
|
|
80
61
|
export const marginTopHandler: ApplicatorHandler = (el, value) => {
|
|
81
|
-
|
|
82
|
-
if (v !== null) el.style.marginTop = `${v}px`;
|
|
62
|
+
el.style.marginTop = toCssLength(value);
|
|
83
63
|
};
|
|
84
64
|
|
|
85
65
|
export const marginBottomHandler: ApplicatorHandler = (el, value) => {
|
|
86
|
-
|
|
87
|
-
if (v !== null) el.style.marginBottom = `${v}px`;
|
|
66
|
+
el.style.marginBottom = toCssLength(value);
|
|
88
67
|
};
|
|
89
68
|
|
|
90
69
|
export const marginLeftHandler: ApplicatorHandler = (el, value) => {
|
|
91
|
-
|
|
92
|
-
if (v !== null) el.style.marginLeft = `${v}px`;
|
|
70
|
+
el.style.marginLeft = toCssLength(value);
|
|
93
71
|
};
|
|
94
72
|
|
|
95
73
|
export const marginRightHandler: ApplicatorHandler = (el, value) => {
|
|
96
|
-
|
|
97
|
-
if (v !== null) el.style.marginRight = `${v}px`;
|
|
74
|
+
el.style.marginRight = toCssLength(value);
|
|
98
75
|
};
|
|
99
76
|
|
|
100
77
|
export const marginHorizontalHandler: ApplicatorHandler = (el, value) => {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
el.style.marginRight = `${v}px`;
|
|
105
|
-
}
|
|
78
|
+
const css = toCssLength(value);
|
|
79
|
+
el.style.marginLeft = css;
|
|
80
|
+
el.style.marginRight = css;
|
|
106
81
|
};
|
|
107
82
|
|
|
108
83
|
export const marginVerticalHandler: ApplicatorHandler = (el, value) => {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
el.style.marginBottom = `${v}px`;
|
|
113
|
-
}
|
|
84
|
+
const css = toCssLength(value);
|
|
85
|
+
el.style.marginTop = css;
|
|
86
|
+
el.style.marginBottom = css;
|
|
114
87
|
};
|
|
@@ -4,17 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ApplicatorHandler } from "./types.js";
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
// Format a value as a CSS length: numbers become "Npx", strings pass through
|
|
7
|
+
// Format a value as a CSS length: numbers become "Npx", strings pass through.
|
|
8
|
+
// Strings come through verbatim because the Tailwind→CSS pipeline emits
|
|
9
|
+
// values like `"0.5rem"` (= py-2) or `"1rem"` (= px-4); stripping the unit
|
|
10
|
+
// via `parseFloat` collapsed them to literal `0.5px` / `1px` and the
|
|
11
|
+
// social feed rendered with no usable spacing (avatars touched the
|
|
12
|
+
// container edges, the BottomNav fell off-screen, etc.).
|
|
16
13
|
const toCssLength = (v: any): string => {
|
|
14
|
+
if (v == null) return "";
|
|
17
15
|
if (typeof v === "number") return `${v}px`;
|
|
16
|
+
if (typeof v === "object" && v["0"] !== undefined) return toCssLength(v["0"]);
|
|
18
17
|
return String(v);
|
|
19
18
|
};
|
|
20
19
|
|
|
@@ -65,38 +64,31 @@ export const paddingHandler: ApplicatorHandler = (el, value) => {
|
|
|
65
64
|
};
|
|
66
65
|
|
|
67
66
|
// Directional padding handlers for .paddingTop(8), .paddingBottom(8), etc.
|
|
67
|
+
// All routed through `toCssLength` so `rem` / `em` / `%` units survive.
|
|
68
68
|
export const paddingTopHandler: ApplicatorHandler = (el, value) => {
|
|
69
|
-
|
|
70
|
-
if (v !== null) el.style.paddingTop = `${v}px`;
|
|
69
|
+
el.style.paddingTop = toCssLength(value);
|
|
71
70
|
};
|
|
72
71
|
|
|
73
72
|
export const paddingBottomHandler: ApplicatorHandler = (el, value) => {
|
|
74
|
-
|
|
75
|
-
if (v !== null) el.style.paddingBottom = `${v}px`;
|
|
73
|
+
el.style.paddingBottom = toCssLength(value);
|
|
76
74
|
};
|
|
77
75
|
|
|
78
76
|
export const paddingLeftHandler: ApplicatorHandler = (el, value) => {
|
|
79
|
-
|
|
80
|
-
if (v !== null) el.style.paddingLeft = `${v}px`;
|
|
77
|
+
el.style.paddingLeft = toCssLength(value);
|
|
81
78
|
};
|
|
82
79
|
|
|
83
80
|
export const paddingRightHandler: ApplicatorHandler = (el, value) => {
|
|
84
|
-
|
|
85
|
-
if (v !== null) el.style.paddingRight = `${v}px`;
|
|
81
|
+
el.style.paddingRight = toCssLength(value);
|
|
86
82
|
};
|
|
87
83
|
|
|
88
84
|
export const paddingHorizontalHandler: ApplicatorHandler = (el, value) => {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
el.style.paddingRight = `${v}px`;
|
|
93
|
-
}
|
|
85
|
+
const css = toCssLength(value);
|
|
86
|
+
el.style.paddingLeft = css;
|
|
87
|
+
el.style.paddingRight = css;
|
|
94
88
|
};
|
|
95
89
|
|
|
96
90
|
export const paddingVerticalHandler: ApplicatorHandler = (el, value) => {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
el.style.paddingBottom = `${v}px`;
|
|
101
|
-
}
|
|
91
|
+
const css = toCssLength(value);
|
|
92
|
+
el.style.paddingTop = css;
|
|
93
|
+
el.style.paddingBottom = css;
|
|
102
94
|
};
|
|
@@ -6,7 +6,17 @@ import type { ApplicatorHandler } from "./types.js";
|
|
|
6
6
|
|
|
7
7
|
export const typographyHandlers: Record<string, ApplicatorHandler> = {
|
|
8
8
|
textAlign: (el, value) => {
|
|
9
|
-
|
|
9
|
+
const v = String(value);
|
|
10
|
+
el.style.textAlign = v;
|
|
11
|
+
// Mirror `font.ts#textAlign`: stretch + block so non-left alignments
|
|
12
|
+
// actually have room to act in a shrink-to-fit `Text` element.
|
|
13
|
+
if (v === "center" || v === "right" || v === "end" || v === "justify") {
|
|
14
|
+
if (!el.style.alignSelf) el.style.alignSelf = "stretch";
|
|
15
|
+
if (!el.style.width) el.style.width = "100%";
|
|
16
|
+
if (el.style.display === "inline-block" || !el.style.display) {
|
|
17
|
+
el.style.display = "block";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
10
20
|
},
|
|
11
21
|
|
|
12
22
|
textTransform: (el, value) => {
|