@hypen-space/web 0.4.980 → 0.5.2
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/README.md +20 -13
- package/dist/canvas/events.js +46 -7
- package/dist/canvas/events.js.map +1 -1
- package/dist/canvas/index.d.ts +0 -1
- package/dist/canvas/index.js +0 -1
- package/dist/canvas/index.js.map +1 -1
- package/dist/canvas/layout.js +68 -98
- package/dist/canvas/layout.js.map +1 -1
- package/dist/canvas/paint.js +168 -71
- package/dist/canvas/paint.js.map +1 -1
- package/dist/canvas/renderer.d.ts +8 -3
- package/dist/canvas/renderer.js +32 -43
- package/dist/canvas/renderer.js.map +1 -1
- package/dist/canvas/text.js +2 -9
- package/dist/canvas/text.js.map +1 -1
- package/dist/canvas/types.d.ts +3 -0
- package/dist/canvas/utils.d.ts +0 -23
- package/dist/canvas/utils.js +3 -67
- package/dist/canvas/utils.js.map +1 -1
- package/dist/canvas/variants.d.ts +49 -0
- package/dist/canvas/variants.js +137 -0
- package/dist/canvas/variants.js.map +1 -0
- package/dist/dom/applicators/events.js +1 -0
- package/dist/dom/applicators/events.js.map +1 -1
- package/dist/dom/applicators/font.d.ts +11 -6
- package/dist/dom/applicators/font.js +16 -27
- package/dist/dom/applicators/font.js.map +1 -1
- package/dist/dom/applicators/index.js +47 -45
- package/dist/dom/applicators/index.js.map +1 -1
- package/dist/dom/canvas/index.js +2 -2
- package/dist/dom/canvas/index.js.map +1 -1
- package/dist/dom/components/app.js +2 -2
- package/dist/dom/components/app.js.map +1 -1
- package/dist/dom/components/audio.js +2 -2
- package/dist/dom/components/audio.js.map +1 -1
- package/dist/dom/components/avatar.js +3 -4
- package/dist/dom/components/avatar.js.map +1 -1
- package/dist/dom/components/badge.js +3 -4
- package/dist/dom/components/badge.js.map +1 -1
- package/dist/dom/components/button.js +2 -2
- package/dist/dom/components/button.js.map +1 -1
- package/dist/dom/components/card.js +2 -2
- package/dist/dom/components/card.js.map +1 -1
- package/dist/dom/components/center.js +2 -2
- package/dist/dom/components/center.js.map +1 -1
- package/dist/dom/components/checkbox.js +4 -5
- package/dist/dom/components/checkbox.js.map +1 -1
- package/dist/dom/components/column.js +2 -2
- package/dist/dom/components/column.js.map +1 -1
- package/dist/dom/components/container.js +2 -2
- package/dist/dom/components/container.js.map +1 -1
- package/dist/dom/components/divider.js +2 -2
- package/dist/dom/components/divider.js.map +1 -1
- package/dist/dom/components/grid.js +10 -10
- package/dist/dom/components/grid.js.map +1 -1
- package/dist/dom/components/heading.js +4 -6
- package/dist/dom/components/heading.js.map +1 -1
- package/dist/dom/components/hypenapp.js +2 -2
- package/dist/dom/components/hypenapp.js.map +1 -1
- package/dist/dom/components/icon.js +4 -5
- package/dist/dom/components/icon.js.map +1 -1
- package/dist/dom/components/image.js +2 -2
- package/dist/dom/components/image.js.map +1 -1
- package/dist/dom/components/index.d.ts +2 -2
- package/dist/dom/components/index.js +2 -2
- package/dist/dom/components/index.js.map +1 -1
- package/dist/dom/components/input.js +2 -2
- package/dist/dom/components/input.js.map +1 -1
- package/dist/dom/components/link.js +2 -2
- package/dist/dom/components/link.js.map +1 -1
- package/dist/dom/components/list.js +2 -2
- package/dist/dom/components/list.js.map +1 -1
- package/dist/dom/components/paragraph.js +3 -4
- package/dist/dom/components/paragraph.js.map +1 -1
- package/dist/dom/components/progressbar.js +3 -3
- package/dist/dom/components/progressbar.js.map +1 -1
- package/dist/dom/components/route.js +2 -2
- package/dist/dom/components/route.js.map +1 -1
- package/dist/dom/components/router.js +2 -2
- package/dist/dom/components/router.js.map +1 -1
- package/dist/dom/components/row.js +10 -10
- package/dist/dom/components/row.js.map +1 -1
- package/dist/dom/components/select.js +3 -4
- package/dist/dom/components/select.js.map +1 -1
- package/dist/dom/components/slider.js +2 -2
- package/dist/dom/components/slider.js.map +1 -1
- package/dist/dom/components/spacer.js +2 -2
- package/dist/dom/components/spacer.js.map +1 -1
- package/dist/dom/components/spinner.js +4 -4
- package/dist/dom/components/spinner.js.map +1 -1
- package/dist/dom/components/stack.js +10 -10
- package/dist/dom/components/stack.js.map +1 -1
- package/dist/dom/components/switch.js +5 -6
- package/dist/dom/components/switch.js.map +1 -1
- package/dist/dom/components/text.js +4 -7
- package/dist/dom/components/text.js.map +1 -1
- package/dist/dom/components/textarea.js +2 -2
- package/dist/dom/components/textarea.js.map +1 -1
- package/dist/dom/components/video.js +2 -2
- package/dist/dom/components/video.js.map +1 -1
- package/dist/dom/debug.js +6 -8
- package/dist/dom/debug.js.map +1 -1
- package/dist/dom/index.d.ts +0 -1
- package/dist/dom/index.js +0 -1
- package/dist/dom/index.js.map +1 -1
- package/dist/dom/renderer.js +2 -3
- package/dist/dom/renderer.js.map +1 -1
- package/dist/variants.d.ts +90 -0
- package/dist/variants.js +206 -0
- package/dist/variants.js.map +1 -0
- package/package.json +2 -2
- package/src/canvas/QUICKSTART.md +12 -7
- package/src/canvas/README.md +8 -5
- package/src/canvas/events.ts +49 -7
- package/src/canvas/index.ts +0 -8
- package/src/canvas/layout.ts +68 -103
- package/src/canvas/paint.ts +172 -69
- package/src/canvas/renderer.ts +38 -46
- package/src/canvas/text.ts +2 -9
- package/src/canvas/types.ts +14 -0
- package/src/canvas/utils.ts +3 -69
- package/src/canvas/variants.ts +153 -0
- package/src/dom/README.md +8 -7
- package/src/dom/applicators/events.ts +1 -0
- package/src/dom/applicators/font.ts +16 -29
- package/src/dom/applicators/index.ts +56 -46
- package/src/dom/canvas/index.ts +2 -2
- package/src/dom/components/app.ts +2 -2
- package/src/dom/components/audio.ts +2 -2
- package/src/dom/components/avatar.ts +3 -4
- package/src/dom/components/badge.ts +3 -4
- package/src/dom/components/button.ts +2 -2
- package/src/dom/components/card.ts +2 -2
- package/src/dom/components/center.ts +2 -2
- package/src/dom/components/checkbox.ts +4 -5
- package/src/dom/components/column.ts +2 -2
- package/src/dom/components/container.ts +2 -2
- package/src/dom/components/divider.ts +2 -2
- package/src/dom/components/grid.ts +10 -10
- package/src/dom/components/heading.ts +4 -6
- package/src/dom/components/hypenapp.ts +2 -2
- package/src/dom/components/icon.ts +4 -5
- package/src/dom/components/image.ts +2 -2
- package/src/dom/components/index.ts +3 -3
- package/src/dom/components/input.ts +2 -2
- package/src/dom/components/link.ts +2 -2
- package/src/dom/components/list.ts +2 -2
- package/src/dom/components/paragraph.ts +3 -4
- package/src/dom/components/progressbar.ts +3 -3
- package/src/dom/components/route.ts +2 -2
- package/src/dom/components/router.ts +2 -2
- package/src/dom/components/row.ts +10 -10
- package/src/dom/components/select.ts +3 -4
- package/src/dom/components/slider.ts +2 -2
- package/src/dom/components/spacer.ts +2 -2
- package/src/dom/components/spinner.ts +4 -4
- package/src/dom/components/stack.ts +10 -10
- package/src/dom/components/switch.ts +5 -6
- package/src/dom/components/text.ts +4 -7
- package/src/dom/components/textarea.ts +2 -2
- package/src/dom/components/video.ts +2 -2
- package/src/dom/debug.ts +6 -8
- package/src/dom/index.ts +0 -8
- package/src/dom/renderer.ts +2 -3
- package/src/variants.ts +249 -0
- package/dist/client.d.ts +0 -36
- package/dist/client.js +0 -47
- package/dist/client.js.map +0 -1
- package/src/client.ts +0 -88
package/src/canvas/QUICKSTART.md
CHANGED
|
@@ -30,7 +30,7 @@ bun add @hypen-space/core @hypen-space/web @hypen-space/web-engine
|
|
|
30
30
|
```typescript
|
|
31
31
|
import { app, HypenModuleInstance } from "@hypen-space/core";
|
|
32
32
|
import { Engine } from "@hypen-space/web-engine";
|
|
33
|
-
import {
|
|
33
|
+
import { CanvasRenderer } from "@hypen-space/web/canvas";
|
|
34
34
|
|
|
35
35
|
// Define state
|
|
36
36
|
type AppState = {
|
|
@@ -54,12 +54,17 @@ async function main() {
|
|
|
54
54
|
const engine = new Engine();
|
|
55
55
|
await engine.init();
|
|
56
56
|
|
|
57
|
-
// Create renderer
|
|
58
|
-
|
|
57
|
+
// Create renderer
|
|
58
|
+
const renderer = new CanvasRenderer(canvas, engine, {
|
|
59
59
|
devicePixelRatio: window.devicePixelRatio,
|
|
60
60
|
backgroundColor: "#ffffff",
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
// Connect renderer
|
|
64
|
+
engine.setRenderCallback((patches) => {
|
|
65
|
+
renderer.applyPatches(patches);
|
|
66
|
+
});
|
|
67
|
+
|
|
63
68
|
// Create module
|
|
64
69
|
const instance = new HypenModuleInstance(engine, appModule);
|
|
65
70
|
|
|
@@ -244,7 +249,7 @@ Container {
|
|
|
244
249
|
## Configuration Options
|
|
245
250
|
|
|
246
251
|
```typescript
|
|
247
|
-
const
|
|
252
|
+
const renderer = new CanvasRenderer(canvas, engine, {
|
|
248
253
|
// Display
|
|
249
254
|
devicePixelRatio: window.devicePixelRatio, // HiDPI support
|
|
250
255
|
backgroundColor: "#ffffff", // Canvas background
|
|
@@ -349,7 +354,7 @@ let timeoutId: number;
|
|
|
349
354
|
### 3. Measure Performance
|
|
350
355
|
|
|
351
356
|
```typescript
|
|
352
|
-
const
|
|
357
|
+
const renderer = new CanvasRenderer(canvas, engine, {
|
|
353
358
|
logPerformance: true, // Logs FPS and frame times
|
|
354
359
|
});
|
|
355
360
|
```
|
|
@@ -361,7 +366,7 @@ const { renderer } = createHypenClient(canvas, engine, {
|
|
|
361
366
|
Make sure to set `devicePixelRatio`:
|
|
362
367
|
|
|
363
368
|
```typescript
|
|
364
|
-
const
|
|
369
|
+
const renderer = new CanvasRenderer(canvas, engine, {
|
|
365
370
|
devicePixelRatio: window.devicePixelRatio,
|
|
366
371
|
});
|
|
367
372
|
```
|
|
@@ -371,7 +376,7 @@ const { renderer } = createHypenClient(canvas, engine, {
|
|
|
371
376
|
Ensure `enableInputOverlay` is true:
|
|
372
377
|
|
|
373
378
|
```typescript
|
|
374
|
-
const
|
|
379
|
+
const renderer = new CanvasRenderer(canvas, engine, {
|
|
375
380
|
enableInputOverlay: true,
|
|
376
381
|
});
|
|
377
382
|
```
|
package/src/canvas/README.md
CHANGED
|
@@ -13,12 +13,10 @@ The Canvas Renderer is a complete alternative to the DOM renderer that draws all
|
|
|
13
13
|
|
|
14
14
|
## Quick Start
|
|
15
15
|
|
|
16
|
-
`createHypenClient` is the recommended one-call wiring (constructs the renderer and subscribes to engine patches). `CanvasRenderer` is still exported directly for advanced setups.
|
|
17
|
-
|
|
18
16
|
```typescript
|
|
19
17
|
import { app } from "@hypen-space/core";
|
|
20
18
|
import { Engine } from "@hypen-space/web-engine";
|
|
21
|
-
import {
|
|
19
|
+
import { CanvasRenderer } from "@hypen-space/web/canvas";
|
|
22
20
|
|
|
23
21
|
// Setup canvas
|
|
24
22
|
const canvas = document.getElementById("app") as HTMLCanvasElement;
|
|
@@ -27,8 +25,13 @@ const canvas = document.getElementById("app") as HTMLCanvasElement;
|
|
|
27
25
|
const engine = new Engine();
|
|
28
26
|
await engine.init();
|
|
29
27
|
|
|
30
|
-
// Create canvas renderer
|
|
31
|
-
|
|
28
|
+
// Create canvas renderer
|
|
29
|
+
const renderer = new CanvasRenderer(canvas, engine);
|
|
30
|
+
|
|
31
|
+
// Set render callback
|
|
32
|
+
engine.setRenderCallback((patches) => {
|
|
33
|
+
renderer.applyPatches(patches);
|
|
34
|
+
});
|
|
32
35
|
|
|
33
36
|
// Render UI
|
|
34
37
|
await engine.renderSource(`
|
package/src/canvas/events.ts
CHANGED
|
@@ -14,6 +14,21 @@ interface IEngine {
|
|
|
14
14
|
dispatchAction(name: string, payload?: any): void;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// Maps a DOM event type to the applicator prop names the engine may have set
|
|
18
|
+
// on a node, in priority order. Multi-word events (`mouseenter`) need their
|
|
19
|
+
// proper camelCase form (`onMouseEnter`) since the engine emits applicator
|
|
20
|
+
// names verbatim. `mouseenter` also accepts `onHover` as an alias.
|
|
21
|
+
const CANVAS_EVENT_PROP_NAMES: Record<string, string[]> = {
|
|
22
|
+
mouseenter: ["onMouseEnter", "onHover", "onmouseenter", "mouseenter"],
|
|
23
|
+
mouseleave: ["onMouseLeave", "onmouseleave", "mouseleave"],
|
|
24
|
+
mousedown: ["onMouseDown", "onmousedown", "mousedown"],
|
|
25
|
+
mouseup: ["onMouseUp", "onmouseup", "mouseup"],
|
|
26
|
+
dblclick: ["onDblClick", "onDoubleClick", "ondblclick", "dblclick"],
|
|
27
|
+
contextmenu: ["onContextMenu", "oncontextmenu", "contextmenu"],
|
|
28
|
+
keydown: ["onKeyDown", "onkeydown", "keydown"],
|
|
29
|
+
keyup: ["onKeyUp", "onkeyup", "keyup"],
|
|
30
|
+
};
|
|
31
|
+
|
|
17
32
|
/**
|
|
18
33
|
* Canvas Event Manager
|
|
19
34
|
*/
|
|
@@ -210,6 +225,13 @@ export class CanvasEventManager {
|
|
|
210
225
|
|
|
211
226
|
this.mouseDownNode = node;
|
|
212
227
|
|
|
228
|
+
// Track pressed (`:active`) state so paint-time `:active` variants resolve.
|
|
229
|
+
// Repaint so the active style appears immediately on press.
|
|
230
|
+
if (node) {
|
|
231
|
+
node.pressed = true;
|
|
232
|
+
this.requestRedraw();
|
|
233
|
+
}
|
|
234
|
+
|
|
213
235
|
if (node && node.clickable) {
|
|
214
236
|
this.dispatchNodeEvent(node, "mousedown", {
|
|
215
237
|
button: e.button,
|
|
@@ -234,6 +256,13 @@ export class CanvasEventManager {
|
|
|
234
256
|
const hit = this.hitTest(point);
|
|
235
257
|
const node = this.findClickableAncestor(hit) ?? hit;
|
|
236
258
|
|
|
259
|
+
// Clear the pressed (`:active`) flag from the node that was pressed —
|
|
260
|
+
// release ends `:active` even if the pointer drifted off the node first.
|
|
261
|
+
if (this.mouseDownNode && this.mouseDownNode.pressed) {
|
|
262
|
+
this.mouseDownNode.pressed = false;
|
|
263
|
+
this.requestRedraw();
|
|
264
|
+
}
|
|
265
|
+
|
|
237
266
|
if (node && node.clickable) {
|
|
238
267
|
this.dispatchNodeEvent(node, "mouseup", {
|
|
239
268
|
button: e.button,
|
|
@@ -383,13 +412,25 @@ export class CanvasEventManager {
|
|
|
383
412
|
* Dispatch event to engine
|
|
384
413
|
*/
|
|
385
414
|
private dispatchNodeEvent(node: VirtualNode, eventType: string, data: any): void {
|
|
386
|
-
// Engine emits event applicators in camelCase (`onClick`, `
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
|
|
392
|
-
|
|
415
|
+
// Engine emits event applicators in camelCase (`onClick`, `onMouseEnter`).
|
|
416
|
+
// Multi-word DOM events like `mouseenter` must map to `onMouseEnter`, not
|
|
417
|
+
// the naive `onMouseenter` that `on${capitalize(eventType)}` would produce.
|
|
418
|
+
// The older flat form `onclick`/`onmouseenter` is still accepted. After
|
|
419
|
+
// prop normalisation the value is either a string (action name) or an
|
|
420
|
+
// object carrying an action name at `"0"` plus an auxiliary payload.
|
|
421
|
+
const propNames = CANVAS_EVENT_PROP_NAMES[eventType] ?? [
|
|
422
|
+
`on${eventType.charAt(0).toUpperCase()}${eventType.slice(1)}`,
|
|
423
|
+
`on${eventType}`,
|
|
424
|
+
eventType,
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
let spec: unknown;
|
|
428
|
+
for (const name of propNames) {
|
|
429
|
+
if (node.props[name] != null) {
|
|
430
|
+
spec = node.props[name];
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
393
434
|
|
|
394
435
|
// Actionable components fall back to the bare `action` prop on click.
|
|
395
436
|
if (spec == null && eventType === "click") {
|
|
@@ -429,6 +470,7 @@ export class CanvasEventManager {
|
|
|
429
470
|
this.canvas.removeEventListener("contextmenu", this.boundOnContextMenu);
|
|
430
471
|
this.canvas.removeEventListener("keydown", this.boundOnKeyDown);
|
|
431
472
|
this.canvas.removeEventListener("keyup", this.boundOnKeyUp);
|
|
473
|
+
if (this.mouseDownNode) this.mouseDownNode.pressed = false;
|
|
432
474
|
this.rootNode = null;
|
|
433
475
|
this.hoveredNode = null;
|
|
434
476
|
this.focusedNode = null;
|
package/src/canvas/index.ts
CHANGED
|
@@ -5,14 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export { CanvasRenderer } from "./renderer.js";
|
|
8
|
-
export {
|
|
9
|
-
createCanvasClient as createHypenClient,
|
|
10
|
-
createCanvasClient,
|
|
11
|
-
type CanvasClient as HypenClient,
|
|
12
|
-
type CanvasClient,
|
|
13
|
-
type CanvasClientOptions as HypenClientOptions,
|
|
14
|
-
type CanvasClientOptions,
|
|
15
|
-
} from "../client.js";
|
|
16
8
|
export { registerPainter } from "./paint.js";
|
|
17
9
|
export { CanvasEventManager } from "./events.js";
|
|
18
10
|
export { InputOverlay } from "./input.js";
|
package/src/canvas/layout.ts
CHANGED
|
@@ -7,15 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { VirtualNode, Layout, BoxSpacing } from "./types.js";
|
|
10
|
-
import {
|
|
11
|
-
parseSpacing,
|
|
12
|
-
parseSize,
|
|
13
|
-
cssLengthToPx,
|
|
14
|
-
cssLengthToDimension,
|
|
15
|
-
resolveLineHeight,
|
|
16
|
-
setCurrentViewport,
|
|
17
|
-
getCurrentViewport,
|
|
18
|
-
} from "./utils.js";
|
|
10
|
+
import { parseSpacing, parseSize, cssLengthToPx, cssLengthToDimension } from "./utils.js";
|
|
19
11
|
import { measureText } from "./text.js";
|
|
20
12
|
import { getImageNaturalAspect } from "./paint.js";
|
|
21
13
|
|
|
@@ -55,6 +47,19 @@ export async function initTaffyLayout(): Promise<boolean> {
|
|
|
55
47
|
taffyInitPromise = (async () => {
|
|
56
48
|
try {
|
|
57
49
|
taffy = await import("taffy-layout");
|
|
50
|
+
// In a server runtime (Bun/Node — including test runs where a JSDOM
|
|
51
|
+
// `window` may be globally registered by another test file), load the
|
|
52
|
+
// WASM from node_modules via fs. This path is deterministic; the
|
|
53
|
+
// fetch-based branch below depends on a reachable origin/CDN and must
|
|
54
|
+
// never be selected just because a test polyfilled `window`.
|
|
55
|
+
const isServerRuntime =
|
|
56
|
+
typeof process !== "undefined" &&
|
|
57
|
+
!!(process.versions?.bun || process.versions?.node);
|
|
58
|
+
if (isServerRuntime) {
|
|
59
|
+
await taffy.loadTaffy();
|
|
60
|
+
taffyReady = true;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
58
63
|
// Bundlers (esp. Bun's browser bundler) sometimes inline a file:// URL
|
|
59
64
|
// for import.meta.url inside taffy_wasm.js, which the browser blocks
|
|
60
65
|
// ("Not allowed to load local resource"). The package's own `loadTaffy`
|
|
@@ -62,26 +67,40 @@ export async function initTaffyLayout(): Promise<boolean> {
|
|
|
62
67
|
// raw `__wbg_init` (default export of the `taffy-layout/wasm` subpath)
|
|
63
68
|
// directly with an explicit Response.
|
|
64
69
|
let usedExplicit = false;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
70
|
+
// Only take the fetch path in a real browser. Test environments (and
|
|
71
|
+
// anything else that installs a fake `window` global) must fall through
|
|
72
|
+
// to `loadTaffy()`, which resolves the WASM from the package on disk —
|
|
73
|
+
// a fake window without `location.origin` used to throw here and
|
|
74
|
+
// silently disable Taffy for the rest of the process.
|
|
75
|
+
const isRealBrowser =
|
|
76
|
+
typeof window !== "undefined" &&
|
|
77
|
+
typeof fetch === "function" &&
|
|
78
|
+
typeof window.location?.origin === "string" &&
|
|
79
|
+
window.document?.defaultView === window;
|
|
80
|
+
if (isRealBrowser) {
|
|
81
|
+
try {
|
|
82
|
+
const candidates = [
|
|
83
|
+
new URL("/taffy_wasm_bg.wasm", window.location.origin),
|
|
84
|
+
new URL("https://cdn.jsdelivr.net/npm/taffy-layout@2.0.3/pkg/taffy_wasm_bg.wasm"),
|
|
85
|
+
new URL("https://unpkg.com/taffy-layout@2.0.3/pkg/taffy_wasm_bg.wasm"),
|
|
86
|
+
];
|
|
87
|
+
const rawWasm = await import(
|
|
88
|
+
/* @vite-ignore */ "taffy-layout/wasm" as string
|
|
89
|
+
);
|
|
90
|
+
const wbgInit = rawWasm.default as (input?: any) => Promise<unknown>;
|
|
91
|
+
for (const url of candidates) {
|
|
92
|
+
try {
|
|
93
|
+
const res = await fetch(url);
|
|
94
|
+
if (!res.ok) continue;
|
|
95
|
+
await wbgInit({ module_or_path: res });
|
|
96
|
+
usedExplicit = true;
|
|
97
|
+
break;
|
|
98
|
+
} catch {
|
|
99
|
+
// Try next candidate
|
|
100
|
+
}
|
|
84
101
|
}
|
|
102
|
+
} catch {
|
|
103
|
+
// Fall through to loadTaffy()
|
|
85
104
|
}
|
|
86
105
|
}
|
|
87
106
|
if (!usedExplicit) {
|
|
@@ -180,22 +199,9 @@ function buildTaffyStyle(
|
|
|
180
199
|
// `List` is the DSL's vertical-stack iterator (see dom/components/list.ts
|
|
181
200
|
// — `flex-direction: column` is its default). Match that here so feeds
|
|
182
201
|
// like Notifications stack their rows vertically instead of flowing
|
|
183
|
-
// sideways. The
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
// vertically because the List default won the tie-break).
|
|
187
|
-
const isListColumn =
|
|
188
|
-
type === "list" &&
|
|
189
|
-
props.direction !== "horizontal" &&
|
|
190
|
-
props.flexDirection !== "row";
|
|
191
|
-
// Button defaults to `flex-direction: column` to match the DOM renderer
|
|
192
|
-
// (see dom/components/button.ts). With Taffy's default `align-items:
|
|
193
|
-
// stretch`, treating Button as a row caused inner `Column { Icon, Text }`
|
|
194
|
-
// children to collapse onto each other (icon overlapping label). Only
|
|
195
|
-
// apply the column default when no explicit `flexDirection` is set so a
|
|
196
|
-
// Tailwind `flex-row` on a button still wins.
|
|
197
|
-
const isButtonColumn = type === "button" && props.flexDirection === undefined;
|
|
198
|
-
const isColumn = !isStack && (type === "column" || isListColumn || isButtonColumn || props.flexDirection === "column");
|
|
202
|
+
// sideways. The `direction` prop can still flip it to row.
|
|
203
|
+
const isListColumn = type === "list" && props.direction !== "horizontal";
|
|
204
|
+
const isColumn = !isStack && (type === "column" || isListColumn || props.flexDirection === "column");
|
|
199
205
|
const isGrid = !isStack && (type === "grid" || props.display === "grid");
|
|
200
206
|
|
|
201
207
|
if (isStack) {
|
|
@@ -386,7 +392,7 @@ function buildTaffyStyle(
|
|
|
386
392
|
// Taffy defaults to `box-sizing: border-box`, so we add the
|
|
387
393
|
// padding/border ourselves into `size.height`.
|
|
388
394
|
const fontSize = cssLengthToPx(props.fontSize) ?? 16;
|
|
389
|
-
const lineHeight =
|
|
395
|
+
const lineHeight = cssLengthToPx(props.lineHeight) ?? fontSize * 1.5;
|
|
390
396
|
const minRows = type === "textarea" ? Math.max(1, Number(props.rows) || 3) : 1;
|
|
391
397
|
const padTop = cssLengthToPx(props.paddingTop ?? props.padding) ?? 0;
|
|
392
398
|
const padBottom = cssLengthToPx(props.paddingBottom ?? props.padding) ?? 0;
|
|
@@ -408,30 +414,7 @@ function buildTaffyStyle(
|
|
|
408
414
|
(parent.type.toLowerCase() === "grid" || parent.props.display === "grid");
|
|
409
415
|
const widthIn = parentIsGridLayout && props.width === "100%" ? undefined : props.width;
|
|
410
416
|
const heightIn = parentIsGridLayout && props.height === "100%" ? undefined : props.height;
|
|
411
|
-
|
|
412
|
-
// Mirror the DOM rule `[data-hypen-type="row"]:has(> [data-hypen-flex])
|
|
413
|
-
// { width: 100% }` (and the symmetric column case). When a flex
|
|
414
|
-
// container has no explicit main-axis size and any child opts into
|
|
415
|
-
// weighted growth (`flex` / `flexGrow > 0`), Taffy would otherwise
|
|
416
|
-
// size the container to its min-content — so a Row of `flex-1`
|
|
417
|
-
// segmented-control buttons stayed pill-sized instead of stretching
|
|
418
|
-
// across the parent. Only do this when at least one child explicitly
|
|
419
|
-
// asks for flex, matching the DOM `:has(> [data-hypen-flex])` guard.
|
|
420
|
-
const isRowContainer = !isStack && !isGrid && !isColumn;
|
|
421
|
-
const isColumnContainer = !isStack && !isGrid && isColumn;
|
|
422
|
-
const childAsksFlex = node.children.some((c) => {
|
|
423
|
-
const fg = parseFloat(c.props.flexGrow);
|
|
424
|
-
if (Number.isFinite(fg) && fg > 0) return true;
|
|
425
|
-
const f = parseFloat(c.props.flex);
|
|
426
|
-
return Number.isFinite(f) && f > 0;
|
|
427
|
-
});
|
|
428
|
-
let resolvedWidth = toDimension(widthIn);
|
|
429
|
-
let resolvedHeight = toDimension(heightIn);
|
|
430
|
-
if (childAsksFlex) {
|
|
431
|
-
if (isRowContainer && widthIn === undefined) resolvedWidth = "100%";
|
|
432
|
-
if (isColumnContainer && heightIn === undefined) resolvedHeight = "100%";
|
|
433
|
-
}
|
|
434
|
-
style.size = { width: resolvedWidth, height: resolvedHeight };
|
|
417
|
+
style.size = { width: toDimension(widthIn), height: toDimension(heightIn) };
|
|
435
418
|
}
|
|
436
419
|
|
|
437
420
|
// --- Min / Max constraints -------------------------------------------------
|
|
@@ -806,17 +789,12 @@ function buildTree(
|
|
|
806
789
|
// container with size 0×0, and downstream siblings were placed on top of
|
|
807
790
|
// each other ("1,431 likes" overlapping the caption row was the visible
|
|
808
791
|
// symptom).
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
// those cases skip the measurement leaf and inherit a 0-height container,
|
|
812
|
-
// which made siblings stack on top of each other (a bound `0` overlapping
|
|
813
|
-
// the next line was the visible symptom in calorie-counter's hero).
|
|
814
|
-
if (node.type.toLowerCase() === "text" && props[0] != null && node.children.length === 0) {
|
|
815
|
-
const text = String(props[0]);
|
|
792
|
+
if (node.type.toLowerCase() === "text" && props[0] && node.children.length === 0) {
|
|
793
|
+
const text = String(props[0] || "");
|
|
816
794
|
const fontSize = cssLengthToPx(props.fontSize) ?? 16;
|
|
817
795
|
const fontWeight = props.fontWeight || "normal";
|
|
818
796
|
const fontFamily = props.fontFamily || "system-ui, sans-serif";
|
|
819
|
-
const lineHeight =
|
|
797
|
+
const lineHeight = cssLengthToPx(props.lineHeight) ?? fontSize * 1.2;
|
|
820
798
|
|
|
821
799
|
const p = parseSpacing(props.padding || 0);
|
|
822
800
|
if (props.paddingTop !== undefined) p.top = cssLengthToPx(props.paddingTop) ?? 0;
|
|
@@ -1130,30 +1108,18 @@ export function computeLayout(
|
|
|
1130
1108
|
x: number = 0,
|
|
1131
1109
|
y: number = 0,
|
|
1132
1110
|
): void {
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
try {
|
|
1142
|
-
if (taffyReady && taffy) {
|
|
1111
|
+
if (taffyReady && taffy) {
|
|
1112
|
+
computeLayoutTaffy(ctx, node, availableWidth, availableHeight, x, y);
|
|
1113
|
+
// After the first pass, Taffy's grid auto-row sizing for aspect-ratio
|
|
1114
|
+
// image leaves can collapse rows (the leaf's max-content height is 0
|
|
1115
|
+
// before column widths are known). Detect that pattern and re-run
|
|
1116
|
+
// with explicit `gridAutoRows` derived from the now-known column
|
|
1117
|
+
// width — see `annotateCollapsedAspectGrids`.
|
|
1118
|
+
if (annotateCollapsedAspectGrids(node)) {
|
|
1143
1119
|
computeLayoutTaffy(ctx, node, availableWidth, availableHeight, x, y);
|
|
1144
|
-
// After the first pass, Taffy's grid auto-row sizing for aspect-ratio
|
|
1145
|
-
// image leaves can collapse rows (the leaf's max-content height is 0
|
|
1146
|
-
// before column widths are known). Detect that pattern and re-run
|
|
1147
|
-
// with explicit `gridAutoRows` derived from the now-known column
|
|
1148
|
-
// width — see `annotateCollapsedAspectGrids`.
|
|
1149
|
-
if (annotateCollapsedAspectGrids(node)) {
|
|
1150
|
-
computeLayoutTaffy(ctx, node, availableWidth, availableHeight, x, y);
|
|
1151
|
-
}
|
|
1152
|
-
} else {
|
|
1153
|
-
computeLayoutFallback(ctx, node, availableWidth, availableHeight, x, y);
|
|
1154
1120
|
}
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1121
|
+
} else {
|
|
1122
|
+
computeLayoutFallback(ctx, node, availableWidth, availableHeight, x, y);
|
|
1157
1123
|
}
|
|
1158
1124
|
}
|
|
1159
1125
|
|
|
@@ -1243,13 +1209,12 @@ function computeLayoutFallback(
|
|
|
1243
1209
|
if (height === null) height = size;
|
|
1244
1210
|
}
|
|
1245
1211
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
const text = String(node.props[0]);
|
|
1212
|
+
if (node.type.toLowerCase() === "text" && node.props[0]) {
|
|
1213
|
+
const text = String(node.props[0] || "");
|
|
1249
1214
|
const fontSize = cssLengthToPx(props.fontSize) ?? 16;
|
|
1250
1215
|
const fontWeight = props.fontWeight || "normal";
|
|
1251
1216
|
const fontFamily = props.fontFamily || "system-ui, sans-serif";
|
|
1252
|
-
const lineHeight =
|
|
1217
|
+
const lineHeight = cssLengthToPx(props.lineHeight) ?? fontSize * 1.2;
|
|
1253
1218
|
|
|
1254
1219
|
const maxWidth = width || availableAfterMargin.width - padding.left - padding.right;
|
|
1255
1220
|
const maxLinesRaw = props.maxLines;
|