@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 +21 -0
- package/README.md +224 -2
- package/dist/bento-binding.d.ts +16 -0
- package/dist/bento-binding.js +120 -0
- package/dist/bento.d.ts +43 -0
- package/dist/bento.js +165 -0
- package/dist/headless.d.ts +5 -0
- package/dist/headless.js +3 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +5 -0
- package/dist/layers.d.ts +19 -0
- package/dist/layers.js +118 -0
- package/dist/layout.d.ts +60 -0
- package/dist/layout.js +336 -0
- package/dist/pointer.d.ts +7 -0
- package/dist/pointer.js +72 -0
- package/dist/provider.d.ts +60 -0
- package/dist/provider.js +163 -0
- package/dist/snap.d.ts +3 -0
- package/dist/snap.js +19 -0
- package/dist/tiling.d.ts +19 -0
- package/dist/tiling.js +78 -0
- package/package.json +37 -3
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
|
-
#
|
|
1
|
+
# Maosaico (`@fungi.computer/maosaico`)
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
}
|
package/dist/bento.d.ts
ADDED
|
@@ -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";
|
package/dist/headless.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -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";
|
package/dist/layers.d.ts
ADDED
|
@@ -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>>;
|