machinalayout 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yuechen Li
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # MachinaLayout
2
+
3
+ MachinaLayout is a framework-independent, machine-native layout system that resolves flat typed layout records into deterministic rectangles.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install machinalayout
9
+ ```
10
+
11
+ ## Core principles
12
+
13
+ - **Layout is data.**
14
+ - **Rendering is an adapter.**
15
+ - **Nesting is an output shape, not an authoring strategy.**
16
+ - **MachinaLayout controls outer rectangles; host frameworks control view internals (using `view ?? slot`).**
17
+
18
+ ## What problem it solves
19
+
20
+ MachinaLayout keeps geometry explicit and local:
21
+
22
+ - avoids vague CSS negotiation,
23
+ - makes numeric layout edits predictable,
24
+ - gives humans and LLMs a table/record-shaped layout format.
25
+
26
+ ## Tiny `LayoutRow[]` example
27
+
28
+ ```ts
29
+ import {
30
+ type LayoutRow,
31
+ resolveLayoutRows,
32
+ type Rect,
33
+ } from "machinalayout";
34
+
35
+ const rows: LayoutRow[] = [
36
+ {
37
+ id: "root",
38
+ frame: { kind: "root" },
39
+ },
40
+ {
41
+ id: "header",
42
+ parent: "root",
43
+ order: 0,
44
+ frame: { kind: "anchor", left: 0, right: 0, top: 0, height: 64 },
45
+ slot: "header",
46
+ },
47
+ {
48
+ id: "sidebar",
49
+ parent: "root",
50
+ order: 1,
51
+ frame: { kind: "anchor", left: 0, top: 64, bottom: 0, width: 240 },
52
+ slot: "sidebar",
53
+ },
54
+ {
55
+ id: "toolbar",
56
+ parent: "root",
57
+ order: 2,
58
+ frame: { kind: "anchor", left: 240, right: 0, top: 64, height: 56 },
59
+ arrange: {
60
+ kind: "stack",
61
+ axis: "horizontal",
62
+ gap: 8,
63
+ padding: { top: 8, right: 8, bottom: 8, left: 8 },
64
+ justify: "start",
65
+ align: "center",
66
+ },
67
+ },
68
+ {
69
+ id: "toolbar-button-1",
70
+ parent: "toolbar",
71
+ order: 0,
72
+ frame: { kind: "fixed", width: 120, height: 40 },
73
+ slot: "toolbarButton",
74
+ },
75
+ ];
76
+
77
+ const rootRect: Rect = { x: 0, y: 0, width: 1024, height: 640 };
78
+ const resolved = resolveLayoutRows(rows, rootRect);
79
+ ```
80
+
81
+ ## React adapter quick example
82
+
83
+ ```tsx
84
+ import { MachinaReactView, resolveLayoutRows } from "machinalayout";
85
+
86
+ const resolved = resolveLayoutRows(rows, rootRect);
87
+
88
+ const views = {
89
+ header: HeaderView,
90
+ sidebar: SidebarView,
91
+ toolbarButton: ToolbarButtonView,
92
+ };
93
+
94
+ export function App() {
95
+ return <MachinaReactView layout={resolved} views={views} viewData={{ sidebar: { collapsed: false } }} />;
96
+ }
97
+ ```
98
+
99
+ ## Sample demo
100
+
101
+ See [`samples/control-room`](samples/control-room/README.md).
102
+
103
+ Run it locally:
104
+
105
+ ```bash
106
+ cd samples/control-room
107
+ npm install
108
+ npm run dev
109
+ ```
110
+
111
+ ## M0 scope (current)
112
+
113
+ M0 supports:
114
+
115
+ - `RootFrame` (M1a)
116
+ - `AbsoluteFrame`
117
+ - `AnchorFrame`
118
+ - `FixedFrame`
119
+ - `FillFrame` (M1b, stack-child weighted fill)
120
+ - `StackArrange`
121
+ - bounded sibling-local z metadata
122
+ - React adapter
123
+
124
+ M0 does **not** support:
125
+
126
+ - FlowBox
127
+ - wrapping
128
+ - flexbox-style shrink/basis/min/max negotiation
129
+ - intrinsic sizing
130
+ - text measurement
131
+ - routing
132
+ - state management
133
+ - CSS layout authority for Machina rectangles
134
+
135
+ ## Documentation
136
+
137
+ - [M0 contract](docs/m0-contract.md)
138
+ - [Row model](docs/row-model.md)
139
+ - [Frames and stack](docs/frames-and-stack.md)
140
+ - [React adapter boundary](docs/react-adapter.md)
141
+ - [Z-order and containment](docs/z-order-and-containment.md)
142
+ - [Forbidden concepts](docs/forbidden-concepts.md)
143
+
144
+
145
+ ## M1c typed UI lengths
146
+
147
+ Anchor fields now accept typed `UiLength` values in addition to numeric pixels:
148
+
149
+ - `number` (implicit px)
150
+ - `{ unit: "px", value: number }`
151
+ - `{ unit: "ui", value: number }` (normalized against parent axis)
152
+
153
+ Example: `left: { unit: "ui", value: 0.25 }`.
154
+
155
+ - node-level `OffsetSpec` post-placement nudges (M1d, not margins)
@@ -0,0 +1,269 @@
1
+ import React from 'react';
2
+
3
+ type NodeId = string;
4
+ type Rect = {
5
+ x: number;
6
+ y: number;
7
+ width: number;
8
+ height: number;
9
+ };
10
+ type AbsoluteFrame = {
11
+ kind: "absolute";
12
+ x: number;
13
+ y: number;
14
+ width: number;
15
+ height: number;
16
+ };
17
+ type UiLength = number | {
18
+ unit: "px";
19
+ value: number;
20
+ } | {
21
+ unit: "ui";
22
+ value: number;
23
+ };
24
+ type OffsetSpec = {
25
+ x?: UiLength;
26
+ y?: UiLength;
27
+ };
28
+ type AnchorFrame = {
29
+ kind: "anchor";
30
+ left?: UiLength;
31
+ right?: UiLength;
32
+ top?: UiLength;
33
+ bottom?: UiLength;
34
+ width?: UiLength;
35
+ height?: UiLength;
36
+ };
37
+ type RootFrame = {
38
+ kind: "root";
39
+ };
40
+ type FixedFrame = {
41
+ kind: "fixed";
42
+ width: number;
43
+ height: number;
44
+ };
45
+ type FillFrame = {
46
+ kind: "fill";
47
+ weight?: number;
48
+ cross?: number | "fill";
49
+ };
50
+ type FrameSpec = RootFrame | AbsoluteFrame | AnchorFrame | FixedFrame | FillFrame;
51
+ type StackAxis = "horizontal" | "vertical";
52
+ type StackJustify = "start" | "center" | "end" | "space-between";
53
+ type StackAlign = "start" | "center" | "end";
54
+ type EdgeInsets = {
55
+ top: number;
56
+ right: number;
57
+ bottom: number;
58
+ left: number;
59
+ };
60
+ type StackArrange = {
61
+ kind: "stack";
62
+ axis: StackAxis;
63
+ gap?: number;
64
+ padding?: number | EdgeInsets;
65
+ justify?: StackJustify;
66
+ align?: StackAlign;
67
+ };
68
+ type ArrangeSpec = StackArrange;
69
+ type LayoutRow = {
70
+ id: NodeId;
71
+ parent?: NodeId;
72
+ order?: number;
73
+ z?: number;
74
+ frame: FrameSpec;
75
+ arrange?: ArrangeSpec;
76
+ view?: string;
77
+ slot?: string;
78
+ debugLabel?: string;
79
+ offset?: OffsetSpec;
80
+ };
81
+ type LayoutNode = {
82
+ id: NodeId;
83
+ z?: number;
84
+ frame: FrameSpec;
85
+ arrange?: ArrangeSpec;
86
+ view?: string;
87
+ slot?: string;
88
+ debugLabel?: string;
89
+ offset?: OffsetSpec;
90
+ };
91
+ type LayoutDocument = {
92
+ rootId: NodeId;
93
+ nodes: Record<NodeId, LayoutNode>;
94
+ children: Record<NodeId, NodeId[]>;
95
+ };
96
+ type ResolvedLayoutNode = {
97
+ id: NodeId;
98
+ z?: number;
99
+ rect: Rect;
100
+ frame: FrameSpec;
101
+ arrange?: ArrangeSpec;
102
+ view?: string;
103
+ slot?: string;
104
+ debugLabel?: string;
105
+ offset?: OffsetSpec;
106
+ };
107
+ type ResolvedLayoutDocument = {
108
+ rootId: NodeId;
109
+ nodes: Record<NodeId, ResolvedLayoutNode>;
110
+ children: Record<NodeId, NodeId[]>;
111
+ };
112
+ type ResolvedLayoutTree = {
113
+ id: NodeId;
114
+ z?: number;
115
+ rect: Rect;
116
+ frame: FrameSpec;
117
+ arrange?: ArrangeSpec;
118
+ view?: string;
119
+ slot?: string;
120
+ debugLabel?: string;
121
+ offset?: OffsetSpec;
122
+ children: ResolvedLayoutTree[];
123
+ };
124
+
125
+ type MachinaLayoutErrorCode = "EmptyRows" | "MissingRoot" | "MultipleRoots" | "DuplicateId" | "InvalidId" | "MissingParent" | "UnknownParent" | "SelfParent" | "Cycle" | "UnreachableNode" | "NonFiniteNumber" | "InvalidLengthUnit" | "InvalidZ" | "NegativeSize" | "NegativeGap" | "NegativePadding" | "InvalidAnchorHorizontal" | "InvalidAnchorVertical" | "NegativeResolvedSize" | "FixedFrameWithoutArranger" | "FillFrameWithoutArranger" | "InvalidFillWeight" | "StackChildMustBeFixed" | "StackContentNegative" | "StackOverflow" | "RootFrameNotRoot" | "RootFrameWithoutRoot";
126
+ declare class MachinaLayoutError extends Error {
127
+ readonly code: MachinaLayoutErrorCode;
128
+ constructor(code: MachinaLayoutErrorCode, message: string);
129
+ }
130
+
131
+ declare function assertFiniteNumber(value: number, fieldName: string): void;
132
+ declare function assertNonNegativeSize(value: number, fieldName: string): void;
133
+ declare function assertNonNegativeGap(value: number, fieldName?: string): void;
134
+ declare function assertNonNegativePadding(value: number, fieldName?: string): void;
135
+
136
+ declare function normalizePadding(padding?: number | EdgeInsets): EdgeInsets;
137
+
138
+ declare function resolveUiLength(length: UiLength, axisSize: number, fieldName?: string): number;
139
+
140
+ declare function applyOffset(rect: Rect, parentRect: Rect, offset?: OffsetSpec): Rect;
141
+
142
+ declare function compileLayoutRows(rows: LayoutRow[]): LayoutDocument;
143
+
144
+ declare function resolveFrame(parent: Rect, frame: FrameSpec): Rect;
145
+
146
+ declare function resolveLayoutDocument(document: LayoutDocument, rootRect: Rect): ResolvedLayoutDocument;
147
+
148
+ declare function resolveLayoutRows(rows: LayoutRow[], rootRect: Rect): ResolvedLayoutDocument;
149
+
150
+ declare function toResolvedTree(document: ResolvedLayoutDocument): ResolvedLayoutTree;
151
+
152
+ declare function flattenResolvedTree(tree: ResolvedLayoutTree): ResolvedLayoutNode[];
153
+
154
+ declare function formatRect(rect: Rect): string;
155
+
156
+ type MachinaSlotProps<TViewData = unknown, TNodeData = unknown> = {
157
+ id: NodeId;
158
+ rect: Rect;
159
+ debugLabel?: string;
160
+ node: ResolvedLayoutNode;
161
+ viewKey?: string;
162
+ viewData?: TViewData;
163
+ nodeData?: TNodeData;
164
+ };
165
+ type MachinaReactViewProps = {
166
+ layout: ResolvedLayoutDocument;
167
+ views?: Record<string, React.ComponentType<MachinaSlotProps>>;
168
+ viewData?: Record<string, unknown>;
169
+ nodeData?: Record<NodeId, unknown>;
170
+ className?: string;
171
+ style?: React.CSSProperties;
172
+ nodeClassName?: string;
173
+ debug?: boolean;
174
+ nodeContainment?: "none" | "layout-paint" | "strict";
175
+ nodeContentVisibility?: "none" | "auto";
176
+ nodeContainIntrinsicSize?: string;
177
+ };
178
+ declare function MachinaReactView(props: MachinaReactViewProps): React.JSX.Element;
179
+
180
+ type MachinaTextSource = {
181
+ kind: "plain";
182
+ text: string;
183
+ } | {
184
+ kind: "machina-text";
185
+ text: string;
186
+ };
187
+ type MachinaTextVariant = "body" | "label" | "caption" | "title" | "mono";
188
+ type MachinaTextWrap = "word" | "none";
189
+ type MachinaTextOverflow = "clip" | "ellipsis" | "scroll";
190
+ type MachinaTextAlign = "start" | "center" | "end";
191
+ type MachinaTextLeading = "tight" | "normal" | "loose" | number;
192
+ type MachinaTextVerticalAlign = "top" | "center" | "bottom";
193
+ type MachinaTextSpec = {
194
+ kind: "text";
195
+ source: MachinaTextSource;
196
+ variant?: MachinaTextVariant;
197
+ wrap?: MachinaTextWrap;
198
+ overflow?: MachinaTextOverflow;
199
+ align?: MachinaTextAlign;
200
+ leading?: MachinaTextLeading;
201
+ blockGap?: number;
202
+ listGap?: number;
203
+ valign?: MachinaTextVerticalAlign;
204
+ };
205
+ type MachinaTextDocument = {
206
+ blocks: MachinaTextBlock[];
207
+ };
208
+ type MachinaTextBlock = {
209
+ kind: "paragraph";
210
+ inline: MachinaInline[];
211
+ } | {
212
+ kind: "bulletList";
213
+ items: MachinaBulletItem[];
214
+ };
215
+ type MachinaBulletItem = {
216
+ inline: MachinaInline[];
217
+ children?: MachinaBulletItem[];
218
+ };
219
+ type MachinaInline = {
220
+ kind: "text";
221
+ text: string;
222
+ } | {
223
+ kind: "strong";
224
+ children: MachinaInline[];
225
+ } | {
226
+ kind: "emphasis";
227
+ children: MachinaInline[];
228
+ } | {
229
+ kind: "code";
230
+ text: string;
231
+ } | {
232
+ kind: "link";
233
+ href: string;
234
+ children: MachinaInline[];
235
+ };
236
+ type MachinaTextDiagnosticLevel = "error" | "warning";
237
+ type MachinaTextDiagnosticCode = "unsupported_syntax" | "heading_forbidden" | "max_list_depth_exceeded" | "malformed_link" | "unclosed_inline" | "invalid_escape";
238
+ type MachinaTextDiagnostic = {
239
+ code: MachinaTextDiagnosticCode;
240
+ message: string;
241
+ index: number;
242
+ length: number;
243
+ line: number;
244
+ column: number;
245
+ level: MachinaTextDiagnosticLevel;
246
+ };
247
+ type ParseMachinaTextResult = {
248
+ ok: boolean;
249
+ document: MachinaTextDocument;
250
+ diagnostics: MachinaTextDiagnostic[];
251
+ };
252
+
253
+ declare function parseMachinaTextInline(text: string): {
254
+ inline: MachinaInline[];
255
+ diagnostics: MachinaTextDiagnostic[];
256
+ };
257
+ declare function parseMachinaText(source: MachinaTextSource | string): ParseMachinaTextResult;
258
+
259
+ type MachinaTextViewProps = {
260
+ text: MachinaTextSpec | MachinaTextSource | MachinaTextDocument | string;
261
+ className?: string;
262
+ style?: React.CSSProperties;
263
+ linkTarget?: React.HTMLAttributeAnchorTarget;
264
+ onLinkClick?: (href: string, event: React.MouseEvent<HTMLAnchorElement>) => void;
265
+ showDiagnostics?: boolean;
266
+ };
267
+ declare function MachinaTextView(props: MachinaTextViewProps): React.JSX.Element;
268
+
269
+ export { type AbsoluteFrame, type AnchorFrame, type ArrangeSpec, type EdgeInsets, type FillFrame, type FixedFrame, type FrameSpec, type LayoutDocument, type LayoutNode, type LayoutRow, type MachinaBulletItem, type MachinaInline, MachinaLayoutError, type MachinaLayoutErrorCode, MachinaReactView, type MachinaReactViewProps, type MachinaSlotProps, type MachinaTextAlign, type MachinaTextBlock, type MachinaTextDiagnostic, type MachinaTextDiagnosticCode, type MachinaTextDiagnosticLevel, type MachinaTextDocument, type MachinaTextLeading, type MachinaTextOverflow, type MachinaTextSource, type MachinaTextSpec, type MachinaTextVariant, type MachinaTextVerticalAlign, MachinaTextView, type MachinaTextViewProps, type MachinaTextWrap, type NodeId, type OffsetSpec, type ParseMachinaTextResult, type Rect, type ResolvedLayoutDocument, type ResolvedLayoutNode, type ResolvedLayoutTree, type RootFrame, type StackAlign, type StackArrange, type StackAxis, type StackJustify, type UiLength, applyOffset, assertFiniteNumber, assertNonNegativeGap, assertNonNegativePadding, assertNonNegativeSize, compileLayoutRows, flattenResolvedTree, formatRect, normalizePadding, parseMachinaText, parseMachinaTextInline, resolveFrame, resolveLayoutDocument, resolveLayoutRows, resolveUiLength, toResolvedTree };