@oh-just-another/templates 0.57.0
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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/built-in.d.ts +16 -0
- package/dist/built-in.d.ts.map +1 -0
- package/dist/built-in.js +204 -0
- package/dist/built-in.js.map +1 -0
- package/dist/constants.d.ts +11 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +11 -0
- package/dist/constants.js.map +1 -0
- package/dist/icons.d.ts +21 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +24 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/library.d.ts +29 -0
- package/dist/library.d.ts.map +1 -0
- package/dist/library.js +60 -0
- package/dist/library.js.map +1 -0
- package/dist/registry.d.ts +27 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +48 -0
- package/dist/registry.js.map +1 -0
- package/dist/rich/binding.d.ts +10 -0
- package/dist/rich/binding.d.ts.map +1 -0
- package/dist/rich/binding.js +42 -0
- package/dist/rich/binding.js.map +1 -0
- package/dist/rich/bounds.d.ts +16 -0
- package/dist/rich/bounds.d.ts.map +1 -0
- package/dist/rich/bounds.js +13 -0
- package/dist/rich/bounds.js.map +1 -0
- package/dist/rich/define.d.ts +44 -0
- package/dist/rich/define.d.ts.map +1 -0
- package/dist/rich/define.js +3 -0
- package/dist/rich/define.js.map +1 -0
- package/dist/rich/drop-zones.d.ts +25 -0
- package/dist/rich/drop-zones.d.ts.map +1 -0
- package/dist/rich/drop-zones.js +63 -0
- package/dist/rich/drop-zones.js.map +1 -0
- package/dist/rich/extends.d.ts +35 -0
- package/dist/rich/extends.d.ts.map +1 -0
- package/dist/rich/extends.js +46 -0
- package/dist/rich/extends.js.map +1 -0
- package/dist/rich/hit-test.d.ts +14 -0
- package/dist/rich/hit-test.d.ts.map +1 -0
- package/dist/rich/hit-test.js +53 -0
- package/dist/rich/hit-test.js.map +1 -0
- package/dist/rich/index.d.ts +22 -0
- package/dist/rich/index.d.ts.map +1 -0
- package/dist/rich/index.js +15 -0
- package/dist/rich/index.js.map +1 -0
- package/dist/rich/interactive.d.ts +44 -0
- package/dist/rich/interactive.d.ts.map +1 -0
- package/dist/rich/interactive.js +80 -0
- package/dist/rich/interactive.js.map +1 -0
- package/dist/rich/layout.d.ts +44 -0
- package/dist/rich/layout.d.ts.map +1 -0
- package/dist/rich/layout.js +418 -0
- package/dist/rich/layout.js.map +1 -0
- package/dist/rich/node.d.ts +96 -0
- package/dist/rich/node.d.ts.map +1 -0
- package/dist/rich/node.js +7 -0
- package/dist/rich/node.js.map +1 -0
- package/dist/rich/ports.d.ts +19 -0
- package/dist/rich/ports.d.ts.map +1 -0
- package/dist/rich/ports.js +52 -0
- package/dist/rich/ports.js.map +1 -0
- package/dist/rich/registry.d.ts +19 -0
- package/dist/rich/registry.d.ts.map +1 -0
- package/dist/rich/registry.js +35 -0
- package/dist/rich/registry.js.map +1 -0
- package/dist/rich/render.d.ts +19 -0
- package/dist/rich/render.d.ts.map +1 -0
- package/dist/rich/render.js +306 -0
- package/dist/rich/render.js.map +1 -0
- package/dist/rich/style.d.ts +126 -0
- package/dist/rich/style.d.ts.map +1 -0
- package/dist/rich/style.js +35 -0
- package/dist/rich/style.js.map +1 -0
- package/dist/rich/svg.d.ts +90 -0
- package/dist/rich/svg.d.ts.map +1 -0
- package/dist/rich/svg.js +415 -0
- package/dist/rich/svg.js.map +1 -0
- package/dist/spec.d.ts +2789 -0
- package/dist/spec.d.ts.map +1 -0
- package/dist/spec.js +239 -0
- package/dist/spec.js.map +1 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hit-test.js","sourceRoot":"","sources":["../../src/rich/hit-test.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG1C,MAAM,aAAa,GAAG,CACpB,CAA0D,EAC1D,CAAO,EACE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;AAExF;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,IAAkB,EAAE,KAAW,EAAuB,EAAE;IAClF,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACpD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClC,MAAM,GAAG,GAAG,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACtC,IAAI,GAAG;YAAE,OAAO,GAAG,CAAC;IACtB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,IAAkB,EAAE,KAAW,EAAuB,EAAE;IAC7F,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACrC,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,OAAO,uBAAuB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AAC5C,CAAC,CAAC;AAEF,MAAM,uBAAuB,GAAG,CAAC,IAAkB,EAAE,MAAoB,EAAuB,EAAE;IAChG,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,OAAO,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IAChD,CAAC;IACD,gEAAgE;IAChE,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACtC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,KAAK,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,IAAI,IAAI,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;IACpD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF,MAAM,SAAS,GAAG,CAAC,IAAkB,EAAE,MAAoB,EAAyB,EAAE;IACpF,IAAI,IAAI,KAAK,MAAM;QAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClC,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACvC,IAAI,KAAK;YAAE,OAAO,CAAC,IAAI,EAAE,GAAG,KAAK,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type { NodeStyle, Spacing, LinkSpacing, FlexDirection, JustifyContent, AlignItems, Position, Length, LayoutStyle, SpotName, SpotRef, } from "./style.js";
|
|
2
|
+
export { resolveSpacing, resolveSpotRatio } from "./style.js";
|
|
3
|
+
export type { Binding, NodeBase, ContainerNode, TextNode, IconNode, ImageNode, ButtonNode, DropZoneNode, PortNode, TemplateNode, } from "./node.js";
|
|
4
|
+
export { isContainer, isInteractive, childrenOf } from "./node.js";
|
|
5
|
+
export { extractPorts } from "./ports.js";
|
|
6
|
+
export { extractDropZone } from "./drop-zones.js";
|
|
7
|
+
export type { MeasureText, LayoutedNode, LayoutOptions } from "./layout.js";
|
|
8
|
+
export { layoutTree, fallbackMeasureText } from "./layout.js";
|
|
9
|
+
export { resolveBindings } from "./binding.js";
|
|
10
|
+
export { getTemplateLocalBounds } from "./bounds.js";
|
|
11
|
+
export type { RichTemplate, RichTemplateSchema } from "./define.js";
|
|
12
|
+
export { defineRichTemplate } from "./define.js";
|
|
13
|
+
export type { RichTemplateExtension } from "./extends.js";
|
|
14
|
+
export { resolveRichTemplateChain } from "./extends.js";
|
|
15
|
+
export { RichTemplateRegistry, defaultRichRegistry } from "./registry.js";
|
|
16
|
+
export { renderTemplateElement, installTemplateShapeRenderer } from "./render.js";
|
|
17
|
+
export { nodeAtPoint, interactiveNodeAtPoint } from "./hit-test.js";
|
|
18
|
+
export type { TemplateTapPayload, DropZoneHit } from "./interactive.js";
|
|
19
|
+
export { templateInteractiveHitTester, findDropZoneAt } from "./interactive.js";
|
|
20
|
+
export type { SvgIcon } from "./svg.js";
|
|
21
|
+
export { parseSvg, paintSvgIcon } from "./svg.js";
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rich/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,SAAS,EACT,OAAO,EACP,WAAW,EACX,aAAa,EACb,cAAc,EACd,UAAU,EACV,QAAQ,EACR,MAAM,EACN,WAAW,EACX,QAAQ,EACR,OAAO,GACR,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9D,YAAY,EACV,OAAO,EACP,QAAQ,EACR,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,YAAY,GACb,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEnE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAE9D,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAErD,YAAY,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAExD,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAE1E,OAAO,EAAE,qBAAqB,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAC;AAElF,OAAO,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAEpE,YAAY,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,4BAA4B,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEhF,YAAY,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { resolveSpacing, resolveSpotRatio } from "./style.js";
|
|
2
|
+
export { isContainer, isInteractive, childrenOf } from "./node.js";
|
|
3
|
+
export { extractPorts } from "./ports.js";
|
|
4
|
+
export { extractDropZone } from "./drop-zones.js";
|
|
5
|
+
export { layoutTree, fallbackMeasureText } from "./layout.js";
|
|
6
|
+
export { resolveBindings } from "./binding.js";
|
|
7
|
+
export { getTemplateLocalBounds } from "./bounds.js";
|
|
8
|
+
export { defineRichTemplate } from "./define.js";
|
|
9
|
+
export { resolveRichTemplateChain } from "./extends.js";
|
|
10
|
+
export { RichTemplateRegistry, defaultRichRegistry } from "./registry.js";
|
|
11
|
+
export { renderTemplateElement, installTemplateShapeRenderer } from "./render.js";
|
|
12
|
+
export { nodeAtPoint, interactiveNodeAtPoint } from "./hit-test.js";
|
|
13
|
+
export { templateInteractiveHitTester, findDropZoneAt } from "./interactive.js";
|
|
14
|
+
export { parseSvg, paintSvgIcon } from "./svg.js";
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/rich/index.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAc9D,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEnE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAE9D,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAGrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGjD,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAExD,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAE1E,OAAO,EAAE,qBAAqB,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAC;AAElF,OAAO,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAGpE,OAAO,EAAE,4BAA4B,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAGhF,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ElementBase } from "@oh-just-another/scene";
|
|
2
|
+
import type { ElementId, Vec2 } from "@oh-just-another/types";
|
|
3
|
+
/**
|
|
4
|
+
* Emit-shaped payload returned by `templateInteractiveHitTester`. Mirrors the
|
|
5
|
+
* `InteractionEmit` "TEMPLATE_TAP" variant from `@state` without taking a
|
|
6
|
+
* compile-time dependency on it — the host wires both sides together.
|
|
7
|
+
*/
|
|
8
|
+
export interface TemplateTapPayload {
|
|
9
|
+
readonly type: "TEMPLATE_TAP";
|
|
10
|
+
readonly elementId: ElementId;
|
|
11
|
+
readonly action: string;
|
|
12
|
+
readonly nodeId?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Host-side helper compatible with `@oh-just-another/state`'s
|
|
16
|
+
* `InteractiveHitTester` signature. Returns a `TEMPLATE_TAP` payload when
|
|
17
|
+
* `localPoint` lands on a `button` node inside the template tree, or `null`
|
|
18
|
+
* otherwise.
|
|
19
|
+
*
|
|
20
|
+
* Wire it up once during app startup:
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { registerInteractiveHitTester } from "@oh-just-another/state";
|
|
24
|
+
* import { rich } from "@oh-just-another/templates";
|
|
25
|
+
*
|
|
26
|
+
* registerInteractiveHitTester("template", rich.templateInteractiveHitTester);
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare const templateInteractiveHitTester: (shape: ElementBase, localPoint: Vec2) => TemplateTapPayload | null;
|
|
30
|
+
export interface DropZoneHit {
|
|
31
|
+
readonly elementId: ElementId;
|
|
32
|
+
readonly nodeId: string | undefined;
|
|
33
|
+
/** Whitelist from the drop-zone node. `undefined` = accept anything. */
|
|
34
|
+
readonly accepts: readonly string[] | undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Find the drop-zone node under `localPoint` for the given template shape.
|
|
38
|
+
* Returns `null` when the point misses every drop-zone (or the shape is not a
|
|
39
|
+
* template). The host uses this from DOM `dragover` / `drop` listeners to
|
|
40
|
+
* decide whether to allow the drop and to construct the `TEMPLATE_DROP`
|
|
41
|
+
* payload.
|
|
42
|
+
*/
|
|
43
|
+
export declare const findDropZoneAt: (shape: ElementBase, localPoint: Vec2) => DropZoneHit | null;
|
|
44
|
+
//# sourceMappingURL=interactive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interactive.d.ts","sourceRoot":"","sources":["../../src/rich/interactive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAA2C,MAAM,wBAAwB,CAAC;AACnG,OAAO,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAM9D;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,4BAA4B,GACvC,OAAO,WAAW,EAClB,YAAY,IAAI,KACf,kBAAkB,GAAG,IAwBvB,CAAC;AAEF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,wEAAwE;IACxE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;CACjD;AAED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,WAAW,EAAE,YAAY,IAAI,KAAG,WAAW,GAAG,IAqBnF,CAAC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { resolveBindings } from "./binding.js";
|
|
2
|
+
import { fallbackMeasureText, layoutTree } from "./layout.js";
|
|
3
|
+
import { defaultRichRegistry } from "./registry.js";
|
|
4
|
+
import { interactiveNodeAtPoint } from "./hit-test.js";
|
|
5
|
+
/**
|
|
6
|
+
* Host-side helper compatible with `@oh-just-another/state`'s
|
|
7
|
+
* `InteractiveHitTester` signature. Returns a `TEMPLATE_TAP` payload when
|
|
8
|
+
* `localPoint` lands on a `button` node inside the template tree, or `null`
|
|
9
|
+
* otherwise.
|
|
10
|
+
*
|
|
11
|
+
* Wire it up once during app startup:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { registerInteractiveHitTester } from "@oh-just-another/state";
|
|
15
|
+
* import { rich } from "@oh-just-another/templates";
|
|
16
|
+
*
|
|
17
|
+
* registerInteractiveHitTester("template", rich.templateInteractiveHitTester);
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export const templateInteractiveHitTester = (shape, localPoint) => {
|
|
21
|
+
if (shape.type !== "template")
|
|
22
|
+
return null;
|
|
23
|
+
const ts = shape;
|
|
24
|
+
const template = defaultRichRegistry.get(ts.templateId);
|
|
25
|
+
if (!template)
|
|
26
|
+
return null;
|
|
27
|
+
const root = resolveBindings(template.root, { ...template.defaults, ...ts.data });
|
|
28
|
+
// Same root-size override as the renderer — keeps hit-test coordinates in
|
|
29
|
+
// sync with what the user actually sees after a resize.
|
|
30
|
+
const rootWithSize = {
|
|
31
|
+
...root,
|
|
32
|
+
layout: { ...(root.layout ?? {}), width: ts.width, height: ts.height },
|
|
33
|
+
};
|
|
34
|
+
const layouted = layoutTree(rootWithSize, {
|
|
35
|
+
measureText: fallbackMeasureText,
|
|
36
|
+
available: { width: ts.width, height: ts.height },
|
|
37
|
+
});
|
|
38
|
+
const hit = interactiveNodeAtPoint(layouted, localPoint);
|
|
39
|
+
if (hit?.node.type !== "button")
|
|
40
|
+
return null;
|
|
41
|
+
return {
|
|
42
|
+
type: "TEMPLATE_TAP",
|
|
43
|
+
elementId: ts.id,
|
|
44
|
+
action: hit.node.action,
|
|
45
|
+
...(hit.node.id !== undefined ? { nodeId: hit.node.id } : {}),
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Find the drop-zone node under `localPoint` for the given template shape.
|
|
50
|
+
* Returns `null` when the point misses every drop-zone (or the shape is not a
|
|
51
|
+
* template). The host uses this from DOM `dragover` / `drop` listeners to
|
|
52
|
+
* decide whether to allow the drop and to construct the `TEMPLATE_DROP`
|
|
53
|
+
* payload.
|
|
54
|
+
*/
|
|
55
|
+
export const findDropZoneAt = (shape, localPoint) => {
|
|
56
|
+
if (shape.type !== "template")
|
|
57
|
+
return null;
|
|
58
|
+
const ts = shape;
|
|
59
|
+
const template = defaultRichRegistry.get(ts.templateId);
|
|
60
|
+
if (!template)
|
|
61
|
+
return null;
|
|
62
|
+
const root = resolveBindings(template.root, { ...template.defaults, ...ts.data });
|
|
63
|
+
const rootWithSize = {
|
|
64
|
+
...root,
|
|
65
|
+
layout: { ...(root.layout ?? {}), width: ts.width, height: ts.height },
|
|
66
|
+
};
|
|
67
|
+
const layouted = layoutTree(rootWithSize, {
|
|
68
|
+
measureText: fallbackMeasureText,
|
|
69
|
+
available: { width: ts.width, height: ts.height },
|
|
70
|
+
});
|
|
71
|
+
const hit = interactiveNodeAtPoint(layouted, localPoint);
|
|
72
|
+
if (hit?.node.type !== "drop-zone")
|
|
73
|
+
return null;
|
|
74
|
+
return {
|
|
75
|
+
elementId: ts.id,
|
|
76
|
+
nodeId: hit.node.id,
|
|
77
|
+
accepts: hit.node.accepts,
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=interactive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interactive.js","sourceRoot":"","sources":["../../src/rich/interactive.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Bounds } from "@oh-just-another/types";
|
|
2
|
+
import { type TemplateNode } from "./node.js";
|
|
3
|
+
/**
|
|
4
|
+
* `MeasureText(text, fontFamily, fontSize)` returns the rendered width of the
|
|
5
|
+
* given text run in CSS pixels. The layout engine relies on this for
|
|
6
|
+
* intrinsic sizing of `Text` and `Button` (label) nodes.
|
|
7
|
+
*
|
|
8
|
+
* The renderer passes a function backed by `CanvasRenderingContext2D.measureText`;
|
|
9
|
+
* tests can supply a deterministic mock.
|
|
10
|
+
*/
|
|
11
|
+
export type MeasureText = (text: string, fontFamily: string, fontSize: number) => number;
|
|
12
|
+
/** Best-effort defaults when the host doesn't supply a measurer. */
|
|
13
|
+
export declare const fallbackMeasureText: MeasureText;
|
|
14
|
+
/** A node augmented with its computed bounds in template-local coordinates. */
|
|
15
|
+
export interface LayoutedNode {
|
|
16
|
+
readonly node: TemplateNode;
|
|
17
|
+
/** Computed bounds in the template's local coordinate space. */
|
|
18
|
+
readonly bounds: Bounds;
|
|
19
|
+
readonly children: readonly LayoutedNode[];
|
|
20
|
+
}
|
|
21
|
+
export interface LayoutOptions {
|
|
22
|
+
readonly measureText?: MeasureText;
|
|
23
|
+
/** Maximum bounds the root may occupy. Defaults to (∞, ∞). */
|
|
24
|
+
readonly available?: {
|
|
25
|
+
readonly width: number;
|
|
26
|
+
readonly height: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Compute a layout for the entire tree. The root is positioned at (0, 0); the
|
|
31
|
+
* caller is responsible for translating it to the shape's world position when
|
|
32
|
+
* rendering.
|
|
33
|
+
*
|
|
34
|
+
* Algorithm:
|
|
35
|
+
* 1. Recursively measure intrinsic size for every node bottom-up.
|
|
36
|
+
* Containers ask their (in-flow) children for sizes then compute their
|
|
37
|
+
* own from `width/height` or by summing children + padding + gap.
|
|
38
|
+
* 2. Position children top-down using `flexDirection`, `justifyContent`,
|
|
39
|
+
* `alignItems`, `gap`, and `flex` grow factor.
|
|
40
|
+
* 3. Absolutely-positioned children are layouted independently inside the
|
|
41
|
+
* parent's padding box, using `top/left/right/bottom` against the parent.
|
|
42
|
+
*/
|
|
43
|
+
export declare const layoutTree: (root: TemplateNode, options?: LayoutOptions) => LayoutedNode;
|
|
44
|
+
//# sourceMappingURL=layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/rich/layout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAOL,KAAK,YAAY,EAElB,MAAM,WAAW,CAAC;AAenB;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;AAEzF,oEAAoE;AACpE,eAAO,MAAM,mBAAmB,EAAE,WACH,CAAC;AAMhC,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,gEAAgE;IAChE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;CAC5C;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;IACnC,8DAA8D;IAC9D,QAAQ,CAAC,SAAS,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1E;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,UAAU,GAAI,MAAM,YAAY,EAAE,UAAS,aAAkB,KAAG,YAe5E,CAAC"}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { isContainer, } from "./node.js";
|
|
2
|
+
import { resolveSpacing, resolveSpotRatio, } from "./style.js";
|
|
3
|
+
/** Index-access helper: asserts a provably-valid index is present. */
|
|
4
|
+
const req = (v) => {
|
|
5
|
+
if (v === undefined)
|
|
6
|
+
throw new Error("packages/templates: index out of range");
|
|
7
|
+
return v;
|
|
8
|
+
};
|
|
9
|
+
/** Best-effort defaults when the host doesn't supply a measurer. */
|
|
10
|
+
export const fallbackMeasureText = (text, _fontFamily, fontSize) => text.length * fontSize * 0.55;
|
|
11
|
+
const DEFAULT_FONT_FAMILY = "system-ui, sans-serif";
|
|
12
|
+
const DEFAULT_FONT_SIZE = 14;
|
|
13
|
+
const DEFAULT_LINE_HEIGHT = 1.2;
|
|
14
|
+
/**
|
|
15
|
+
* Compute a layout for the entire tree. The root is positioned at (0, 0); the
|
|
16
|
+
* caller is responsible for translating it to the shape's world position when
|
|
17
|
+
* rendering.
|
|
18
|
+
*
|
|
19
|
+
* Algorithm:
|
|
20
|
+
* 1. Recursively measure intrinsic size for every node bottom-up.
|
|
21
|
+
* Containers ask their (in-flow) children for sizes then compute their
|
|
22
|
+
* own from `width/height` or by summing children + padding + gap.
|
|
23
|
+
* 2. Position children top-down using `flexDirection`, `justifyContent`,
|
|
24
|
+
* `alignItems`, `gap`, and `flex` grow factor.
|
|
25
|
+
* 3. Absolutely-positioned children are layouted independently inside the
|
|
26
|
+
* parent's padding box, using `top/left/right/bottom` against the parent.
|
|
27
|
+
*/
|
|
28
|
+
export const layoutTree = (root, options = {}) => {
|
|
29
|
+
const measure = options.measureText ?? fallbackMeasureText;
|
|
30
|
+
const available = options.available ?? { width: Infinity, height: Infinity };
|
|
31
|
+
// Measure pass.
|
|
32
|
+
const intrinsic = measureNode(root, { width: available.width, height: available.height }, measure);
|
|
33
|
+
// Position pass: root occupies its intrinsic size at (0, 0).
|
|
34
|
+
return positionNode(root, { x: 0, y: 0, width: intrinsic.width, height: intrinsic.height }, measure);
|
|
35
|
+
};
|
|
36
|
+
const measureNode = (node, available, measure) => {
|
|
37
|
+
const layout = node.layout ?? {};
|
|
38
|
+
// Absolute sizing wins if both axes are given as numbers.
|
|
39
|
+
const fixedW = lengthToPx(layout.width, available.width);
|
|
40
|
+
const fixedH = lengthToPx(layout.height, available.height);
|
|
41
|
+
if (fixedW !== undefined && fixedH !== undefined) {
|
|
42
|
+
return clampSize({ width: fixedW, height: fixedH }, layout);
|
|
43
|
+
}
|
|
44
|
+
let intrinsic;
|
|
45
|
+
switch (node.type) {
|
|
46
|
+
case "container":
|
|
47
|
+
intrinsic = measureContainer(node, sizeMinus(available, marginOf(layout)), measure);
|
|
48
|
+
break;
|
|
49
|
+
case "text":
|
|
50
|
+
intrinsic = measureText(node, available, measure);
|
|
51
|
+
break;
|
|
52
|
+
case "icon":
|
|
53
|
+
case "image":
|
|
54
|
+
intrinsic = { width: 24, height: 24 };
|
|
55
|
+
break;
|
|
56
|
+
case "button":
|
|
57
|
+
intrinsic = measureButton(node, available, measure);
|
|
58
|
+
break;
|
|
59
|
+
case "drop-zone":
|
|
60
|
+
intrinsic = { width: 80, height: 60 };
|
|
61
|
+
break;
|
|
62
|
+
case "port":
|
|
63
|
+
// Ports are dimensionless — they collapse to a 0×0 point at their
|
|
64
|
+
// computed position. The host paints the port-dot overlay from
|
|
65
|
+
// `shape.anchors`, not from the template tree.
|
|
66
|
+
intrinsic = { width: 0, height: 0 };
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
return clampSize({
|
|
70
|
+
width: fixedW ?? intrinsic.width,
|
|
71
|
+
height: fixedH ?? intrinsic.height,
|
|
72
|
+
}, layout);
|
|
73
|
+
};
|
|
74
|
+
const measureContainer = (node, available, measure) => {
|
|
75
|
+
const layout = node.layout ?? {};
|
|
76
|
+
const padding = resolveSpacing(layout.padding);
|
|
77
|
+
const direction = layout.flexDirection ?? "row";
|
|
78
|
+
const gap = layout.gap ?? 0;
|
|
79
|
+
const inFlow = (node.children ?? []).filter((c) => (c.layout?.position ?? "relative") !== "absolute");
|
|
80
|
+
let mainContent = 0;
|
|
81
|
+
let crossContent = 0;
|
|
82
|
+
const innerAvail = {
|
|
83
|
+
width: available.width - padding.left - padding.right,
|
|
84
|
+
height: available.height - padding.top - padding.bottom,
|
|
85
|
+
};
|
|
86
|
+
for (let i = 0; i < inFlow.length; i++) {
|
|
87
|
+
const child = req(inFlow[i]);
|
|
88
|
+
const childMargin = marginOf(child.layout ?? {});
|
|
89
|
+
const childSize = measureNode(child, innerAvail, measure);
|
|
90
|
+
const childWithMarginMain = (direction === "row" ? childSize.width : childSize.height) +
|
|
91
|
+
(direction === "row"
|
|
92
|
+
? childMargin.left + childMargin.right
|
|
93
|
+
: childMargin.top + childMargin.bottom);
|
|
94
|
+
const childWithMarginCross = (direction === "row" ? childSize.height : childSize.width) +
|
|
95
|
+
(direction === "row"
|
|
96
|
+
? childMargin.top + childMargin.bottom
|
|
97
|
+
: childMargin.left + childMargin.right);
|
|
98
|
+
mainContent += childWithMarginMain;
|
|
99
|
+
if (i > 0)
|
|
100
|
+
mainContent += gap;
|
|
101
|
+
if (childWithMarginCross > crossContent)
|
|
102
|
+
crossContent = childWithMarginCross;
|
|
103
|
+
}
|
|
104
|
+
if (direction === "row") {
|
|
105
|
+
return {
|
|
106
|
+
width: mainContent + padding.left + padding.right,
|
|
107
|
+
height: crossContent + padding.top + padding.bottom,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
width: crossContent + padding.left + padding.right,
|
|
112
|
+
height: mainContent + padding.top + padding.bottom,
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
const measureText = (node, available, measure) => {
|
|
116
|
+
const text = typeof node.text === "string" ? node.text : "";
|
|
117
|
+
const fontFamily = node.style?.fontFamily ?? DEFAULT_FONT_FAMILY;
|
|
118
|
+
const fontSize = node.style?.fontSize ?? DEFAULT_FONT_SIZE;
|
|
119
|
+
const intrinsic = measure(text, fontFamily, fontSize);
|
|
120
|
+
// Cap by available width — the renderer will paint with ellipsis when the
|
|
121
|
+
// string doesn't fit. Without this cap a long label would push siblings
|
|
122
|
+
// outside the container.
|
|
123
|
+
return {
|
|
124
|
+
width: Math.min(intrinsic, available.width),
|
|
125
|
+
height: fontSize * DEFAULT_LINE_HEIGHT,
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
const measureButton = (node, available, measure) => {
|
|
129
|
+
const labelText = typeof node.label === "string" ? node.label : "";
|
|
130
|
+
const fontFamily = node.style?.fontFamily ?? DEFAULT_FONT_FAMILY;
|
|
131
|
+
const fontSize = node.style?.fontSize ?? DEFAULT_FONT_SIZE;
|
|
132
|
+
const padX = 10;
|
|
133
|
+
const padY = 6;
|
|
134
|
+
const labelW = measure(labelText, fontFamily, fontSize);
|
|
135
|
+
const intrinsicW = labelW + padX * 2;
|
|
136
|
+
return {
|
|
137
|
+
width: Math.min(intrinsicW, available.width),
|
|
138
|
+
height: fontSize * DEFAULT_LINE_HEIGHT + padY * 2,
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
// --- Position pass ---
|
|
142
|
+
const positionNode = (node, bounds, measure) => {
|
|
143
|
+
if (!isContainer(node)) {
|
|
144
|
+
return { node, bounds, children: [] };
|
|
145
|
+
}
|
|
146
|
+
const layout = node.layout ?? {};
|
|
147
|
+
const padding = resolveSpacing(layout.padding);
|
|
148
|
+
const direction = layout.flexDirection ?? "row";
|
|
149
|
+
const wrap = (layout.flexWrap ?? "nowrap") === "wrap";
|
|
150
|
+
const gap = layout.gap ?? 0;
|
|
151
|
+
const inner = {
|
|
152
|
+
x: bounds.x + padding.left,
|
|
153
|
+
y: bounds.y + padding.top,
|
|
154
|
+
width: Math.max(0, bounds.width - padding.left - padding.right),
|
|
155
|
+
height: Math.max(0, bounds.height - padding.top - padding.bottom),
|
|
156
|
+
};
|
|
157
|
+
const children = [];
|
|
158
|
+
const allChildren = node.children ?? [];
|
|
159
|
+
const positionOf = (c) => c.layout?.position ?? "relative";
|
|
160
|
+
const inFlow = allChildren.filter((c) => positionOf(c) === "relative");
|
|
161
|
+
const absolute = allChildren.filter((c) => positionOf(c) === "absolute");
|
|
162
|
+
const spot = allChildren.filter((c) => positionOf(c) === "spot");
|
|
163
|
+
const sized = inFlow.map((c) => ({
|
|
164
|
+
node: c,
|
|
165
|
+
baseSize: measureNode(c, { width: inner.width, height: inner.height }, measure),
|
|
166
|
+
flexGrow: c.layout?.flex ?? 0,
|
|
167
|
+
margin: marginOf(c.layout ?? {}),
|
|
168
|
+
}));
|
|
169
|
+
// Split children into lines. With `flexWrap: "wrap"`, push children onto a
|
|
170
|
+
// new line whenever the in-progress one would overflow along the main axis.
|
|
171
|
+
// Without wrapping (default) everything goes on a single line.
|
|
172
|
+
const lines = [];
|
|
173
|
+
if (!wrap || sized.length === 0) {
|
|
174
|
+
lines.push(sized);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const availMain = direction === "row" ? inner.width : inner.height;
|
|
178
|
+
let current = [];
|
|
179
|
+
let currentSize = 0;
|
|
180
|
+
for (const c of sized) {
|
|
181
|
+
const childMain = direction === "row"
|
|
182
|
+
? c.baseSize.width + c.margin.left + c.margin.right
|
|
183
|
+
: c.baseSize.height + c.margin.top + c.margin.bottom;
|
|
184
|
+
const projected = current.length === 0 ? childMain : currentSize + gap + childMain;
|
|
185
|
+
if (current.length > 0 && projected > availMain) {
|
|
186
|
+
lines.push(current);
|
|
187
|
+
current = [c];
|
|
188
|
+
currentSize = childMain;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
current.push(c);
|
|
192
|
+
currentSize = projected;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (current.length > 0)
|
|
196
|
+
lines.push(current);
|
|
197
|
+
}
|
|
198
|
+
// Compute per-line cross size (max child cross) plus the total cross size.
|
|
199
|
+
const lineCrossSizes = lines.map((line) => line.reduce((acc, c) => {
|
|
200
|
+
const cross = direction === "row"
|
|
201
|
+
? c.baseSize.height + c.margin.top + c.margin.bottom
|
|
202
|
+
: c.baseSize.width + c.margin.left + c.margin.right;
|
|
203
|
+
return Math.max(acc, cross);
|
|
204
|
+
}, 0));
|
|
205
|
+
const totalCross = lineCrossSizes.reduce((acc, h) => acc + h, 0) + Math.max(0, lines.length - 1) * gap;
|
|
206
|
+
// When wrap = true, lines are laid top-to-bottom inside `inner`. When wrap =
|
|
207
|
+
// false there's a single line that takes the full cross axis.
|
|
208
|
+
let lineCursorCross = direction === "row" ? inner.y : inner.x;
|
|
209
|
+
if (wrap && lines.length > 1) {
|
|
210
|
+
// Distribute extra cross space at the start (no align-content support).
|
|
211
|
+
void totalCross;
|
|
212
|
+
}
|
|
213
|
+
// Place each line individually.
|
|
214
|
+
for (let li = 0; li < lines.length; li++) {
|
|
215
|
+
const line = req(lines[li]);
|
|
216
|
+
const lineCross = wrap
|
|
217
|
+
? (lineCrossSizes[li] ?? 0)
|
|
218
|
+
: direction === "row"
|
|
219
|
+
? inner.height
|
|
220
|
+
: inner.width;
|
|
221
|
+
const lineBounds = direction === "row"
|
|
222
|
+
? { x: inner.x, y: lineCursorCross, width: inner.width, height: lineCross }
|
|
223
|
+
: { x: lineCursorCross, y: inner.y, width: lineCross, height: inner.height };
|
|
224
|
+
placeLine(node, line, lineBounds, direction, layout, measure, children);
|
|
225
|
+
lineCursorCross += lineCross + gap;
|
|
226
|
+
}
|
|
227
|
+
// Place absolute children inside `inner` using top/left/right/bottom.
|
|
228
|
+
for (const child of absolute) {
|
|
229
|
+
const cLayout = child.layout ?? {};
|
|
230
|
+
const childSize = measureNode(child, { width: inner.width, height: inner.height }, measure);
|
|
231
|
+
let x = inner.x;
|
|
232
|
+
let y = inner.y;
|
|
233
|
+
let width = childSize.width;
|
|
234
|
+
let height = childSize.height;
|
|
235
|
+
if (cLayout.left !== undefined)
|
|
236
|
+
x = inner.x + cLayout.left;
|
|
237
|
+
if (cLayout.top !== undefined)
|
|
238
|
+
y = inner.y + cLayout.top;
|
|
239
|
+
if (cLayout.right !== undefined) {
|
|
240
|
+
if (cLayout.left === undefined)
|
|
241
|
+
x = inner.x + inner.width - cLayout.right - width;
|
|
242
|
+
else
|
|
243
|
+
width = inner.width - cLayout.left - cLayout.right;
|
|
244
|
+
}
|
|
245
|
+
if (cLayout.bottom !== undefined) {
|
|
246
|
+
if (cLayout.top === undefined)
|
|
247
|
+
y = inner.y + inner.height - cLayout.bottom - height;
|
|
248
|
+
else
|
|
249
|
+
height = inner.height - cLayout.top - cLayout.bottom;
|
|
250
|
+
}
|
|
251
|
+
children.push(positionNode(child, { x, y, width, height }, measure));
|
|
252
|
+
}
|
|
253
|
+
// Spot-positioned children: pin the child's `anchorFocus` to the parent's
|
|
254
|
+
// `anchor`, then apply `offset`:
|
|
255
|
+
// parentAnchorPoint - childFocusOffset + offset = child top-left.
|
|
256
|
+
for (const child of spot) {
|
|
257
|
+
const cLayout = child.layout ?? {};
|
|
258
|
+
const childSize = measureNode(child, { width: inner.width, height: inner.height }, measure);
|
|
259
|
+
const parentAnchor = resolveSpotRatio(cLayout.anchor ?? "center");
|
|
260
|
+
const childFocus = resolveSpotRatio(cLayout.anchorFocus ?? "center");
|
|
261
|
+
const offset = cLayout.offset ?? { x: 0, y: 0 };
|
|
262
|
+
const x = inner.x + inner.width * parentAnchor.x - childSize.width * childFocus.x + offset.x;
|
|
263
|
+
const y = inner.y + inner.height * parentAnchor.y - childSize.height * childFocus.y + offset.y;
|
|
264
|
+
children.push(positionNode(child, { x, y, width: childSize.width, height: childSize.height }, measure));
|
|
265
|
+
}
|
|
266
|
+
return { node, bounds, children };
|
|
267
|
+
};
|
|
268
|
+
const placeLine = (parent, line, lineBounds, direction, parentLayout, measure, out) => {
|
|
269
|
+
void parent;
|
|
270
|
+
if (line.length === 0)
|
|
271
|
+
return;
|
|
272
|
+
const justify = parentLayout.justifyContent ?? "start";
|
|
273
|
+
const align = parentLayout.alignItems ?? "stretch";
|
|
274
|
+
const gap = parentLayout.gap ?? 0;
|
|
275
|
+
// Sum base size + gap to compute leftover along main axis for this line.
|
|
276
|
+
const availMain = direction === "row" ? lineBounds.width : lineBounds.height;
|
|
277
|
+
let usedMain = 0;
|
|
278
|
+
for (let i = 0; i < line.length; i++) {
|
|
279
|
+
const c = req(line[i]);
|
|
280
|
+
usedMain +=
|
|
281
|
+
direction === "row"
|
|
282
|
+
? c.baseSize.width + c.margin.left + c.margin.right
|
|
283
|
+
: c.baseSize.height + c.margin.top + c.margin.bottom;
|
|
284
|
+
if (i > 0)
|
|
285
|
+
usedMain += gap;
|
|
286
|
+
}
|
|
287
|
+
const leftover = Math.max(0, availMain - usedMain);
|
|
288
|
+
const totalFlex = line.reduce((acc, c) => acc + c.flexGrow, 0);
|
|
289
|
+
let mainCursor = direction === "row" ? lineBounds.x : lineBounds.y;
|
|
290
|
+
let gapBetween = gap;
|
|
291
|
+
if (totalFlex === 0) {
|
|
292
|
+
switch (justify) {
|
|
293
|
+
case "center":
|
|
294
|
+
mainCursor += leftover / 2;
|
|
295
|
+
break;
|
|
296
|
+
case "end":
|
|
297
|
+
mainCursor += leftover;
|
|
298
|
+
break;
|
|
299
|
+
case "space-between":
|
|
300
|
+
if (line.length > 1)
|
|
301
|
+
gapBetween = gap + leftover / (line.length - 1);
|
|
302
|
+
break;
|
|
303
|
+
case "space-around":
|
|
304
|
+
mainCursor += leftover / line.length / 2;
|
|
305
|
+
gapBetween = gap + leftover / line.length;
|
|
306
|
+
break;
|
|
307
|
+
case "start":
|
|
308
|
+
default:
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Baseline: for `row` lines, find the strongest text-baseline among the
|
|
313
|
+
// line's text/button children so all "baseline"-aligned siblings line up.
|
|
314
|
+
const lineBaseline = direction === "row" ? lineBaselineY(line, lineBounds.y) : null;
|
|
315
|
+
for (let i = 0; i < line.length; i++) {
|
|
316
|
+
const c = req(line[i]);
|
|
317
|
+
const grow = totalFlex > 0 ? (c.flexGrow / totalFlex) * leftover : 0;
|
|
318
|
+
const mainSize = (direction === "row" ? c.baseSize.width : c.baseSize.height) + grow;
|
|
319
|
+
const alignSelf = c.node.layout?.alignSelf ?? align;
|
|
320
|
+
const crossAvail = direction === "row" ? lineBounds.height : lineBounds.width;
|
|
321
|
+
const childMarginCrossStart = direction === "row" ? c.margin.top : c.margin.left;
|
|
322
|
+
const childMarginCrossEnd = direction === "row" ? c.margin.bottom : c.margin.right;
|
|
323
|
+
let crossSize;
|
|
324
|
+
if (alignSelf === "stretch") {
|
|
325
|
+
crossSize = crossAvail - childMarginCrossStart - childMarginCrossEnd;
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
crossSize = direction === "row" ? c.baseSize.height : c.baseSize.width;
|
|
329
|
+
}
|
|
330
|
+
let crossOffset = childMarginCrossStart;
|
|
331
|
+
if (alignSelf === "center") {
|
|
332
|
+
crossOffset = (crossAvail - crossSize - childMarginCrossStart - childMarginCrossEnd) / 2;
|
|
333
|
+
}
|
|
334
|
+
else if (alignSelf === "end") {
|
|
335
|
+
crossOffset = crossAvail - crossSize - childMarginCrossEnd;
|
|
336
|
+
}
|
|
337
|
+
else if (alignSelf === "baseline" && direction === "row" && lineBaseline !== null) {
|
|
338
|
+
// Place the node so its baseline matches the line baseline.
|
|
339
|
+
const childBaselineFromTop = nodeBaselineOffset(c.node);
|
|
340
|
+
const targetY = lineBaseline - childBaselineFromTop;
|
|
341
|
+
crossOffset = targetY - lineBounds.y;
|
|
342
|
+
}
|
|
343
|
+
const cursorStart = mainCursor + (direction === "row" ? c.margin.left : c.margin.top);
|
|
344
|
+
const childBounds = direction === "row"
|
|
345
|
+
? { x: cursorStart, y: lineBounds.y + crossOffset, width: mainSize, height: crossSize }
|
|
346
|
+
: { x: lineBounds.x + crossOffset, y: cursorStart, width: crossSize, height: mainSize };
|
|
347
|
+
out.push(positionNode(c.node, childBounds, measure));
|
|
348
|
+
mainCursor +=
|
|
349
|
+
mainSize +
|
|
350
|
+
(direction === "row" ? c.margin.left + c.margin.right : c.margin.top + c.margin.bottom);
|
|
351
|
+
if (i < line.length - 1)
|
|
352
|
+
mainCursor += gapBetween;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
/**
|
|
356
|
+
* Approximate font baseline offset from the node's top edge. Uses 0.8 ×
|
|
357
|
+
* font-size as a rough ascent — accurate enough for editor templates without
|
|
358
|
+
* pulling in real font-metric tables. Non-text nodes report their own
|
|
359
|
+
* bottom edge as the baseline (matches CSS for replaced elements).
|
|
360
|
+
*/
|
|
361
|
+
const nodeBaselineOffset = (node) => {
|
|
362
|
+
if (node.type === "text") {
|
|
363
|
+
const fontSize = node.style?.fontSize ?? DEFAULT_FONT_SIZE;
|
|
364
|
+
return fontSize * 0.8;
|
|
365
|
+
}
|
|
366
|
+
if (node.type === "button") {
|
|
367
|
+
const fontSize = node.style?.fontSize ?? DEFAULT_FONT_SIZE;
|
|
368
|
+
// Buttons render their label centered vertically — baseline ≈ vertical
|
|
369
|
+
// center + half-ascent.
|
|
370
|
+
return fontSize * 0.8 + 6; // mirrors paintButton's padY
|
|
371
|
+
}
|
|
372
|
+
return Infinity; // non-text: ignored when reducing the line baseline.
|
|
373
|
+
};
|
|
374
|
+
const lineBaselineY = (line, lineY) => {
|
|
375
|
+
let best = null;
|
|
376
|
+
for (const c of line) {
|
|
377
|
+
const offset = nodeBaselineOffset(c.node);
|
|
378
|
+
if (!Number.isFinite(offset))
|
|
379
|
+
continue;
|
|
380
|
+
const y = lineY + c.margin.top + offset;
|
|
381
|
+
if (best === null || y > best)
|
|
382
|
+
best = y;
|
|
383
|
+
}
|
|
384
|
+
return best;
|
|
385
|
+
};
|
|
386
|
+
// --- Helpers ---
|
|
387
|
+
const lengthToPx = (l, parent) => {
|
|
388
|
+
if (l === undefined || l === "auto")
|
|
389
|
+
return undefined;
|
|
390
|
+
if (typeof l === "number")
|
|
391
|
+
return l;
|
|
392
|
+
// `${number}%`
|
|
393
|
+
const pct = parseFloat(l);
|
|
394
|
+
if (Number.isFinite(pct) && Number.isFinite(parent))
|
|
395
|
+
return (pct / 100) * parent;
|
|
396
|
+
return undefined;
|
|
397
|
+
};
|
|
398
|
+
const clampSize = (size, layout) => {
|
|
399
|
+
let w = size.width;
|
|
400
|
+
let h = size.height;
|
|
401
|
+
if (layout.minWidth !== undefined && w < layout.minWidth)
|
|
402
|
+
w = layout.minWidth;
|
|
403
|
+
if (layout.maxWidth !== undefined && w > layout.maxWidth)
|
|
404
|
+
w = layout.maxWidth;
|
|
405
|
+
if (layout.minHeight !== undefined && h < layout.minHeight)
|
|
406
|
+
h = layout.minHeight;
|
|
407
|
+
if (layout.maxHeight !== undefined && h > layout.maxHeight)
|
|
408
|
+
h = layout.maxHeight;
|
|
409
|
+
return { width: w, height: h };
|
|
410
|
+
};
|
|
411
|
+
const marginOf = (layout) => resolveSpacing(layout.margin);
|
|
412
|
+
const sizeMinus = (s, margin) => ({
|
|
413
|
+
width: s.width - margin.left - margin.right,
|
|
414
|
+
height: s.height - margin.top - margin.bottom,
|
|
415
|
+
});
|
|
416
|
+
// Quiet the unused-var checker for renderer-side variants.
|
|
417
|
+
void null;
|
|
418
|
+
//# sourceMappingURL=layout.js.map
|