machinalayout 0.2.0 → 0.3.1

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.
Files changed (37) hide show
  1. package/README.md +15 -0
  2. package/dist/MachinaReactView-Bau5bErA.d.ts +41 -0
  3. package/dist/chunk-2ZQ2RFFI.js +400 -0
  4. package/dist/chunk-33CKBEJH.js +186 -0
  5. package/dist/{chunk-HU6XYOH7.js → chunk-MSTBFD7H.js} +106 -17
  6. package/dist/{chunk-TR24ERZT.js → chunk-SVWYWI7I.js} +3 -10
  7. package/dist/chunk-VREK57S3.js +13 -0
  8. package/dist/chunk-ZIGW44D5.js +63 -0
  9. package/dist/debugOverlay-fWLv1cS7.d.ts +36 -0
  10. package/dist/deus/index.d.ts +15 -0
  11. package/dist/deus/index.js +26 -0
  12. package/dist/handoff/index.d.ts +44 -0
  13. package/dist/handoff/index.js +83 -0
  14. package/dist/index.d.ts +47 -5
  15. package/dist/index.js +169 -3
  16. package/dist/inspect/index.d.ts +8 -0
  17. package/dist/inspect/index.js +97 -0
  18. package/dist/react/index.d.ts +6 -33
  19. package/dist/react/index.js +9 -3
  20. package/dist/react-native/index.d.ts +3 -1
  21. package/dist/react-native/index.js +8 -2
  22. package/dist/screenCatalog-ZjonGiOi.d.ts +46 -0
  23. package/dist/types-CWaup8Z6.d.ts +92 -0
  24. package/dist/{types-BudfpzZX.d.ts → types-CYgsjDai.d.ts} +1 -1
  25. package/dist/types-bJlg6wno.d.ts +32 -0
  26. package/dist/useDeusMachine-2w2u_dki.d.ts +14 -0
  27. package/dist/vue/index.d.ts +15 -3
  28. package/dist/vue/index.js +39 -4
  29. package/docs/deusmachina.md +191 -0
  30. package/docs/error-codes.md +11 -0
  31. package/docs/inspection-and-handoff.md +126 -0
  32. package/docs/react-adapter.md +27 -0
  33. package/docs/react-native-adapter.md +4 -0
  34. package/docs/screen-catalog-and-viewports.md +124 -0
  35. package/docs/stack-geometry-helpers.md +115 -0
  36. package/docs/vue-adapter.md +4 -0
  37. package/package.json +127 -115
package/README.md CHANGED
@@ -55,6 +55,8 @@ Machina adapters ask you to learn one layout model: Machina records. The framewo
55
55
  - React Native: `machinalayout/text/react-native`
56
56
  - Vue DOM: `machinalayout/text/vue`
57
57
 
58
+ Inspection and handoff utilities are available at `machinalayout/inspect` and `machinalayout/handoff`.
59
+
58
60
  Subpath imports are preferred for adapters/renderers. Root imports remain valid during `0.x` compatibility windows.
59
61
 
60
62
  Framework peers are adapter-specific (`react`/`react-dom`, `react-native`, `vue`) based on the subpaths you use.
@@ -79,6 +81,7 @@ Normal users should not need to learn each framework's layout/template box-drawi
79
81
  - bounded `z`
80
82
  - named layers
81
83
  - layout interpolation helpers
84
+ - stack geometry/content query helpers
82
85
 
83
86
  ### React
84
87
 
@@ -116,6 +119,17 @@ Normal users should not need to learn each framework's layout/template box-drawi
116
119
 
117
120
  Named layers organize paint order over the existing bounded `z` system. Layers are not portals.
118
121
 
122
+
123
+ ## Docs index
124
+
125
+ - [Row model](docs/row-model.md)
126
+ - [Frames and stack](docs/frames-and-stack.md)
127
+ - [Grid arrange](docs/grid-arrange.md)
128
+ - [Stack geometry helpers](docs/stack-geometry-helpers.md)
129
+ - [Screen catalog and viewport matrix](docs/screen-catalog-and-viewports.md)
130
+ - [Inspection and handoff bundles](docs/inspection-and-handoff.md)
131
+ - [Error codes](docs/error-codes.md)
132
+
119
133
  ## Tiny `LayoutRow[]` example
