@fungi.computer/maosaico 0.0.0-stage → 0.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jamstack Consulting LLC
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 CHANGED
@@ -1,3 +1,225 @@
1
- # Temporary Holding Version
1
+ # Maosaico (`@fungi.computer/maosaico`)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Maosaico arranges React panes inside a measured container. It supports floating
4
+ frames and optional tiling while leaving pane content, styling, controls, and
5
+ DOM focus with the host.
6
+
7
+ ## Install or set up
8
+
9
+ Maosaico is a React tiling layout package. It requires React 17 or newer as a
10
+ peer dependency. From the repository root, build it with:
11
+
12
+ ```sh
13
+ pnpm --filter @fungi.computer/maosaico build
14
+ ```
15
+
16
+ The package exports `MaosaicoRoot`, `useMaosaicoItem`, `useMaosaicoLayout`,
17
+ `MaosaicoLayers`, `useLayer`, and `usePointerDrag` from
18
+ `@fungi.computer/maosaico`.
19
+
20
+ `usePointerDrag` supplies captured-pointer DOM props for a caller-owned gesture.
21
+ Its `move` callback receives incremental pixel deltas; cancellation and unmount
22
+ release the same capture. An optional third `AbortSignal` lets the caller cancel
23
+ a gesture; Escape policy stays with the caller. Grove uses it with the Caps
24
+ `ResizeHandle` for its conversation rail, without introducing a tiling root for
25
+ one sidebar width.
26
+
27
+ ## Quick start
28
+
29
+ This illustrative example registers panes and asks Maosaico to tile them within
30
+ the supplied bounds:
31
+
32
+ ```tsx
33
+ import { MaosaicoRoot, useMaosaicoItem } from "@fungi.computer/maosaico";
34
+
35
+ function Pane({ id }: { id: string }) {
36
+ const pane = useMaosaicoItem({
37
+ id,
38
+ frame: { x: 0, y: 0, width: 320, height: 240 },
39
+ minima: { width: 180, height: 120 },
40
+ });
41
+
42
+ return (
43
+ <section
44
+ hidden={!pane.visible}
45
+ {...pane.props}
46
+ style={{
47
+ position: "absolute",
48
+ left: pane.frame.x,
49
+ top: pane.frame.y,
50
+ width: pane.frame.width,
51
+ height: pane.frame.height,
52
+ }}
53
+ >
54
+ Pane content stays mounted here.
55
+ </section>
56
+ );
57
+ }
58
+
59
+ function Workspace() {
60
+ return (
61
+ <div style={{ position: "relative", width: 1000, height: 600 }}>
62
+ <MaosaicoRoot bounds={{ width: 1000, height: 600 }} tiling={{ gap: 8 }}>
63
+ <Pane id="chat" />
64
+ <Pane id="files" />
65
+ </MaosaicoRoot>
66
+ </div>
67
+ );
68
+ }
69
+ ```
70
+
71
+ This sketch is illustrative; the layout tests exercise the public React boundary
72
+ and pane registration in [`test/tiling.test.tsx`](test/tiling.test.tsx).
73
+
74
+ ## Core concepts
75
+
76
+ `MaosaicoRoot` owns layout state for one root. Pass its current container size
77
+ as `bounds`. Without `tiling`, panes use floating frames. With `tiling`, the
78
+ first pane is the main pane and the others share the remaining space. A
79
+ preferred `mainSize` is measured in pixels along `axis` (`"x"` by default). The
80
+ layout clamps frames to the container and respects the minima supplied by each
81
+ pane.
82
+
83
+ `useMaosaicoItem` registers an ID, starting frame, and minimum size. The
84
+ returned binding provides the current frame, visibility, active state, drag
85
+ props, and `focus`, `snap`, and `restore` actions. Keep keyed content mounted
86
+ when a pane is hidden; apply `hidden={!visible}` so hidden content leaves input
87
+ and accessibility navigation. Closing a pane means removing it from the mounted
88
+ list.
89
+
90
+ ### Shell layers
91
+
92
+ `MaosaicoLayers` renders five isolated sibling overlay roots: `chrome`,
93
+ `popover`, `dialog`, `toast`, `tooltip`. Mount it inside the shell's positioned
94
+ container, outside clipped panes. The host loads Stipe's stylesheet; Maosaico
95
+ consumes its layer tokens. Overlay roots are pointer-transparent; set
96
+ `pointerEvents: "auto"` on their interactive content.
97
+
98
+ `MaosaicoRegion` supplies the `desktop` and `windows` layers in the host's
99
+ positioned work area. It forwards ordinary div attributes and a ref, keeps the
100
+ host's positioning styles, and owns isolation and the layer token. Inside
101
+ `MaosaicoLayers`, it also registers that region for `useLayer`. A windows region
102
+ is inert while the dialog root contains painted content; closing the dialog
103
+ releases it without remounting windows or desktop content.
104
+
105
+ `useLayer("popover")` returns the nearest provider's portal container. It is
106
+ `null` until that root mounts, and during server rendering. Outside a provider
107
+ it returns `document.body`, so standalone overlays keep an ordinary portal
108
+ container. Pass the container to the maintained overlay primitive, or use React
109
+ DOM's `createPortal`. Choosing a layer does not implement positioning,
110
+ light-dismiss, focus trapping or iframe occlusion.
111
+
112
+ Place window content in `<MaosaicoRegion layer="windows">` and merge the
113
+ binding's `stackingStyle` into each window's style. Its rank is dense, starting
114
+ at zero, and follows the layout owner's focus order.
115
+ `useMaosaicoLayout().stackingStyle` ranks divider and snap decorations directly
116
+ above the windows inside the same region. Arbitrarily many ranks stay below
117
+ `chrome` because the windows root owns their stacking context. Each shell's
118
+ roots and window order are independent; portaled content stays mounted when
119
+ other layer roots change.
120
+
121
+ `useMaosaicoLayout` provides focus and swap commands, zoom and restore, tile
122
+ order, tab groups, divider interaction, and pending snap information. Pass a
123
+ `containerRef` for edge snapping relative to a positioned workspace, such as a
124
+ workbench below a toolbar.
125
+
126
+ ## API reference
127
+
128
+ The package has no generated API page. See the public
129
+ [exports and types](src/index.ts); implementation details such as state
130
+ collections remain private.
131
+
132
+ ## Examples and checks
133
+
134
+ The
135
+ [Layouts / Automatic Tiling in the shared workbench](../caps/workbench/maosaico.stories.tsx)
136
+ demonstrates tiling, tabs, divider resizing, floating panes, zoom, focus, and
137
+ swapping. Run the workbench from the repository root with:
138
+
139
+ ```sh
140
+ pnpm workbench:serve
141
+ ```
142
+
143
+ The focused tests cover layout, tiling, pointer contracts, and the public
144
+ boundary:
145
+
146
+ ```sh
147
+ pnpm --filter @fungi.computer/maosaico test
148
+ ```
149
+
150
+ The browser proof also mounts 33 windows through the public bindings, checks
151
+ that chrome receives real pointer clicks before and after raising a window, and
152
+ consumes Stipe's actual CSS rather than reproducing its numbers.
153
+
154
+ The browser pointer proof is available separately with
155
+ `pnpm --filter @fungi.computer/maosaico browser:contract`; it does not prove a
156
+ mounted Tourtois App.
157
+
158
+ ## Limits and errors
159
+
160
+ Maosaico stores layout state only for the lifetime of a mounted root. It does
161
+ not persist frames, provide App identity, own keyboard policy, or move/focus the
162
+ caller's content. The host provides bounds, minima, controls, theme, and
163
+ animation. Resize gestures and tiled mode do not offer snap targets.
164
+
165
+ Bounds, minima, and size preferences are layout measurements and must be finite
166
+ and nonnegative. Maosaico adapts the MaoMaoLabs core gesture and snap behavior;
167
+ see [`LICENSE`](LICENSE) for its MIT license terms and attribution.
168
+
169
+ ## Contribute and license
170
+
171
+ Maosaico is MIT licensed; see [`LICENSE`](LICENSE).
172
+
173
+ `MaosaicoLayers onDismiss` reports host-window blur and focus entering the
174
+ windows region. Compose it with Caps `OverlayRoot subscribeDismiss` to retire
175
+ transient popovers and tooltips together; blocking dialogs and toasts stay live.
176
+
177
+ ### Controlled bento grids
178
+
179
+ `arrangeBento` reflows caller-owned `{ id, col, row, w, h }` placements into 1,
180
+ 2 or 4 columns. Spans clamp to the current grid; collisions push occupants down
181
+ and vertical gaps compact deterministically. IDs are unique within a grid.
182
+ `applyBentoOps` accepts move and resize intents without mutating the input.
183
+ `bentoFrames` maps accepted cells to container-relative pixel frames. Every
184
+ accepted move or resize compacts all tiles to an `arrangeBento` fixpoint;
185
+ downward moves pass the intersecting occupant. Cells and their extents must fit
186
+ finite safe integers; duplicate IDs throw. There is no engine product quota on
187
+ tile count or size.
188
+
189
+ `useMaosaicoBento` takes those placements, measured `width`, `columns`,
190
+ `rowHeight`, `gap` and an `onChange(ops)` callback. Optional `rowHeights`
191
+ provide measured pixel heights for individual rows; missing rows use
192
+ `rowHeight`. Frames and pointer snapping share those row boundaries. Changing
193
+ measurements cancels an in-flight gesture; equal fresh arrays preserve it. These
194
+ measurements never change the integer placements. Its `previewId` names only the
195
+ live gesture tile. Its `frames` include the live pointer preview. Spread
196
+ `bind(id, "move")` or `bind(id, "resize")` onto a focusable handle with
197
+ `touchAction: "none"`. Arrow keys propose one cell; Escape, lost capture,
198
+ disabling, unmount and host geometry changes cancel a pointer gesture. A
199
+ completed drop proposes operations through `onChange`; the host accepts them
200
+ with `applyBentoOps` and supplies the new placements.
201
+
202
+ The host chooses breakpoints and persists its own placements. Reflow does not
203
+ rewrite storage, and bento does not change `MaosaicoRoot` window behavior. The
204
+ [controlled bento story](../caps/workbench/bento.stories.tsx) composes Caps
205
+ SectionCard and Button. The [browser contract](test/browser/bento.proof.tsx)
206
+ exercises real captured-pointer move, resize, cancellation and keyboard input.
207
+
208
+ ## Native hosts
209
+
210
+ `@fungi.computer/maosaico/headless` exports `createMaosaicoState` and
211
+ `arrangeTiles` without the React renderer. The native TUI shell uses the same
212
+ focus, grouping, divider and zoom state in terminal cells. Browser bento and
213
+ window geometry keep their existing entrypoints and rules.
214
+
215
+ ## Packed consumer readiness
216
+
217
+ Run `pnpm run build`, `pnpm run pack` and `pnpm run stranger` in this package.
218
+ The maintained stranger proof installs only package tarballs into a fresh npm
219
+ consumer, checks the exact shipped files, runs the public runtime fixture and
220
+ compiles every JavaScript entrypoint with strict NodeNext and no library-check
221
+ skip. This qualification does not publish npm releases or push a repository
222
+ split.
223
+
224
+ See [LICENSE](LICENSE) for the license and the package manifest for its public
225
+ repository destination.
@@ -0,0 +1,16 @@
1
+ import type { DOMAttributes } from "react";
2
+ import { bentoFrames } from "./bento.js";
3
+ import type { BentoGeometry, BentoOp, BentoPlacement } from "./bento.js";
4
+ export type BentoOptions = BentoGeometry & {
5
+ placements: readonly BentoPlacement[];
6
+ onChange: (ops: readonly BentoOp[]) => void;
7
+ enabled?: boolean;
8
+ };
9
+ export type BentoBinding = {
10
+ frames: ReturnType<typeof bentoFrames>;
11
+ preview: boolean;
12
+ previewId: string | null;
13
+ bind: (id: string, kind: "move" | "resize") => DOMAttributes<HTMLElement>;
14
+ };
15
+ /** Captured, cell-snapped previews. Only a completed gesture proposes host-owned changes. */
16
+ export declare function useMaosaicoBento(options: BentoOptions): BentoBinding;
@@ -0,0 +1,120 @@
1
+ import { useRef, useState } from "react";
2
+ import { applyBentoOps, resolveBento, bentoFrames, bentoRowAt, bentoRowOffset, } from "./bento.js";
3
+ import { usePointerDrag } from "./pointer.js";
4
+ function intent(item, kind, dx, dy) {
5
+ return kind === "move"
6
+ ? { kind, id: item.id, col: item.col + dx, row: item.row + dy }
7
+ : { kind, id: item.id, w: item.w + dx, h: item.h + dy };
8
+ }
9
+ function sameGeometry(a, b) {
10
+ return (a.columns === b.columns &&
11
+ a.width === b.width &&
12
+ a.rowHeight === b.rowHeight &&
13
+ a.gap === b.gap &&
14
+ (a.rowHeights?.length ?? 0) === (b.rowHeights?.length ?? 0) &&
15
+ (a.rowHeights ?? []).every((height, index) => height === b.rowHeights?.[index]));
16
+ }
17
+ function samePlacements(a, b) {
18
+ return (a.length === b.length &&
19
+ a.every((item, index) => {
20
+ const other = b[index];
21
+ return (item.id === other?.id &&
22
+ item.col === other.col &&
23
+ item.row === other.row &&
24
+ item.w === other.w &&
25
+ item.h === other.h);
26
+ }));
27
+ }
28
+ /** Captured, cell-snapped previews. Only a completed gesture proposes host-owned changes. */
29
+ export function useMaosaicoBento(options) {
30
+ const [cancellation, setCancellation] = useState(() => new AbortController());
31
+ const latest = useRef(options);
32
+ latest.current = options;
33
+ const selected = useRef(null);
34
+ const gesture = useRef(null);
35
+ const [preview, setPreview] = useState(null);
36
+ const current = gesture.current;
37
+ const stale = current !== null &&
38
+ (!samePlacements(current.source.placements, options.placements) ||
39
+ !sameGeometry(current.source, options));
40
+ const pointer = usePointerDrag({
41
+ begin: () => {
42
+ const selection = selected.current;
43
+ const source = latest.current;
44
+ const item = resolveBento(source.placements, source.columns).find((tile) => tile.id === selection?.id);
45
+ if (!selection || !item || source.width <= 0 || source.rowHeight <= 0)
46
+ return false;
47
+ gesture.current = {
48
+ source,
49
+ item,
50
+ kind: selection.kind,
51
+ dx: 0,
52
+ dy: 0,
53
+ op: null,
54
+ };
55
+ return true;
56
+ },
57
+ move: (dx, dy) => {
58
+ const active = gesture.current;
59
+ if (!active)
60
+ return;
61
+ active.dx += dx;
62
+ active.dy += dy;
63
+ const { columns, width, gap } = active.source;
64
+ const anchor = active.item.row + (active.kind === "resize" ? active.item.h : 0);
65
+ const stride = (width + gap) / columns;
66
+ active.op = intent(active.item, active.kind, Math.round(active.dx / stride), bentoRowAt(bentoRowOffset(anchor, active.source) + active.dy, active.source) - anchor);
67
+ setPreview(applyBentoOps(active.source.placements, [active.op], columns));
68
+ },
69
+ end: (cancel) => {
70
+ const active = gesture.current;
71
+ gesture.current = null;
72
+ setPreview(null);
73
+ if (!cancel &&
74
+ active?.op &&
75
+ samePlacements(active.source.placements, latest.current.placements) &&
76
+ sameGeometry(active.source, latest.current)) {
77
+ latest.current.onChange([active.op]);
78
+ }
79
+ },
80
+ }, options.enabled !== false && !stale, cancellation.signal);
81
+ return {
82
+ previewId: stale || preview === null ? null : (current?.item.id ?? null),
83
+ frames: bentoFrames(stale ? options.placements : (preview ?? options.placements), options),
84
+ preview: !stale && preview !== null,
85
+ bind: (id, kind) => ({
86
+ ...pointer,
87
+ onPointerDown: (event) => {
88
+ selected.current = { id, kind };
89
+ pointer.onPointerDown?.(event);
90
+ },
91
+ onKeyDown: (event) => {
92
+ if (event.key === "Escape" && gesture.current) {
93
+ event.preventDefault();
94
+ event.stopPropagation();
95
+ cancellation.abort();
96
+ setCancellation(new AbortController());
97
+ return;
98
+ }
99
+ if (event.defaultPrevented ||
100
+ gesture.current ||
101
+ options.enabled === false)
102
+ return;
103
+ const delta = event.key === "ArrowLeft"
104
+ ? [-1, 0]
105
+ : event.key === "ArrowRight"
106
+ ? [1, 0]
107
+ : event.key === "ArrowUp"
108
+ ? [0, -1]
109
+ : event.key === "ArrowDown"
110
+ ? [0, 1]
111
+ : null;
112
+ const item = resolveBento(options.placements, options.columns).find((tile) => tile.id === id);
113
+ if (!delta || !item)
114
+ return;
115
+ event.preventDefault();
116
+ options.onChange([intent(item, kind, delta[0] ?? 0, delta[1] ?? 0)]);
117
+ },
118
+ }),
119
+ };
120
+ }
@@ -0,0 +1,43 @@
1
+ import type { Frame } from "./layout.js";
2
+ /** Host-selected responsive grid; Maosaico does not select breakpoints. */
3
+ export type BentoColumns = 1 | 2 | 4;
4
+ /** A caller-owned tile in integer grid cells. IDs are unique within a grid. */
5
+ export type BentoPlacement = Readonly<{
6
+ id: string;
7
+ col: number;
8
+ row: number;
9
+ w: number;
10
+ h: number;
11
+ }>;
12
+ /** Intent proposed by the interaction binding and accepted by the host. */
13
+ export type BentoOp = Readonly<{
14
+ kind: "move";
15
+ id: string;
16
+ col: number;
17
+ row: number;
18
+ }> | Readonly<{
19
+ kind: "resize";
20
+ id: string;
21
+ w: number;
22
+ h: number;
23
+ }>;
24
+ /** Container-relative pixel geometry supplied by the measured host. */
25
+ export type BentoGeometry = Readonly<{
26
+ columns: BentoColumns;
27
+ width: number;
28
+ rowHeight: number;
29
+ /** Measured row heights; omitted rows retain rowHeight. Cells stay unchanged. */
30
+ rowHeights?: readonly number[] | undefined;
31
+ gap: number;
32
+ }>;
33
+ export declare function resolveBento(placements: readonly BentoPlacement[], columns: BentoColumns, priority?: string): readonly BentoPlacement[];
34
+ /** Reflow spans and compact vertical gaps while retaining stable tile identity/order. */
35
+ export declare function arrangeBento(placements: readonly BentoPlacement[], columns: BentoColumns): readonly BentoPlacement[];
36
+ /** Apply intents in order, reorder downward past occupants, then compact every tile. */
37
+ export declare function applyBentoOps(placements: readonly BentoPlacement[], ops: readonly BentoOp[], columns: BentoColumns): readonly BentoPlacement[];
38
+ /** Pixel origin of an integer row, shared by frames and pointer snapping. */
39
+ export declare function bentoRowOffset(row: number, geometry: BentoGeometry): number;
40
+ /** Nearest row boundary for a pointer; remaining rows use the uniform stride. */
41
+ export declare function bentoRowAt(y: number, geometry: BentoGeometry): number;
42
+ /** Map cells to frames; content and scrolling remain with the host. */
43
+ export declare function bentoFrames(placements: readonly BentoPlacement[], geometry: BentoGeometry): ReadonlyMap<string, Frame>;
package/dist/bento.js ADDED
@@ -0,0 +1,165 @@
1
+ const overlaps = (a, b) => a.col < b.col + b.w &&
2
+ a.col + a.w > b.col &&
3
+ a.row < b.row + b.h &&
4
+ a.row + a.h > b.row;
5
+ function cell(value) {
6
+ const rounded = Math.round(value);
7
+ if (!Number.isFinite(value) || !Number.isSafeInteger(rounded))
8
+ throw new RangeError("Bento cells must be finite safe integers");
9
+ return rounded;
10
+ }
11
+ const normalize = (item, columns) => {
12
+ const w = Math.max(1, Math.min(columns, cell(item.w)));
13
+ return {
14
+ ...item,
15
+ w,
16
+ h: Math.max(1, cell(item.h)),
17
+ col: Math.max(0, Math.min(columns - w, cell(item.col))),
18
+ row: Math.max(0, cell(item.row)),
19
+ };
20
+ };
21
+ export function resolveBento(placements, columns, priority) {
22
+ if (new Set(placements.map((item) => item.id)).size !== placements.length)
23
+ throw new RangeError("Bento IDs must be unique");
24
+ const ordered = placements
25
+ .map((item) => normalize(item, columns))
26
+ .sort((a, b) => Number(b.id === priority) - Number(a.id === priority) ||
27
+ a.row - b.row ||
28
+ a.col - b.col);
29
+ const pushed = [];
30
+ for (const item of ordered) {
31
+ let next = item;
32
+ // Each jump passes at least one occupant; work is bounded by the tile count,
33
+ // rather than by a persisted row number.
34
+ for (let attempt = 0; attempt <= pushed.length; attempt += 1) {
35
+ const collisions = pushed.filter((other) => overlaps(next, other));
36
+ if (collisions.length === 0)
37
+ break;
38
+ next = {
39
+ ...next,
40
+ row: Math.max(...collisions.map((other) => other.row + other.h)),
41
+ };
42
+ }
43
+ if (!Number.isSafeInteger(next.row + next.h))
44
+ throw new RangeError("Bento extent exceeds safe integer cells");
45
+ pushed.push(next);
46
+ }
47
+ const byId = new Map(pushed.map((item) => [item.id, item]));
48
+ return placements.map((item) => byId.get(item.id) ?? item);
49
+ }
50
+ function settle(placements, columns, priority) {
51
+ const pushed = resolveBento(placements, columns, priority);
52
+ const chosen = pushed.find((item) => item.id === priority);
53
+ const packed = chosen ? [chosen] : [];
54
+ for (const item of [...pushed].sort((a, b) => a.row - b.row || a.col - b.col)) {
55
+ if (item.id === priority)
56
+ continue;
57
+ const rows = [
58
+ ...new Set([0, ...packed.map((other) => other.row + other.h)]),
59
+ ]
60
+ .filter((row) => row <= item.row)
61
+ .sort((a, b) => a - b);
62
+ const row = rows.find((candidate) => !packed.some((other) => overlaps({ ...item, row: candidate }, other)));
63
+ packed.push({ ...item, row: row ?? item.row });
64
+ }
65
+ if (chosen) {
66
+ const others = packed.filter((item) => item.id !== chosen.id);
67
+ const rows = [...new Set([0, ...others.map((item) => item.row + item.h)])]
68
+ .filter((row) => row <= chosen.row)
69
+ .sort((a, b) => a - b);
70
+ const row = rows.find((candidate) => !others.some((item) => overlaps({ ...chosen, row: candidate }, item)));
71
+ return settle(placements.map((item) => item.id === chosen.id
72
+ ? { ...chosen, row: row ?? chosen.row }
73
+ : (others.find((other) => other.id === item.id) ?? item)), columns);
74
+ }
75
+ const byId = new Map(packed.map((item) => [item.id, item]));
76
+ return placements.map((item) => byId.get(item.id) ?? item);
77
+ }
78
+ /** Reflow spans and compact vertical gaps while retaining stable tile identity/order. */
79
+ export function arrangeBento(placements, columns) {
80
+ return settle(placements, columns);
81
+ }
82
+ function downwardTarget(item, op, placements, columns) {
83
+ const target = normalize({ ...item, col: op.col, row: op.row }, columns);
84
+ if (target.row <= item.row)
85
+ return target;
86
+ const occupants = placements.filter((other) => other.id !== item.id && overlaps(target, other));
87
+ return {
88
+ ...target,
89
+ row: Math.max(target.row, ...occupants.map((other) => other.row + other.h - item.h)),
90
+ };
91
+ }
92
+ /** Apply intents in order, reorder downward past occupants, then compact every tile. */
93
+ export function applyBentoOps(placements, ops, columns) {
94
+ let result = resolveBento(placements, columns);
95
+ for (const op of ops) {
96
+ if (op.kind === "move") {
97
+ const item = result.find((item) => item.id === op.id);
98
+ const target = item
99
+ ? normalize({ ...item, col: op.col, row: op.row }, columns)
100
+ : undefined;
101
+ const occupant = target && item && target.col !== item.col
102
+ ? result.find((other) => other.id !== item.id &&
103
+ overlaps(target, other) &&
104
+ other.row === target.row &&
105
+ other.w === item.w &&
106
+ other.h === item.h)
107
+ : undefined;
108
+ if (item && target && occupant) {
109
+ result = settle(result.map((other) => other.id === item.id
110
+ ? { ...target, col: occupant.col, row: occupant.row }
111
+ : other.id === occupant.id
112
+ ? { ...other, col: item.col, row: item.row }
113
+ : other), columns);
114
+ continue;
115
+ }
116
+ }
117
+ result = settle(result.map((item) => item.id !== op.id
118
+ ? item
119
+ : op.kind === "move"
120
+ ? downwardTarget(item, op, result, columns)
121
+ : { ...item, w: op.w, h: op.h }), columns, op.id);
122
+ }
123
+ return result;
124
+ }
125
+ /** Pixel origin of an integer row, shared by frames and pointer snapping. */
126
+ export function bentoRowOffset(row, geometry) {
127
+ const measured = geometry.rowHeights ?? [];
128
+ const count = Math.min(row, measured.length);
129
+ return (measured
130
+ .slice(0, count)
131
+ .reduce((sum, height) => sum + height + geometry.gap, 0) +
132
+ (row - count) * (geometry.rowHeight + geometry.gap));
133
+ }
134
+ /** Nearest row boundary for a pointer; remaining rows use the uniform stride. */
135
+ export function bentoRowAt(y, geometry) {
136
+ let offset = 0;
137
+ const measured = geometry.rowHeights ?? [];
138
+ for (let row = 0; row < measured.length; row += 1) {
139
+ const stride = (measured[row] ?? geometry.rowHeight) + geometry.gap;
140
+ if (y < offset + stride / 2)
141
+ return row;
142
+ offset += stride;
143
+ }
144
+ const stride = geometry.rowHeight + geometry.gap;
145
+ return (measured.length +
146
+ (stride > 0 ? Math.max(0, Math.round((y - offset) / stride)) : 0));
147
+ }
148
+ /** Map cells to frames; content and scrolling remain with the host. */
149
+ export function bentoFrames(placements, geometry) {
150
+ const { columns, width, rowHeight, gap } = geometry;
151
+ if (![width, rowHeight, gap, ...(geometry.rowHeights ?? [])].every((value) => Number.isFinite(value) && value >= 0))
152
+ throw new RangeError("Bento pixel geometry must be finite and nonnegative");
153
+ const cellWidth = Math.max(0, width - gap * (columns - 1)) / columns;
154
+ return new Map(resolveBento(placements, columns).map((item) => [
155
+ item.id,
156
+ {
157
+ x: item.col * (cellWidth + gap),
158
+ y: bentoRowOffset(item.row, geometry),
159
+ width: item.w * cellWidth + (item.w - 1) * gap,
160
+ height: bentoRowOffset(item.row + item.h, geometry) -
161
+ bentoRowOffset(item.row, geometry) -
162
+ gap,
163
+ },
164
+ ]));
165
+ }
@@ -0,0 +1,5 @@
1
+ /** Layout state shared by browser and native hosts; renderers own containers. */
2
+ export { createMaosaicoState } from "./layout.js";
3
+ export type { MaosaicoState, Frame, Bounds, Minima } from "./layout.js";
4
+ export { arrangeTiles } from "./tiling.js";
5
+ export type { Arrangement, TilingOptions } from "./tiling.js";
@@ -0,0 +1,3 @@
1
+ /** Layout state shared by browser and native hosts; renderers own containers. */
2
+ export { createMaosaicoState } from "./layout.js";
3
+ export { arrangeTiles } from "./tiling.js";
@@ -0,0 +1,11 @@
1
+ export { MaosaicoRoot, useMaosaicoItem, useMaosaicoLayout, } from "./provider.js";
2
+ export { MaosaicoLayers, MaosaicoRegion, useLayer } from "./layers.js";
3
+ export type { MaosaicoLayer } from "./layers.js";
4
+ export type { TilingOptions } from "./tiling.js";
5
+ export type { MaosaicoItem, MaosaicoItemOptions, MaosaicoItemBinding, } from "./provider.js";
6
+ export type { Point, Size, Frame, Bounds, Minima, SnapSide } from "./layout.js";
7
+ export { usePointerDrag } from "./pointer.js";
8
+ export { arrangeBento, applyBentoOps, bentoFrames } from "./bento.js";
9
+ export type { BentoColumns, BentoPlacement, BentoOp, BentoGeometry, } from "./bento.js";
10
+ export { useMaosaicoBento } from "./bento-binding.js";
11
+ export type { BentoOptions, BentoBinding } from "./bento-binding.js";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { MaosaicoRoot, useMaosaicoItem, useMaosaicoLayout, } from "./provider.js";
2
+ export { MaosaicoLayers, MaosaicoRegion, useLayer } from "./layers.js";
3
+ export { usePointerDrag } from "./pointer.js";
4
+ export { arrangeBento, applyBentoOps, bentoFrames } from "./bento.js";
5
+ export { useMaosaicoBento } from "./bento-binding.js";
@@ -0,0 +1,19 @@
1
+ import { type ReactNode, type ReactElement, type HTMLAttributes } from "react";
2
+ /** Shell paint order. Window ranks are local to the windows root. */
3
+ export type MaosaicoLayer = "desktop" | "windows" | "chrome" | "popover" | "dialog" | "toast" | "tooltip";
4
+ /**
5
+ * Mount inside the shell's positioned container, outside clipped panes.
6
+ * Roots are pointer-transparent; interactive portal content enables pointers.
7
+ * The host supplies Stipe's stylesheet and retains content and focus ownership.
8
+ */
9
+ export declare function MaosaicoLayers({ children, onDismiss, }: {
10
+ children?: ReactNode;
11
+ /** Light-dismiss transient overlays on host blur or focus entering a window. */
12
+ onDismiss?: () => void;
13
+ }): ReactElement;
14
+ /** Null until the shell root mounts (also during SSR); body outside a shell. */
15
+ export declare function useLayer(layer: MaosaicoLayer): HTMLElement | null;
16
+ /** The host positions the region; Maosaico owns its isolated layer and modality. */
17
+ export declare const MaosaicoRegion: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & {
18
+ layer: "desktop" | "windows";
19
+ } & import("react").RefAttributes<HTMLDivElement>>;