@wonderlandlabs-pixi-ux/box 1.2.6 → 1.2.7
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 +33 -264
- package/TODO.md +10 -0
- package/dist/BoxStore.d.ts +7 -0
- package/dist/BoxStore.js +54 -0
- package/dist/BoxStore.stories.d.ts +7 -0
- package/dist/BoxStore.stories.js +213 -0
- package/dist/ComputeAxis.d.ts +17 -0
- package/dist/ComputeAxis.js +202 -0
- package/dist/ComputeAxis.stories.d.ts +13 -0
- package/dist/ComputeAxis.stories.js +286 -0
- package/dist/_deprecated/BoxTree.d.ts +123 -0
- package/dist/_deprecated/BoxTree.helpers.d.ts +4 -0
- package/dist/_deprecated/BoxTree.helpers.js +166 -0
- package/dist/_deprecated/BoxTree.js +789 -0
- package/dist/_deprecated/BoxUx.d.ts +50 -0
- package/dist/_deprecated/BoxUx.js +360 -0
- package/dist/_deprecated/BoxUxBase.d.ts +24 -0
- package/dist/_deprecated/BoxUxBase.js +86 -0
- package/dist/_deprecated/BoxUxContextMap.d.ts +3 -0
- package/dist/_deprecated/BoxUxContextMap.js +10 -0
- package/dist/_deprecated/boxTreeRenderers.d.ts +4 -0
- package/dist/_deprecated/boxTreeRenderers.js +97 -0
- package/dist/_deprecated/constants.d.ts +86 -0
- package/dist/_deprecated/constants.js +131 -0
- package/dist/_deprecated/constants.ux.d.ts +11 -0
- package/dist/_deprecated/constants.ux.js +11 -0
- package/dist/_deprecated/enumUtils.d.ts +1 -0
- package/dist/_deprecated/enumUtils.js +7 -0
- package/dist/_deprecated/index.d.ts +12 -0
- package/dist/_deprecated/index.js +11 -0
- package/dist/_deprecated/pathUtils.d.ts +5 -0
- package/dist/_deprecated/pathUtils.js +31 -0
- package/dist/_deprecated/pixiEnvironment.d.ts +1 -0
- package/dist/_deprecated/pixiEnvironment.js +9 -0
- package/dist/_deprecated/sizeUtils.d.ts +13 -0
- package/dist/_deprecated/sizeUtils.js +39 -0
- package/dist/_deprecated/types.boxtree.d.ts +937 -0
- package/dist/_deprecated/types.boxtree.js +110 -0
- package/dist/_deprecated/types.d.ts +161 -0
- package/dist/_deprecated/types.js +66 -0
- package/dist/_deprecated/types.ux.d.ts +20 -0
- package/dist/_deprecated/types.ux.js +1 -0
- package/dist/_deprecated/utils.ux.d.ts +13 -0
- package/dist/_deprecated/utils.ux.js +61 -0
- package/dist/constants.d.ts +26 -81
- package/dist/constants.js +42 -126
- package/dist/helpers.d.ts +24 -0
- package/dist/helpers.js +116 -0
- package/dist/types.d.ts +199 -105
- package/dist/types.js +68 -53
- package/package.json +1 -1
- package/src/BoxStore.stories.ts +239 -0
- package/src/BoxStore.ts +69 -0
- package/src/ComputeAxis.stories.ts +319 -0
- package/src/ComputeAxis.ts +296 -0
- package/src/_deprecated/BoxTree.helpers.ts +216 -0
- package/src/{BoxTree.ts → _deprecated/BoxTree.ts} +201 -278
- package/src/{boxTreeRenderers.ts → _deprecated/boxTreeRenderers.ts} +2 -47
- package/src/_deprecated/constants.ts +153 -0
- package/src/{sizeUtils.ts → _deprecated/sizeUtils.ts} +9 -1
- package/src/_deprecated/types.ts +138 -0
- package/src/constants.ts +46 -144
- package/src/helpers.ts +162 -0
- package/src/types.ts +113 -74
- package/test/ComputeAxis.test.ts +168 -0
- package/test/helpers.test.ts +76 -0
- package/test.zip +0 -0
- package/tsconfig.json +2 -1
- package/test/BoxTree.test.ts +0 -958
- package/test/BoxUx.test.ts +0 -436
- package/test/boxTreeRenderers.test.ts +0 -80
- package/test/sizeUtils.test.ts +0 -132
- /package/src/{BoxUx.ts → _deprecated/BoxUx.ts} +0 -0
- /package/src/{BoxUxBase.ts → _deprecated/BoxUxBase.ts} +0 -0
- /package/src/{BoxUxContextMap.ts → _deprecated/BoxUxContextMap.ts} +0 -0
- /package/src/{constants.ux.ts → _deprecated/constants.ux.ts} +0 -0
- /package/src/{enumUtils.ts → _deprecated/enumUtils.ts} +0 -0
- /package/src/{index.ts → _deprecated/index.ts} +0 -0
- /package/src/{pathUtils.ts → _deprecated/pathUtils.ts} +0 -0
- /package/src/{pixiEnvironment.ts → _deprecated/pixiEnvironment.ts} +0 -0
- /package/src/{types.boxtree.ts → _deprecated/types.boxtree.ts} +0 -0
- /package/src/{types.ux.ts → _deprecated/types.ux.ts} +0 -0
- /package/src/{utils.ux.ts → _deprecated/utils.ux.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,284 +1,53 @@
|
|
|
1
1
|
# @wonderlandlabs-pixi-ux/box
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
This emulates FlexBox based layout. This module is a data-model of a flex layout system.
|
|
5
|
-
It was designed for Pixi; however, as an abstract DSL for visual trees it may have other uses.
|
|
3
|
+
`box` is a small parent-driven layout model for rectangular children.
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
The current implementation is centered around:
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
```
|
|
7
|
+
- [`BoxStore`](/Users/bingomanatee/Documents/repos/wonderlandlabs-pixi-ux/packages/box/src/BoxStore.ts)
|
|
8
|
+
- [`ComputeAxis`](/Users/bingomanatee/Documents/repos/wonderlandlabs-pixi-ux/packages/box/src/ComputeAxis.ts)
|
|
12
9
|
|
|
13
|
-
##
|
|
10
|
+
## Current Model
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from '@wonderlandlabs-pixi-ux/box';
|
|
12
|
+
- The parent owns alignment.
|
|
13
|
+
- Children provide dimensions.
|
|
14
|
+
- Child `x` / `y` are not part of the happy-path alignment flow.
|
|
15
|
+
- Width and height are computed from the parent container.
|
|
16
|
+
- All units are relative to the parent in the dimension they address.
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
id: 'root',
|
|
24
|
-
styleName: 'button',
|
|
25
|
-
globalVerb: [],
|
|
26
|
-
area: {
|
|
27
|
-
x: 60,
|
|
28
|
-
y: 50,
|
|
29
|
-
width: { mode: UNIT_BASIS.PX, value: 720 },
|
|
30
|
-
height: { mode: UNIT_BASIS.PX, value: 340 },
|
|
31
|
-
px: 's',
|
|
32
|
-
py: 's',
|
|
33
|
-
},
|
|
34
|
-
align: {
|
|
35
|
-
x: ALIGN.START,
|
|
36
|
-
y: ALIGN.START,
|
|
37
|
-
direction: 'column',
|
|
38
|
-
},
|
|
39
|
-
children: {
|
|
40
|
-
header: {
|
|
41
|
-
styleName: 'header',
|
|
42
|
-
area: {
|
|
43
|
-
x: 0,
|
|
44
|
-
y: 0,
|
|
45
|
-
width: { mode: UNIT_BASIS.PERCENT, value: 1 },
|
|
46
|
-
height: { mode: UNIT_BASIS.PX, value: 60 },
|
|
47
|
-
px: 's',
|
|
48
|
-
py: 's',
|
|
49
|
-
},
|
|
50
|
-
align: { x: 's', y: 's', direction: 'row' },
|
|
51
|
-
},
|
|
52
|
-
icon: {
|
|
53
|
-
styleName: 'icon',
|
|
54
|
-
modeVerb: ['hover'],
|
|
55
|
-
area: {
|
|
56
|
-
x: 0,
|
|
57
|
-
y: 0,
|
|
58
|
-
width: { mode: UNIT_BASIS.PX, value: 24 },
|
|
59
|
-
height: { mode: UNIT_BASIS.PX, value: 24 },
|
|
60
|
-
px: 's',
|
|
61
|
-
py: 's',
|
|
62
|
-
},
|
|
63
|
-
align: { x: 's', y: 's', direction: 'column' },
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
});
|
|
18
|
+
That means:
|
|
67
19
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
20
|
+
- width-like values resolve against the parent width
|
|
21
|
+
- height-like values resolve against the parent height
|
|
22
|
+
- percent values are always percentages of the parent dimension
|
|
23
|
+
- absolute pixel values are still interpreted within the parent container
|
|
72
24
|
|
|
73
|
-
##
|
|
25
|
+
## Flow
|
|
74
26
|
|
|
75
|
-
|
|
27
|
+
The current layout pass is:
|
|
76
28
|
|
|
77
|
-
|
|
78
|
-
|
|
29
|
+
1. Resolve the main-axis spans for the children.
|
|
30
|
+
2. Complete unresolved fractional spans.
|
|
31
|
+
3. Place children from the parent alignment and the resolved spans.
|
|
79
32
|
|
|
80
|
-
|
|
81
|
-
layout.assignUx((box) => new BoxUxPixi(box));
|
|
82
|
-
layout.render(); // calls ux.init() once, then ux.render()
|
|
83
|
-
```
|
|
33
|
+
`ComputeAxis` currently handles the green path for:
|
|
84
34
|
|
|
85
|
-
|
|
35
|
+
- absolute pixel dimensions
|
|
36
|
+
- percentage dimensions
|
|
37
|
+
- parent-owned start / center / end alignment
|
|
86
38
|
|
|
87
|
-
|
|
39
|
+
## Status
|
|
88
40
|
|
|
89
|
-
|
|
90
|
-
const layout = new BoxTree({
|
|
91
|
-
id: 'root',
|
|
92
|
-
area: { x: 0, y: 0, width: 200, height: 100 },
|
|
93
|
-
ux: (box) => new BoxUxPixi(box),
|
|
94
|
-
});
|
|
95
|
-
layout.styles = styles;
|
|
96
|
-
```
|
|
41
|
+
This package is mid-refactor.
|
|
97
42
|
|
|
98
|
-
|
|
43
|
+
The active architecture is the simplified `BoxStore` / `ComputeAxis` path in `src/`.
|
|
44
|
+
Older `BoxTree`-style code still exists under [`src/_deprecated`](/Users/bingomanatee/Documents/repos/wonderlandlabs-pixi-ux/packages/box/src/_deprecated), but it is not the current direction.
|
|
99
45
|
|
|
100
|
-
|
|
46
|
+
## Test Artifacts
|
|
101
47
|
|
|
102
|
-
|
|
103
|
-
- `modeVerb` is node-local state (`hover`, `selected`, ...)
|
|
104
|
-
- `globalVerb` is root-level state shared by all descendants (`disabled`, ...)
|
|
48
|
+
The `ComputeAxis` tests generate:
|
|
105
49
|
|
|
106
|
-
|
|
50
|
+
- SVG diagrams for layout inspection
|
|
51
|
+
- HTML tables for readable scenario metadata
|
|
107
52
|
|
|
108
|
-
|
|
109
|
-
2. Fallback to atomic noun (`icon`, `label`, ...)
|
|
110
|
-
|
|
111
|
-
Combined state list is:
|
|
112
|
-
|
|
113
|
-
- `globalVerb + modeVerb + extraStates`
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
import { fromJSON } from '@wonderlandlabs-pixi-ux/style-tree';
|
|
117
|
-
|
|
118
|
-
const styles = fromJSON({
|
|
119
|
-
button: {
|
|
120
|
-
icon: {
|
|
121
|
-
fill: {
|
|
122
|
-
$*: { color: { r: 0.2, g: 0.2, b: 0.2 }, alpha: 1 },
|
|
123
|
-
$disabled: { color: { r: 0.45, g: 0.45, b: 0.45 }, alpha: 1 },
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
icon: {
|
|
128
|
-
fill: {
|
|
129
|
-
$*: { color: { r: 1, g: 1, b: 1 }, alpha: 1 },
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
const icon = layout.getChild('icon');
|
|
135
|
-
const fillStyle = icon?.resolveStyle(styles, ['pressed']);
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
## Default Ux
|
|
139
|
-
|
|
140
|
-
`box` ships with `BoxUxPixi`, a default Pixi UX implementation for `BoxTree`.
|
|
141
|
-
|
|
142
|
-
Behavior:
|
|
143
|
-
|
|
144
|
-
- Creates a container when `container` is not provided
|
|
145
|
-
- Uses public `content: MapEnhanced` (a `Map<string, unknown>` subclass)
|
|
146
|
-
- `content.$box` points at the owning `BoxTree`
|
|
147
|
-
- Exposes `ux.getContainer(key): unknown` for renderer-to-renderer handoff:
|
|
148
|
-
- `ROOT_CONTAINER`, `BACKGROUND_CONTAINER`, `CHILD_CONTAINER`, `CONTENT_CONTAINER`, `OVERLAY_CONTAINER`, `STROKE_CONTAINER`
|
|
149
|
-
- Exposes `ux.attach(targetContainer?)`:
|
|
150
|
-
- attaches root container to `targetContainer`
|
|
151
|
-
- if omitted, uses `ux.app?.stage`
|
|
152
|
-
- Pre-populates built-in layers if absent:
|
|
153
|
-
- `BOX_RENDER_CONTENT_ORDER.BACKGROUND = 0`
|
|
154
|
-
- `BOX_RENDER_CONTENT_ORDER.CHILDREN = 50`
|
|
155
|
-
- `BOX_RENDER_CONTENT_ORDER.CONTENT = 75`
|
|
156
|
-
- `BOX_RENDER_CONTENT_ORDER.OVERLAY = 100`
|
|
157
|
-
- Supports named layer order lookups:
|
|
158
|
-
- `BOX_UX_ORDER` (`ReadonlyMap<string, number>`)
|
|
159
|
-
- `setUxOrder(name, zIndex)` with duplicate z-index protection
|
|
160
|
-
- `getUxOrder(name)` for safe lookup
|
|
161
|
-
- Rebuilds children layer each render by clearing and re-adding child UX containers
|
|
162
|
-
- Draws a background graphic from style props:
|
|
163
|
-
- `[stylePath].bgColor`
|
|
164
|
-
- `[stylePath].bgAlpha`
|
|
165
|
-
- `[stylePath].bgStrokeColor`
|
|
166
|
-
- `[stylePath].bgStrokeAlpha`
|
|
167
|
-
- `[stylePath].bgStrokeSize`
|
|
168
|
-
- Exposes `ux.generateStyleMap(box)` with normalized shape:
|
|
169
|
-
- `fill: { color, alpha }`
|
|
170
|
-
- `stroke: { color, alpha, width }`
|
|
171
|
-
- Honors `box.isVisible`:
|
|
172
|
-
- when `false`, detaches (hides) the container without destroying render content
|
|
173
|
-
- when `true` again, existing layers are reused on next render
|
|
174
|
-
- Iterates `content` and injects non-empty items into root container sorted by each item's `zIndex`
|
|
175
|
-
|
|
176
|
-
```ts
|
|
177
|
-
import { Graphics } from 'pixi.js';
|
|
178
|
-
import {
|
|
179
|
-
BOX_RENDER_CONTENT_ORDER,
|
|
180
|
-
BoxTree,
|
|
181
|
-
BoxUxPixi,
|
|
182
|
-
} from '@wonderlandlabs-pixi-ux/box';
|
|
183
|
-
import { fromJSON } from '@wonderlandlabs-pixi-ux/style-tree';
|
|
184
|
-
|
|
185
|
-
const styles = fromJSON({
|
|
186
|
-
button: {
|
|
187
|
-
bgColor: { $*: 0x2d3a45 },
|
|
188
|
-
bgStrokeColor: { $*: 0x8fd3ff },
|
|
189
|
-
bgStrokeSize: { $*: 2 },
|
|
190
|
-
icon: {
|
|
191
|
-
bgColor: { $*: 0x3a4957 },
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
icon: {
|
|
195
|
-
bgColor: { $*: 0x222222 },
|
|
196
|
-
},
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
const root = new BoxTree({
|
|
200
|
-
id: 'root',
|
|
201
|
-
styleName: 'button',
|
|
202
|
-
area: { x: 40, y: 30, width: 200, height: 100 },
|
|
203
|
-
children: {
|
|
204
|
-
icon: {
|
|
205
|
-
styleName: 'icon',
|
|
206
|
-
order: 0,
|
|
207
|
-
area: { width: 24, height: 24 },
|
|
208
|
-
},
|
|
209
|
-
},
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
root.styles = styles;
|
|
213
|
-
const ux = new BoxUxPixi(root);
|
|
214
|
-
root.render();
|
|
215
|
-
// manual mount
|
|
216
|
-
ux.attach(app.stage);
|
|
217
|
-
|
|
218
|
-
// custom content layer example:
|
|
219
|
-
const custom = new Graphics();
|
|
220
|
-
custom.zIndex = 76;
|
|
221
|
-
custom.visible = true;
|
|
222
|
-
ux.content.set('CUSTOM', custom);
|
|
223
|
-
ux.content.get('OVERLAY')?.visible = true;
|
|
224
|
-
const ownerBox = ux.content.$box;
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
## Override Points
|
|
228
|
-
|
|
229
|
-
`BoxUxBase` is the renderer-agnostic lifecycle base (`init`, `render`, `clear`, visible up/down flow).
|
|
230
|
-
`BoxUxPixi` extends `BoxUxBase` and provides the Pixi-specific containers/graphics behavior.
|
|
231
|
-
|
|
232
|
-
For custom behavior, return your own UX instance from `assignUx((box) => ...)`, either:
|
|
233
|
-
|
|
234
|
-
- extending `BoxUxBase` for a new renderer, or
|
|
235
|
-
- extending `BoxUxPixi` for Pixi-specific customization.
|
|
236
|
-
|
|
237
|
-
Detailed style/composition docs:
|
|
238
|
-
|
|
239
|
-
- [`README.STYLES.md`](./README.STYLES.md)
|
|
240
|
-
|
|
241
|
-
## Optional Pixi Debug Rendering
|
|
242
|
-
|
|
243
|
-
If you just want geometry previews, you can use `boxTreeToPixi`:
|
|
244
|
-
|
|
245
|
-
```ts
|
|
246
|
-
import { boxTreeToPixi } from '@wonderlandlabs-pixi-ux/box';
|
|
247
|
-
|
|
248
|
-
const graphics = await boxTreeToPixi(layout, {
|
|
249
|
-
includeRoot: true,
|
|
250
|
-
fill: 0x2d3a45,
|
|
251
|
-
fillAlpha: 0.35,
|
|
252
|
-
stroke: 0x8fd3ff,
|
|
253
|
-
strokeAlpha: 0.9,
|
|
254
|
-
strokeWidth: 2,
|
|
255
|
-
});
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
## Public API
|
|
259
|
-
|
|
260
|
-
- `BoxTree`
|
|
261
|
-
- `BoxUx` (UX object type)
|
|
262
|
-
- `BoxUxMapFn`
|
|
263
|
-
- `BoxRenderer` (legacy alias of `BoxUx`)
|
|
264
|
-
- `BoxRenderMapFn` (legacy alias of `BoxUxMapFn`)
|
|
265
|
-
- `MapEnhanced`
|
|
266
|
-
- `BoxUxBase`
|
|
267
|
-
- `BoxUxPixi`
|
|
268
|
-
- `BoxTreeRenderer` (legacy alias of `BoxUxPixi`)
|
|
269
|
-
- `BOX_UX_ORDER`, `getUxOrder`, `setUxOrder`
|
|
270
|
-
- `BOX_RENDER_CONTENT_ORDER`
|
|
271
|
-
- `createBoxTreeState`
|
|
272
|
-
- `resolveTreeMeasurement`
|
|
273
|
-
- `resolveMeasurementPx`
|
|
274
|
-
- `resolveConstraintValuePx`
|
|
275
|
-
- `applyAxisConstraints`
|
|
276
|
-
- `boxTreeToPixi`
|
|
277
|
-
- `boxTreeToSvg`
|
|
278
|
-
- `pathToString`, `pathString`, `combinePaths`
|
|
279
|
-
- `ALIGN`, `AXIS`, `UNIT_BASIS`, `SIZE_MODE`, `SIZE_MODE_INPUT`
|
|
280
|
-
- `TreeStyleNameSchema`, `TreeVerbSchema`, `TreeVerbListSchema`
|
|
281
|
-
|
|
282
|
-
## Data Model
|
|
283
|
-
|
|
284
|
-
Use exported BoxTree schemas/types in `src/types.boxtree.ts` and measurement schemas in `src/types.ts` as the source of truth.
|
|
53
|
+
These artifacts are written under [`packages/box/test/artifacts`](/Users/bingomanatee/Documents/repos/wonderlandlabs-pixi-ux/packages/box/test/artifacts) and are ignored by the package-level `.gitignore`.
|
package/TODO.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# TODO
|
|
2
|
+
|
|
3
|
+
- Achieve recursive layout so each child can compute and apply layout to its own children.
|
|
4
|
+
- use Immer/diff to only recompute the children that have changed or the children of changed nodes.
|
|
5
|
+
- Properly compute fractional dimensions on the main axis.
|
|
6
|
+
- Replace the current no-op fractional completion step with remainder distribution across unresolved spans.
|
|
7
|
+
- Decide how overflow should resolve when content exceeds the parent container:
|
|
8
|
+
squash content to fit, crop content to the container, or allow overflow without intervention.
|
|
9
|
+
- Consider adding min/max sizing constraints, especially if fractional sizing makes alignment too inert.
|
|
10
|
+
- Keep the parent-owned alignment model strict: children define dimensions, parents place them.
|
package/dist/BoxStore.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Forest } from '@wonderlandlabs/forestry4';
|
|
2
|
+
import { rectToAbsolute } from "./helpers.js";
|
|
3
|
+
import { ComputeAxis } from './ComputeAxis.js';
|
|
4
|
+
export class BoxStore extends Forest {
|
|
5
|
+
update() {
|
|
6
|
+
this.alignChildren();
|
|
7
|
+
}
|
|
8
|
+
get location() {
|
|
9
|
+
const { dim, location } = this.value;
|
|
10
|
+
return rectToAbsolute(dim ?? location);
|
|
11
|
+
}
|
|
12
|
+
alignChildren() {
|
|
13
|
+
const { children, align } = this.value;
|
|
14
|
+
if (!Array.isArray(children) || children.length === 0) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const myContainer = this.location;
|
|
18
|
+
if (!myContainer) {
|
|
19
|
+
throw new Error('cannot align children: store location not set');
|
|
20
|
+
}
|
|
21
|
+
const childLocations = new ComputeAxis(align, myContainer, children.map((child) => child.dim)).compute();
|
|
22
|
+
this.mutate((draft) => {
|
|
23
|
+
if (!Array.isArray(draft.children)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
draft.children = draft.children.map((child, index) => {
|
|
27
|
+
const newChild = {
|
|
28
|
+
...child,
|
|
29
|
+
location: childLocations[index],
|
|
30
|
+
};
|
|
31
|
+
if (Array.isArray(newChild.children) && newChild.children.length > 0) {
|
|
32
|
+
updateChildren(newChild);
|
|
33
|
+
}
|
|
34
|
+
return newChild;
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function updateChildren(cell) {
|
|
40
|
+
const { location: myContainer, children, align } = cell;
|
|
41
|
+
if (!Array.isArray(children) || children.length === 0 || !myContainer)
|
|
42
|
+
return;
|
|
43
|
+
const childLocations = new ComputeAxis(align, myContainer, children.map((child) => child.dim)).compute();
|
|
44
|
+
cell.children = children.map((child, index) => {
|
|
45
|
+
const newChild = {
|
|
46
|
+
...child,
|
|
47
|
+
location: childLocations[index],
|
|
48
|
+
};
|
|
49
|
+
if (Array.isArray(newChild.children) && newChild.children.length > 0) {
|
|
50
|
+
updateChildren(newChild);
|
|
51
|
+
}
|
|
52
|
+
return newChild;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/html';
|
|
2
|
+
declare const meta: Meta;
|
|
3
|
+
export default meta;
|
|
4
|
+
type Story = StoryObj;
|
|
5
|
+
export declare const BasicNestedLayout: Story;
|
|
6
|
+
export declare const ComplexFractionalNesting: Story;
|
|
7
|
+
export declare const DeeplyNestedLayout: Story;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { BoxStore } from './BoxStore.js';
|
|
2
|
+
import { DIR_HORIZ, DIR_VERT, POS_CENTER, POS_END, POS_FILL, POS_START, SIZE_FRACTION, SIZE_PCT, } from './constants.js';
|
|
3
|
+
const meta = {
|
|
4
|
+
title: 'Box/BoxStore',
|
|
5
|
+
};
|
|
6
|
+
export default meta;
|
|
7
|
+
function escapeHtml(input) {
|
|
8
|
+
return input
|
|
9
|
+
.replace(/&/g, '&')
|
|
10
|
+
.replace(/</g, '<')
|
|
11
|
+
.replace(/>/g, '>')
|
|
12
|
+
.replace(/"/g, '"')
|
|
13
|
+
.replace(/'/g, ''');
|
|
14
|
+
}
|
|
15
|
+
function renderCellSvg(cell, depth = 0) {
|
|
16
|
+
const location = cell.location;
|
|
17
|
+
if (!location)
|
|
18
|
+
return '';
|
|
19
|
+
const colors = [
|
|
20
|
+
['#d6e4ff', '#1b263b'], // level 0
|
|
21
|
+
['#fde2e4', '#5e0b15'], // level 1
|
|
22
|
+
['#d8f3dc', '#081c15'], // level 2
|
|
23
|
+
['#fff1c1', '#432818'], // level 3
|
|
24
|
+
['#e0e1dd', '#0d1b2a'], // level 4
|
|
25
|
+
];
|
|
26
|
+
const [fill, text] = colors[depth % colors.length];
|
|
27
|
+
const rect = `<rect x="${location.x}" y="${location.y}" width="${location.w}" height="${location.h}" fill="${fill}" stroke="${text}" stroke-width="${Math.max(0.5, 2 - depth * 0.5)}" rx="${Math.max(0, 4 - depth)}" fill-opacity="0.8" />`;
|
|
28
|
+
const label = `<text x="${location.x + 4}" y="${location.y + 12}" font-family="monospace" font-size="${Math.max(8, 12 - depth * 2)}" fill="${text}">${escapeHtml(cell.name)}</text>`;
|
|
29
|
+
const childrenSvg = cell.children?.map(child => renderCellSvg(child, depth + 1)).join('') || '';
|
|
30
|
+
return rect + label + childrenSvg;
|
|
31
|
+
}
|
|
32
|
+
function renderStoreSvg(store) {
|
|
33
|
+
const root = store.value;
|
|
34
|
+
const location = store.location;
|
|
35
|
+
const padding = 20;
|
|
36
|
+
const diagramWidth = location.w + location.x + padding;
|
|
37
|
+
const diagramHeight = location.h + location.y + padding;
|
|
38
|
+
return [
|
|
39
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${diagramWidth}" height="${diagramHeight}" viewBox="0 0 ${diagramWidth} ${diagramHeight}">`,
|
|
40
|
+
`<rect x="0" y="0" width="${diagramWidth}" height="${diagramHeight}" fill="#f8f9fa" />`,
|
|
41
|
+
renderCellSvg({ ...root, location }),
|
|
42
|
+
`</svg>`,
|
|
43
|
+
].join('');
|
|
44
|
+
}
|
|
45
|
+
function renderStoreStory(rootCell) {
|
|
46
|
+
const store = new BoxStore({ value: rootCell });
|
|
47
|
+
store.update();
|
|
48
|
+
const wrapper = document.createElement('div');
|
|
49
|
+
wrapper.innerHTML = `
|
|
50
|
+
<style>
|
|
51
|
+
.store-story {
|
|
52
|
+
padding: 20px;
|
|
53
|
+
font-family: ui-sans-serif, system-ui, sans-serif;
|
|
54
|
+
}
|
|
55
|
+
.canvas-container {
|
|
56
|
+
background: white;
|
|
57
|
+
border: 1px solid #ddd;
|
|
58
|
+
border-radius: 8px;
|
|
59
|
+
overflow: auto;
|
|
60
|
+
margin-bottom: 20px;
|
|
61
|
+
}
|
|
62
|
+
pre {
|
|
63
|
+
background: #272822;
|
|
64
|
+
color: #f8f8f2;
|
|
65
|
+
padding: 15px;
|
|
66
|
+
border-radius: 8px;
|
|
67
|
+
font-size: 12px;
|
|
68
|
+
overflow: auto;
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
71
|
+
<div class="store-story">
|
|
72
|
+
<h2>BoxStore Layout</h2>
|
|
73
|
+
<div class="canvas-container">
|
|
74
|
+
${renderStoreSvg(store)}
|
|
75
|
+
</div>
|
|
76
|
+
<h3>Store Configuration</h3>
|
|
77
|
+
<pre>${escapeHtml(JSON.stringify(rootCell, null, 2))}</pre>
|
|
78
|
+
</div>
|
|
79
|
+
`;
|
|
80
|
+
return wrapper;
|
|
81
|
+
}
|
|
82
|
+
export const BasicNestedLayout = {
|
|
83
|
+
render: () => renderStoreStory({
|
|
84
|
+
name: 'root',
|
|
85
|
+
absolute: true,
|
|
86
|
+
dim: { x: 10, y: 10, w: 500, h: 400 },
|
|
87
|
+
align: { direction: DIR_HORIZ, xPosition: POS_START, yPosition: POS_START },
|
|
88
|
+
children: [
|
|
89
|
+
{
|
|
90
|
+
name: 'sidebar',
|
|
91
|
+
absolute: false,
|
|
92
|
+
dim: { w: 100, h: { value: 100, unit: SIZE_PCT } },
|
|
93
|
+
align: { direction: DIR_VERT, xPosition: POS_CENTER, yPosition: POS_START },
|
|
94
|
+
children: [
|
|
95
|
+
{ name: 'logo', absolute: false, dim: { w: 60, h: 60 }, align: { direction: DIR_HORIZ } },
|
|
96
|
+
{ name: 'nav1', absolute: false, dim: { w: 80, h: 30 }, align: { direction: DIR_HORIZ } },
|
|
97
|
+
{ name: 'nav2', absolute: false, dim: { w: 80, h: 30 }, align: { direction: DIR_HORIZ } },
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'content',
|
|
102
|
+
absolute: false,
|
|
103
|
+
dim: { w: { value: 1, unit: SIZE_FRACTION }, h: { value: 100, unit: SIZE_PCT } },
|
|
104
|
+
align: { direction: DIR_VERT, xPosition: POS_START, yPosition: POS_START },
|
|
105
|
+
children: [
|
|
106
|
+
{
|
|
107
|
+
name: 'header',
|
|
108
|
+
absolute: false,
|
|
109
|
+
dim: { w: { value: 100, unit: SIZE_PCT }, h: 60 },
|
|
110
|
+
align: { direction: DIR_HORIZ, xPosition: POS_END, yPosition: POS_CENTER },
|
|
111
|
+
children: [
|
|
112
|
+
{ name: 'user', absolute: false, dim: { w: 40, h: 40 }, align: { direction: DIR_HORIZ } },
|
|
113
|
+
{ name: 'logout', absolute: false, dim: { w: 60, h: 30 }, align: { direction: DIR_HORIZ } },
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'body',
|
|
118
|
+
absolute: false,
|
|
119
|
+
dim: { w: { value: 100, unit: SIZE_PCT }, h: { value: 1, unit: SIZE_FRACTION } },
|
|
120
|
+
align: { direction: DIR_HORIZ, xPosition: POS_FILL, yPosition: POS_FILL },
|
|
121
|
+
children: [
|
|
122
|
+
{ name: 'card1', absolute: false, dim: { w: { value: 1, unit: SIZE_FRACTION }, h: { value: 100, unit: SIZE_PCT } }, align: { direction: DIR_HORIZ } },
|
|
123
|
+
{ name: 'card2', absolute: false, dim: { w: { value: 1, unit: SIZE_FRACTION }, h: { value: 100, unit: SIZE_PCT } }, align: { direction: DIR_HORIZ } },
|
|
124
|
+
{ name: 'card3', absolute: false, dim: { w: { value: 1, unit: SIZE_FRACTION }, h: { value: 100, unit: SIZE_PCT } }, align: { direction: DIR_HORIZ } },
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
})
|
|
131
|
+
};
|
|
132
|
+
export const ComplexFractionalNesting = {
|
|
133
|
+
render: () => renderStoreStory({
|
|
134
|
+
name: 'grid',
|
|
135
|
+
absolute: true,
|
|
136
|
+
dim: { x: 10, y: 10, w: 600, h: 600 },
|
|
137
|
+
align: { direction: DIR_VERT, xPosition: POS_FILL, yPosition: POS_FILL },
|
|
138
|
+
children: [
|
|
139
|
+
{
|
|
140
|
+
name: 'row1',
|
|
141
|
+
absolute: false,
|
|
142
|
+
dim: { w: { value: 100, unit: SIZE_PCT }, h: { value: 1, unit: SIZE_FRACTION } },
|
|
143
|
+
align: { direction: DIR_HORIZ, xPosition: POS_FILL, yPosition: POS_FILL },
|
|
144
|
+
children: [
|
|
145
|
+
{ name: 'r1c1', absolute: false, dim: { w: { value: 1, unit: SIZE_FRACTION }, h: { value: 100, unit: SIZE_PCT } }, align: { direction: DIR_HORIZ } },
|
|
146
|
+
{ name: 'r1c2', absolute: false, dim: { w: { value: 2, unit: SIZE_FRACTION }, h: { value: 100, unit: SIZE_PCT } }, align: { direction: DIR_HORIZ } },
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: 'row2',
|
|
151
|
+
absolute: false,
|
|
152
|
+
dim: { w: { value: 100, unit: SIZE_PCT }, h: { value: 2, unit: SIZE_FRACTION } },
|
|
153
|
+
align: { direction: DIR_HORIZ, xPosition: POS_FILL, yPosition: POS_FILL },
|
|
154
|
+
children: [
|
|
155
|
+
{ name: 'r2c1', absolute: false, dim: { w: { value: 1, unit: SIZE_FRACTION }, h: { value: 100, unit: SIZE_PCT } }, align: { direction: DIR_HORIZ } },
|
|
156
|
+
{
|
|
157
|
+
name: 'r2c2-nested',
|
|
158
|
+
absolute: false,
|
|
159
|
+
dim: { w: { value: 1, unit: SIZE_FRACTION }, h: { value: 100, unit: SIZE_PCT } },
|
|
160
|
+
align: { direction: DIR_VERT, xPosition: POS_FILL, yPosition: POS_FILL },
|
|
161
|
+
children: [
|
|
162
|
+
{ name: 'inner1', absolute: false, dim: { w: { value: 100, unit: SIZE_PCT }, h: { value: 1, unit: SIZE_FRACTION } }, align: { direction: DIR_HORIZ } },
|
|
163
|
+
{ name: 'inner2', absolute: false, dim: { w: { value: 100, unit: SIZE_PCT }, h: { value: 1, unit: SIZE_FRACTION } }, align: { direction: DIR_HORIZ } },
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
})
|
|
170
|
+
};
|
|
171
|
+
export const DeeplyNestedLayout = {
|
|
172
|
+
render: () => renderStoreStory({
|
|
173
|
+
name: 'root',
|
|
174
|
+
absolute: true,
|
|
175
|
+
dim: { x: 10, y: 10, w: 400, h: 400 },
|
|
176
|
+
align: { direction: DIR_VERT, xPosition: POS_FILL, yPosition: POS_FILL },
|
|
177
|
+
children: [
|
|
178
|
+
{
|
|
179
|
+
name: 'L1',
|
|
180
|
+
absolute: false,
|
|
181
|
+
dim: { w: { value: 100, unit: SIZE_PCT }, h: { value: 1, unit: SIZE_FRACTION } },
|
|
182
|
+
align: { direction: DIR_HORIZ, xPosition: POS_FILL, yPosition: POS_FILL },
|
|
183
|
+
children: [
|
|
184
|
+
{
|
|
185
|
+
name: 'L2',
|
|
186
|
+
absolute: false,
|
|
187
|
+
dim: { w: { value: 1, unit: SIZE_FRACTION }, h: { value: 100, unit: SIZE_PCT } },
|
|
188
|
+
align: { direction: DIR_VERT, xPosition: POS_FILL, yPosition: POS_FILL },
|
|
189
|
+
children: [
|
|
190
|
+
{
|
|
191
|
+
name: 'L3',
|
|
192
|
+
absolute: false,
|
|
193
|
+
dim: { w: { value: 100, unit: SIZE_PCT }, h: { value: 1, unit: SIZE_FRACTION } },
|
|
194
|
+
align: { direction: DIR_HORIZ, xPosition: POS_FILL, yPosition: POS_FILL },
|
|
195
|
+
children: [
|
|
196
|
+
{
|
|
197
|
+
name: 'L4',
|
|
198
|
+
absolute: false,
|
|
199
|
+
dim: { w: { value: 1, unit: SIZE_FRACTION }, h: { value: 100, unit: SIZE_PCT } },
|
|
200
|
+
align: { direction: DIR_HORIZ, xPosition: POS_CENTER, yPosition: POS_CENTER },
|
|
201
|
+
children: [
|
|
202
|
+
{ name: 'Core', absolute: false, dim: { w: 20, h: 20 }, align: { direction: DIR_HORIZ } }
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
]
|
|
212
|
+
})
|
|
213
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type BoxAlignType, type BoxSizeObjType, type RectPartialType, type RectPXType } from './types.js';
|
|
2
|
+
export declare class ComputeAxis {
|
|
3
|
+
#private;
|
|
4
|
+
readonly align: BoxAlignType;
|
|
5
|
+
readonly parent: RectPXType;
|
|
6
|
+
readonly dims: RectPartialType[];
|
|
7
|
+
widths: Array<number | BoxSizeObjType>;
|
|
8
|
+
heights: Array<number | BoxSizeObjType>;
|
|
9
|
+
xPositions: number[];
|
|
10
|
+
yPositions: number[];
|
|
11
|
+
mainAxisResolvedSpanTotal: number;
|
|
12
|
+
effectiveMainAlignment: string;
|
|
13
|
+
effectiveCrossAlignment: string;
|
|
14
|
+
constructor(align: BoxAlignType, parent: RectPXType, dims: RectPartialType[]);
|
|
15
|
+
compute(): RectPXType[];
|
|
16
|
+
get mainAxisRemainder(): number;
|
|
17
|
+
}
|