120
134
 
121
135
  ```ts
@@ -383,4 +397,5 @@ This repo uses Biome.
383
397
  - [Z-order and containment](docs/z-order-and-containment.md)
384
398
  - [Error code reference](docs/error-codes.md)
385
399
  - [MachinaDispatch runtime guide](docs/machina-dispatch.md)
400
+ - [DeusMachina behavioral kernel and framework bindings](docs/deusmachina.md)
386
401
  - Dispatch sample: [`samples/dispatch-counter`](samples/dispatch-counter/README.md) (uses `machinalayout/dispatch`)
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { M as MachinaDebugOverlayMode } from './debugOverlay-fWLv1cS7.js';
3
+ import { R as ResolvedLayoutDocument, N as NodeId, a as Rect, b as ResolvedLayoutNode } from './types-CYgsjDai.js';
4
+
5
+ type MachinaSlotProps<TViewData = unknown, TNodeData = unknown> = {
6
+ id: NodeId;
7
+ rect: Rect;
8
+ debugLabel?: string;
9
+ node: ResolvedLayoutNode;
10
+ viewKey?: string;
11
+ viewData?: TViewData;
12
+ nodeData?: TNodeData;
13
+ };
14
+ type MachinaRenderLayer = {
15
+ z: number;
16
+ };
17
+ type MachinaReactDebugOverlayOptions = {
18
+ mode?: MachinaDebugOverlayMode;
19
+ labels?: boolean;
20
+ borders?: boolean;
21
+ selectedNodeId?: string;
22
+ };
23
+ type MachinaReactViewProps = {
24
+ layout: ResolvedLayoutDocument;
25
+ views?: Record<string, React.ComponentType<MachinaSlotProps>>;
26
+ viewData?: Record<string, unknown>;
27
+ nodeData?: Record<NodeId, unknown>;
28
+ className?: string;
29
+ style?: React.CSSProperties;
30
+ nodeClassName?: string;
31
+ debug?: boolean;
32
+ nodeContainment?: "none" | "layout-paint" | "strict";
33
+ nodeContentVisibility?: "none" | "auto";
34
+ nodeContainIntrinsicSize?: string;
35
+ layers?: Record<string, MachinaRenderLayer>;
36
+ defaultLayer?: string;
37
+ debugOverlay?: MachinaReactDebugOverlayOptions;
38
+ };
39
+ declare function MachinaReactView(props: MachinaReactViewProps): React.JSX.Element;
40
+
41
+ export { type MachinaReactDebugOverlayOptions as M, MachinaReactView as a, type MachinaReactViewProps as b, type MachinaSlotProps as c };
@@ -0,0 +1,400 @@
1
+ // src/deus/types.ts
2
+ var DeusMachinaError = class extends Error {
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = "DeusMachinaError";
7
+ }
8
+ code;
9
+ };
10
+
11
+ // src/deus/utility.ts
12
+ function finite(value, code, label) {
13
+ if (!Number.isFinite(value)) throw new DeusMachinaError(code, `${label} must be finite`);
14
+ return value;
15
+ }
16
+ function judgeUtility(context, candidates, options = {}) {
17
+ if (options.hysteresis !== void 0 && (!Number.isFinite(options.hysteresis) || options.hysteresis < 0)) {
18
+ throw new DeusMachinaError("InvalidHysteresis", "hysteresis must be finite and >= 0");
19
+ }
20
+ const results = candidates.map((candidate, index) => {
21
+ const eligible = candidate.when?.(context) ?? true;
22
+ const score = eligible ? finite(
23
+ typeof candidate.score === "function" ? candidate.score(context) : candidate.score,
24
+ "InvalidUtilityScore",
25
+ `utility score for ${candidate.key}`
26
+ ) : 0;
27
+ const reason = typeof candidate.reason === "function" ? candidate.reason(context) : candidate.reason;
28
+ return {
29
+ key: candidate.key,
30
+ eligible,
31
+ score,
32
+ index,
33
+ ...reason !== void 0 ? { reason } : null
34
+ };
35
+ });
36
+ let selected = results.filter((r) => r.eligible).reduce(
37
+ (best, r) => best === null || r.score > best.score ? r : best,
38
+ null
39
+ );
40
+ if (selected && options.previousKey !== void 0 && options.hysteresis !== void 0) {
41
+ const previous = results.find((r) => r.key === options.previousKey && r.eligible);
42
+ if (previous && selected.key !== previous.key && selected.score - previous.score < options.hysteresis)
43
+ selected = previous;
44
+ }
45
+ return { selected, candidates: results };
46
+ }
47
+
48
+ // src/deus/machine.ts
49
+ function assertValidDeusPath(path, label) {
50
+ if (!Array.isArray(path) || path.length === 0) {
51
+ throw new DeusMachinaError("InvalidDeusPath", `${label} must be a non-empty path`);
52
+ }
53
+ path.forEach((segment, index) => {
54
+ if (typeof segment !== "string" || segment.length === 0 || segment.trim().length === 0) {
55
+ throw new DeusMachinaError(
56
+ "InvalidDeusPath",
57
+ `${label} segment ${index} must be a non-empty string`
58
+ );
59
+ }
60
+ });
61
+ }
62
+ function formatDeusPath(path) {
63
+ assertValidDeusPath(path, "path");
64
+ return path.join("/");
65
+ }
66
+ function sameDeusPath(a, b) {
67
+ assertValidDeusPath(a, "left path");
68
+ assertValidDeusPath(b, "right path");
69
+ return a.length === b.length && a.every((v, i) => v === b[i]);
70
+ }
71
+ function isDeusAncestorPath(ancestor, path) {
72
+ assertValidDeusPath(ancestor, "ancestor path");
73
+ assertValidDeusPath(path, "path");
74
+ return ancestor.length <= path.length && ancestor.every((v, i) => v === path[i]);
75
+ }
76
+ function finite2(value, code, label) {
77
+ if (!Number.isFinite(value)) throw new DeusMachinaError(code, `${label} must be finite`);
78
+ return value;
79
+ }
80
+ function pathKey(path) {
81
+ return formatDeusPath(path);
82
+ }
83
+ function validateReason(reason, code, label) {
84
+ if (reason !== void 0 && typeof reason !== "string" && typeof reason !== "function") {
85
+ throw new DeusMachinaError(code, `${label} reason must be a string or function`);
86
+ }
87
+ }
88
+ function defineDeusMachine(machine) {
89
+ if (!machine || typeof machine !== "object") {
90
+ throw new DeusMachinaError("InvalidDeusMachine", "machine must be an object");
91
+ }
92
+ assertValidDeusPath(machine.initial, "initial");
93
+ if (!Array.isArray(machine.states)) {
94
+ throw new DeusMachinaError("InvalidDeusMachine", "states must be an array");
95
+ }
96
+ if (!Array.isArray(machine.transitions)) {
97
+ throw new DeusMachinaError("InvalidDeusMachine", "transitions must be an array");
98
+ }
99
+ const stateKeys = /* @__PURE__ */ new Set();
100
+ const states = machine.states.map((s) => {
101
+ assertValidDeusPath(s.path, "state path");
102
+ const key = pathKey(s.path);
103
+ if (stateKeys.has(key))
104
+ throw new DeusMachinaError("DuplicateDeusStatePath", `duplicate state path ${key}`);
105
+ stateKeys.add(key);
106
+ return { ...s, path: [...s.path] };
107
+ });
108
+ if (!stateKeys.has(pathKey(machine.initial)))
109
+ throw new DeusMachinaError("UnknownDeusStatePath", "initial path must exist");
110
+ const transitionKeys = /* @__PURE__ */ new Set();
111
+ const transitions = machine.transitions.map((t) => {
112
+ if (typeof t.key !== "string" || t.key.length === 0 || t.key.trim().length === 0)
113
+ throw new DeusMachinaError("InvalidDeusTransition", "transition keys must be non-empty");
114
+ if (transitionKeys.has(t.key))
115
+ throw new DeusMachinaError("DuplicateDeusTransitionKey", `duplicate transition key ${t.key}`);
116
+ transitionKeys.add(t.key);
117
+ assertValidDeusPath(t.from, `transition ${t.key} from`);
118
+ if (!stateKeys.has(pathKey(t.from)))
119
+ throw new DeusMachinaError(
120
+ "UnknownDeusStatePath",
121
+ `transition ${t.key} from path must exist`
122
+ );
123
+ if (Array.isArray(t.to)) {
124
+ assertValidDeusPath(t.to, `transition ${t.key} to`);
125
+ if (!stateKeys.has(pathKey(t.to)))
126
+ throw new DeusMachinaError(
127
+ "UnknownDeusStatePath",
128
+ `transition ${t.key} to path must exist`
129
+ );
130
+ } else if (t.to !== void 0 && typeof t.to !== "function") {
131
+ throw new DeusMachinaError(
132
+ "InvalidDeusTransition",
133
+ `transition ${t.key} to must be a path or function`
134
+ );
135
+ }
136
+ if (typeof t.score === "number")
137
+ finite2(t.score, "InvalidDeusTransition", `transition ${t.key} score`);
138
+ validateReason(t.reason, "InvalidDeusTransition", `transition ${t.key}`);
139
+ if (t.hysteresis !== void 0) {
140
+ if (typeof t.hysteresis.previous !== "function")
141
+ throw new DeusMachinaError(
142
+ "InvalidHysteresis",
143
+ `transition ${t.key} hysteresis.previous must be a function`
144
+ );
145
+ finite2(t.hysteresis.margin, "InvalidHysteresis", `transition ${t.key} hysteresis margin`);
146
+ if (t.hysteresis.margin < 0)
147
+ throw new DeusMachinaError(
148
+ "InvalidHysteresis",
149
+ `transition ${t.key} hysteresis margin must be >= 0`
150
+ );
151
+ }
152
+ const utilityKeys = /* @__PURE__ */ new Set();
153
+ for (const u of t.utility ?? []) {
154
+ if (typeof u.key !== "string" || u.key.length === 0 || u.key.trim().length === 0)
155
+ throw new DeusMachinaError(
156
+ "InvalidDeusTransition",
157
+ `transition ${t.key} utility key must be non-empty`
158
+ );
159
+ if (utilityKeys.has(u.key))
160
+ throw new DeusMachinaError("DuplicateUtilityKey", `duplicate utility key ${u.key}`);
161
+ utilityKeys.add(u.key);
162
+ if (typeof u.score !== "number" && typeof u.score !== "function")
163
+ throw new DeusMachinaError(
164
+ "InvalidUtilityScore",
165
+ `utility score for ${u.key} must be a number or function`
166
+ );
167
+ if (typeof u.score === "number")
168
+ finite2(u.score, "InvalidUtilityScore", `utility score for ${u.key}`);
169
+ validateReason(u.reason, "InvalidDeusTransition", `utility ${u.key}`);
170
+ }
171
+ return { ...t, from: [...t.from], to: Array.isArray(t.to) ? [...t.to] : t.to };
172
+ });
173
+ return { initial: [...machine.initial], states, transitions };
174
+ }
175
+ function createDeusSnapshot(machine, board) {
176
+ return { state: [...machine.initial], board, stepIndex: 0 };
177
+ }
178
+ function stepDeusMachine(machine, snapshot, event) {
179
+ const stateBefore = [...snapshot.state];
180
+ assertValidDeusPath(stateBefore, "snapshot state");
181
+ const stateMap = new Map(machine.states.map((s) => [pathKey(s.path), s]));
182
+ const orderedFrom = stateBefore.map((_, i) => stateBefore.slice(0, stateBefore.length - i));
183
+ const candidates = orderedFrom.flatMap(
184
+ (from) => machine.transitions.map((t) => ({ t })).filter(({ t }) => sameDeusPath(t.from, from))
185
+ );
186
+ const traces = [];
187
+ let selected;
188
+ candidates.forEach(({ t }, index) => {
189
+ const eventMatches = t.event === void 0 || t.event === event.type;
190
+ let eligible = eventMatches && (t.when?.(snapshot.board, event) ?? true);
191
+ let utility;
192
+ let utilityKey;
193
+ let score = eligible ? t.score === void 0 ? 1 : finite2(
194
+ typeof t.score === "function" ? t.score(snapshot.board, event) : t.score,
195
+ "InvalidDeusTransition",
196
+ `transition ${t.key} score`
197
+ ) : 0;
198
+ if (eligible && t.utility) {
199
+ utility = judgeUtility(
200
+ { board: snapshot.board, event },
201
+ t.utility.map((u) => ({
202
+ key: u.key,
203
+ when: (ctx) => u.when?.(ctx.board, ctx.event) ?? true,
204
+ score: (ctx) => typeof u.score === "function" ? u.score(ctx.board, ctx.event) : u.score,
205
+ reason: typeof u.reason === "function" ? (ctx) => {
206
+ const reason2 = u.reason;
207
+ return typeof reason2 === "function" ? reason2(ctx.board, ctx.event) : "";
208
+ } : u.reason
209
+ })),
210
+ t.hysteresis ? { previousKey: t.hysteresis.previous(snapshot.board), hysteresis: t.hysteresis.margin } : void 0
211
+ );
212
+ if (!utility.selected) eligible = false;
213
+ else {
214
+ utilityKey = utility.selected.key;
215
+ if (t.score === void 0) score = utility.selected.score;
216
+ }
217
+ }
218
+ const to = eligible && t.to ? typeof t.to === "function" ? [...t.to(snapshot.board, event)] : [...t.to] : void 0;
219
+ if (to) {
220
+ assertValidDeusPath(to, `transition ${t.key} to`);
221
+ if (!stateMap.has(pathKey(to)))
222
+ throw new DeusMachinaError(
223
+ "UnknownDeusStatePath",
224
+ `transition ${t.key} to path must exist`
225
+ );
226
+ }
227
+ const reason = typeof t.reason === "function" ? t.reason(snapshot.board, event) : t.reason;
228
+ const trace = {
229
+ key: t.key,
230
+ from: [...t.from],
231
+ ...to ? { to } : null,
232
+ event: t.event,
233
+ eligible,
234
+ score: eligible ? score : 0,
235
+ index,
236
+ ...reason !== void 0 ? { reason } : null,
237
+ ...utility ? { utility } : null
238
+ };
239
+ traces.push(trace);
240
+ if (eligible && (!selected || trace.score > selected.trace.score))
241
+ selected = { trace, t, utilityKey };
242
+ });
243
+ if (!selected)
244
+ return {
245
+ snapshot: { state: stateBefore, board: snapshot.board, stepIndex: snapshot.stepIndex + 1 },
246
+ trace: { stateBefore, stateAfter: stateBefore, event: event.type, transitions: traces }
247
+ };
248
+ const target = selected.trace.to ?? stateBefore;
249
+ const common = stateBefore.findIndex((v, i) => target[i] !== v);
250
+ const prefix = common === -1 ? Math.min(stateBefore.length, target.length) : common;
251
+ for (let i = stateBefore.length; i > prefix; i--)
252
+ stateMap.get(pathKey(stateBefore.slice(0, i)))?.onExit?.(snapshot.board, event);
253
+ if (selected.utilityKey)
254
+ selected.t.utility?.find((u) => u.key === selected?.utilityKey)?.do?.(snapshot.board, event);
255
+ selected.t.do?.(snapshot.board, event);
256
+ for (let i = prefix + 1; i <= target.length; i++)
257
+ stateMap.get(pathKey(target.slice(0, i)))?.onEnter?.(snapshot.board, event);
258
+ return {
259
+ snapshot: { state: [...target], board: snapshot.board, stepIndex: snapshot.stepIndex + 1 },
260
+ trace: {
261
+ stateBefore,
262
+ stateAfter: [...target],
263
+ event: event.type,
264
+ selectedTransition: selected.trace,
265
+ transitions: traces
266
+ }
267
+ };
268
+ }
269
+ function formatDeusStepTrace(trace) {
270
+ const selected = trace.selectedTransition ? trace.selectedTransition.key : "none";
271
+ return `${formatDeusPath(trace.stateBefore)} --${trace.event}/${selected}--> ${formatDeusPath(trace.stateAfter)}`;
272
+ }
273
+
274
+ // src/deus/debugOverlay.ts
275
+ var collapsed = ["debugOverlay", "collapsed"];
276
+ var overlay = ["debugOverlay", "nonInteractiveOverlay"];
277
+ var panel = ["debugOverlay", "interactivePanel"];
278
+ function createMachinaDebugOverlayMachine() {
279
+ return defineDeusMachine({
280
+ initial: collapsed,
281
+ states: [
282
+ {
283
+ path: collapsed,
284
+ onEnter: (b) => {
285
+ b.mode = "collapsed";
286
+ b.selectedNodeId = void 0;
287
+ }
288
+ },
289
+ {
290
+ path: overlay,
291
+ onEnter: (b) => {
292
+ b.mode = "nonInteractiveOverlay";
293
+ }
294
+ },
295
+ {
296
+ path: panel,
297
+ onEnter: (b) => {
298
+ b.mode = "interactivePanel";
299
+ }
300
+ }
301
+ ],
302
+ transitions: [
303
+ { key: "collapsed.showOverlay", from: collapsed, event: "showOverlay", to: overlay },
304
+ {
305
+ key: "overlay.openPanel",
306
+ from: overlay,
307
+ event: "openPanel",
308
+ to: panel,
309
+ do: (b, e) => {
310
+ if (e.type === "openPanel") b.selectedNodeId = e.nodeId;
311
+ }
312
+ },
313
+ { key: "panel.showOverlay", from: panel, event: "showOverlay", to: overlay },
314
+ { key: "overlay.collapse", from: overlay, event: "collapse", to: collapsed },
315
+ { key: "panel.collapse", from: panel, event: "collapse", to: collapsed },
316
+ {
317
+ key: "overlay.toggleLabels",
318
+ from: overlay,
319
+ event: "toggleLabels",
320
+ do: (b) => {
321
+ b.labels = !b.labels;
322
+ }
323
+ },
324
+ {
325
+ key: "panel.toggleLabels",
326
+ from: panel,
327
+ event: "toggleLabels",
328
+ do: (b) => {
329
+ b.labels = !b.labels;
330
+ }
331
+ },
332
+ {
333
+ key: "overlay.toggleBorders",
334
+ from: overlay,
335
+ event: "toggleBorders",
336
+ do: (b) => {
337
+ b.borders = !b.borders;
338
+ }
339
+ },
340
+ {
341
+ key: "panel.toggleBorders",
342
+ from: panel,
343
+ event: "toggleBorders",
344
+ do: (b) => {
345
+ b.borders = !b.borders;
346
+ }
347
+ },
348
+ {
349
+ key: "panel.selectNode",
350
+ from: panel,
351
+ event: "selectNode",
352
+ do: (b, e) => {
353
+ if (e.type === "selectNode") b.selectedNodeId = e.nodeId;
354
+ }
355
+ }
356
+ ]
357
+ });
358
+ }
359
+ function getMachinaDebugOverlayBehavior(board) {
360
+ if (board.mode === "collapsed")
361
+ return {
362
+ visible: false,
363
+ pointerEvents: "none",
364
+ consumesLayoutSpace: false,
365
+ showPanel: false,
366
+ showLabels: false,
367
+ showBorders: false
368
+ };
369
+ if (board.mode === "nonInteractiveOverlay")
370
+ return {
371
+ visible: true,
372
+ pointerEvents: "none",
373
+ consumesLayoutSpace: false,
374
+ showPanel: false,
375
+ showLabels: board.labels,
376
+ showBorders: board.borders
377
+ };
378
+ return {
379
+ visible: true,
380
+ pointerEvents: "auto",
381
+ consumesLayoutSpace: true,
382
+ showPanel: true,
383
+ showLabels: board.labels,
384
+ showBorders: board.borders
385
+ };
386
+ }
387
+
388
+ export {
389
+ DeusMachinaError,
390
+ judgeUtility,
391
+ formatDeusPath,
392
+ sameDeusPath,
393
+ isDeusAncestorPath,
394
+ defineDeusMachine,
395
+ createDeusSnapshot,
396
+ stepDeusMachine,
397
+ formatDeusStepTrace,
398
+ createMachinaDebugOverlayMachine,
399
+ getMachinaDebugOverlayBehavior
400
+ };
@@ -0,0 +1,186 @@
1
+ import {
2
+ MachinaLayoutError
3
+ } from "./chunk-VREK57S3.js";
4
+
5
+ // src/screenCatalog.ts
6
+ var STANDARD_VIEWPORTS = [
7
+ { key: "desktop", width: 1440, height: 900, label: "Desktop", tags: ["desktop"] },
8
+ { key: "tablet", width: 1024, height: 768, label: "Tablet", tags: ["tablet"] },
9
+ { key: "phone", width: 390, height: 844, label: "Phone", tags: ["phone", "mobile"] }
10
+ ];
11
+ function isPositiveFiniteNumber(value) {
12
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
13
+ }
14
+ function validateStringArray(value, code, field) {
15
+ if (value === void 0) return;
16
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
17
+ throw new MachinaLayoutError(code, `${field} must be an array of strings`);
18
+ }
19
+ }
20
+ function copyViewport(viewport) {
21
+ return {
22
+ ...viewport,
23
+ tags: viewport.tags === void 0 ? void 0 : [...viewport.tags]
24
+ };
25
+ }
26
+ function copyScreen(screen) {
27
+ return {
28
+ ...screen,
29
+ viewports: screen.viewports === void 0 ? void 0 : [...screen.viewports],
30
+ tags: screen.tags === void 0 ? void 0 : [...screen.tags]
31
+ };
32
+ }
33
+ function defineMachinaViewports(viewports) {
34
+ const seen = /* @__PURE__ */ new Set();
35
+ return viewports.map((viewport) => {
36
+ if (typeof viewport.key !== "string" || viewport.key.trim() === "") {
37
+ throw new MachinaLayoutError("InvalidViewport", "viewport key must be a non-empty string");
38
+ }
39
+ if (seen.has(viewport.key)) {
40
+ throw new MachinaLayoutError(
41
+ "DuplicateViewportKey",
42
+ `duplicate viewport key: ${viewport.key}`
43
+ );
44
+ }
45
+ seen.add(viewport.key);
46
+ if (!isPositiveFiniteNumber(viewport.width) || !isPositiveFiniteNumber(viewport.height)) {
47
+ throw new MachinaLayoutError(
48
+ "InvalidViewport",
49
+ `viewport ${viewport.key} width and height must be finite positive numbers`
50
+ );
51
+ }
52
+ if (viewport.deviceScaleFactor !== void 0 && !isPositiveFiniteNumber(viewport.deviceScaleFactor)) {
53
+ throw new MachinaLayoutError(
54
+ "InvalidViewport",
55
+ `viewport ${viewport.key} deviceScaleFactor must be a finite positive number`
56
+ );
57
+ }
58
+ if (viewport.label !== void 0 && typeof viewport.label !== "string") {
59
+ throw new MachinaLayoutError(
60
+ "InvalidViewport",
61
+ `viewport ${viewport.key} label must be a string`
62
+ );
63
+ }
64
+ validateStringArray(viewport.tags, "InvalidViewport", `viewport ${viewport.key} tags`);
65
+ return copyViewport(viewport);
66
+ });
67
+ }
68
+ function createViewportMatrix(preset = "standard-responsive") {
69
+ if (preset === "desktop-only") return defineMachinaViewports([STANDARD_VIEWPORTS[0]]);
70
+ if (preset === "mobile-first")
71
+ return defineMachinaViewports([
72
+ STANDARD_VIEWPORTS[2],
73
+ STANDARD_VIEWPORTS[1],
74
+ STANDARD_VIEWPORTS[0]
75
+ ]);
76
+ return defineMachinaViewports(STANDARD_VIEWPORTS);
77
+ }
78
+ function defineMachinaScreens(screens) {
79
+ const catalog = { screens: {}, order: [] };
80
+ for (const screen of screens) {
81
+ if (typeof screen.key !== "string" || screen.key.trim() === "") {
82
+ throw new MachinaLayoutError("InvalidScreen", "screen key must be a non-empty string");
83
+ }
84
+ if (catalog.screens[screen.key] !== void 0) {
85
+ throw new MachinaLayoutError("DuplicateScreenKey", `duplicate screen key: ${screen.key}`);
86
+ }
87
+ if (typeof screen.route !== "string" || screen.route.trim() === "") {
88
+ throw new MachinaLayoutError(
89
+ "InvalidScreen",
90
+ `screen ${screen.key} route must be a non-empty string`
91
+ );
92
+ }
93
+ if (screen.fixture !== void 0 && typeof screen.fixture !== "string") {
94
+ throw new MachinaLayoutError(
95
+ "InvalidScreen",
96
+ `screen ${screen.key} fixture must be a string`
97
+ );
98
+ }
99
+ validateStringArray(screen.viewports, "InvalidScreen", `screen ${screen.key} viewports`);
100
+ validateStringArray(screen.tags, "InvalidScreen", `screen ${screen.key} tags`);
101
+ if (screen.metadata !== void 0 && (typeof screen.metadata !== "object" || screen.metadata === null || Array.isArray(screen.metadata))) {
102
+ throw new MachinaLayoutError(
103
+ "InvalidScreen",
104
+ `screen ${screen.key} metadata must be an object`
105
+ );
106
+ }
107
+ catalog.screens[screen.key] = copyScreen(screen);
108
+ catalog.order.push(screen.key);
109
+ }
110
+ return catalog;
111
+ }
112
+ function slugMachinaArtifactName(input) {
113
+ const slug = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
114
+ return slug === "" ? "artifact" : slug;
115
+ }
116
+ function getMachinaViewport(viewports, key) {
117
+ const viewport = viewports.find((candidate) => candidate.key === key);
118
+ if (!viewport) throw new MachinaLayoutError("UnknownViewportKey", `unknown viewport key: ${key}`);
119
+ return viewport;
120
+ }
121
+ function orderedUnique(values) {
122
+ const result = [];
123
+ for (const value of values ?? []) {
124
+ if (!result.includes(value)) result.push(value);
125
+ }
126
+ return result;
127
+ }
128
+ function expandScreenViewportTasks(catalog, viewports, options = {}) {
129
+ const viewportKeys = viewports.map((viewport) => viewport.key);
130
+ const viewportKeySet = new Set(viewportKeys);
131
+ for (const key of options.screenKeys ?? []) {
132
+ if (catalog.screens[key] === void 0)
133
+ throw new MachinaLayoutError("UnknownScreenKey", `unknown screen key: ${key}`);
134
+ }
135
+ for (const key of options.viewportKeys ?? []) {
136
+ if (!viewportKeySet.has(key))
137
+ throw new MachinaLayoutError("UnknownViewportKey", `unknown viewport key: ${key}`);
138
+ }
139
+ const requestedScreens = options.screenKeys === void 0 ? void 0 : new Set(options.screenKeys);
140
+ const requestedViewports = options.viewportKeys === void 0 ? void 0 : new Set(options.viewportKeys);
141
+ const tasks = [];
142
+ for (const screenKey of catalog.order) {
143
+ if (requestedScreens && !requestedScreens.has(screenKey)) continue;
144
+ const screen = catalog.screens[screenKey];
145
+ if (!screen)
146
+ throw new MachinaLayoutError(
147
+ "UnknownScreenKey",
148
+ `unknown screen key in catalog order: ${screenKey}`
149
+ );
150
+ const screenViewportSet = screen.viewports === void 0 ? void 0 : new Set(screen.viewports);
151
+ for (const key of screen.viewports ?? []) {
152
+ if (!viewportKeySet.has(key))
153
+ throw new MachinaLayoutError(
154
+ "UnknownViewportKey",
155
+ `screen ${screen.key} references unknown viewport key: ${key}`
156
+ );
157
+ }
158
+ for (const viewport of viewports) {
159
+ if (screenViewportSet && !screenViewportSet.has(viewport.key)) continue;
160
+ if (requestedViewports && !requestedViewports.has(viewport.key)) continue;
161
+ const tags = orderedUnique([...screen.tags ?? [], ...viewport.tags ?? []]);
162
+ if ((options.tags ?? []).some((tag) => !tags.includes(tag))) continue;
163
+ tasks.push({
164
+ key: `${screen.key}__${viewport.key}`,
165
+ screenKey: screen.key,
166
+ viewportKey: viewport.key,
167
+ route: screen.route,
168
+ fixture: screen.fixture,
169
+ viewport,
170
+ screen,
171
+ tags,
172
+ artifactBaseName: `${slugMachinaArtifactName(screen.key)}__${slugMachinaArtifactName(viewport.key)}`
173
+ });
174
+ }
175
+ }
176
+ return tasks;
177
+ }
178
+
179
+ export {
180
+ defineMachinaViewports,
181
+ createViewportMatrix,
182
+ defineMachinaScreens,
183
+ slugMachinaArtifactName,
184
+ getMachinaViewport,
185
+ expandScreenViewportTasks
186
+ };