@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.
@@ -0,0 +1,163 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useSyncExternalStore, } from "react";
3
+ import { createMaosaicoState, clampFrame, } from "./layout.js";
4
+ import { arrangeTiles, } from "./tiling.js";
5
+ import { usePointerDrag } from "./pointer.js";
6
+ import { detectSnap } from "./snap.js";
7
+ const Context = createContext(null);
8
+ /** Omit tiling to retain floating frames. Width is a preference, in pixels. */
9
+ export function MaosaicoRoot({ bounds, tiling, containerRef, children, }) {
10
+ const state = useMemo(createMaosaicoState, []);
11
+ useSyncExternalStore(state.subscribe, () => state.version, () => state.version);
12
+ useEffect(() => () => {
13
+ state.destroy();
14
+ }, [state]);
15
+ const options = { ...tiling };
16
+ if (state.mainSize !== undefined)
17
+ options.mainSize = state.mainSize;
18
+ const arrangement = tiling
19
+ ? arrangeTiles(state.tiles, state.minima, state.order, bounds, options)
20
+ : {
21
+ frames: new Map([...state.frames]
22
+ .filter(([id]) => state.participating.has(id))
23
+ .map(([id, frame]) => [
24
+ id,
25
+ clampFrame(frame, bounds, state.minima.get(id) ?? { width: 0, height: 0 }),
26
+ ])),
27
+ tabGroups: [],
28
+ divider: null,
29
+ };
30
+ if (state.zoomed && state.participating.has(state.zoomed)) {
31
+ arrangement.frames = new Map([[state.zoomed, { x: 0, y: 0, ...bounds }]]);
32
+ arrangement.divider = null;
33
+ arrangement.tabGroups = [];
34
+ }
35
+ return (_jsx(Context.Provider, { value: {
36
+ state,
37
+ bounds,
38
+ tiled: tiling !== undefined,
39
+ arrangement,
40
+ containerRef,
41
+ }, children: children }));
42
+ }
43
+ function useRoot() {
44
+ const context = useContext(Context);
45
+ if (!context)
46
+ throw new Error("Maosaico hooks must be used within MaosaicoRoot");
47
+ return context;
48
+ }
49
+ /** Layout commands keep content and DOM ownership with the caller. */
50
+ export function useMaosaicoLayout() {
51
+ const { state, arrangement, tiled } = useRoot();
52
+ const order = state.tiles;
53
+ const start = useRef({ pointer: 0, position: 0 });
54
+ const divider = arrangement.divider;
55
+ const dividerProps = usePointerDrag({
56
+ begin: (event) => {
57
+ if (!divider || !state.beginDivider(event.pointerId))
58
+ return false;
59
+ start.current = {
60
+ pointer: event.pointerId,
61
+ position: divider.position,
62
+ };
63
+ return true;
64
+ },
65
+ move: (dx, dy) => {
66
+ if (!divider)
67
+ return;
68
+ start.current.position = Math.min(divider.max, Math.max(divider.min, start.current.position + (divider.axis === "x" ? dx : dy)));
69
+ state.resizeMain(start.current.position);
70
+ },
71
+ end: (cancel) => {
72
+ state.finishDivider(start.current.pointer, cancel);
73
+ },
74
+ }, divider !== null);
75
+ return {
76
+ /** Decorations rank immediately above every window, inside the windows region. */
77
+ stackingStyle: { zIndex: state.order.length },
78
+ mode: tiled ? "tiled" : "floating",
79
+ order,
80
+ interacting: state.gesture !== null,
81
+ snapPreview: !tiled && !state.zoomed && state.gesture?.kind === "frame"
82
+ ? (state.gesture.snap ?? null)
83
+ : null,
84
+ activeId: state.order.at(-1) ?? null,
85
+ zoomedId: state.zoomed,
86
+ tabGroups: arrangement.tabGroups,
87
+ divider: divider && { ...divider, props: dividerProps },
88
+ focus: state.focus,
89
+ swap: state.swap,
90
+ zoom: state.zoom,
91
+ resizeMain: (size) => {
92
+ if (divider)
93
+ state.resizeMain(Math.min(divider.max, Math.max(divider.min, size)));
94
+ },
95
+ };
96
+ }
97
+ export function useMaosaicoItem(item, options = {}) {
98
+ const { state, bounds, tiled, arrangement, containerRef } = useRoot();
99
+ // Frame seeds a mounted identity; later layout changes use commands, not remounts.
100
+ useEffect(() => {
101
+ state.register(item.id, item.frame, item.minima);
102
+ return () => {
103
+ state.unregister(item.id);
104
+ };
105
+ }, [state, item.id]);
106
+ useEffect(() => {
107
+ state.setMinima(item.id, item.minima);
108
+ }, [state, item.id, item.minima.width, item.minima.height]);
109
+ const participating = options.participating ?? true;
110
+ useLayoutEffect(() => {
111
+ state.setParticipation(item.id, participating);
112
+ }, [state, item.id, participating]);
113
+ const pointer = useRef(0);
114
+ const props = usePointerDrag({
115
+ begin: (event) => {
116
+ if (!state.frames.has(item.id) || state.gesture)
117
+ return false;
118
+ pointer.current = event.pointerId;
119
+ state.begin(item.id, event.pointerId);
120
+ return true;
121
+ },
122
+ move: (dx, dy, event) => {
123
+ const container = containerRef?.current?.getBoundingClientRect();
124
+ const side = container && options.kind !== "resize"
125
+ ? detectSnap({
126
+ x: event.clientX - container.left,
127
+ y: event.clientY - container.top,
128
+ }, bounds)
129
+ : null;
130
+ const frame = clampFrame(state.frames.get(item.id) ?? item.frame, bounds, item.minima);
131
+ state.move(item.id, pointer.current, options.kind === "resize"
132
+ ? { ...frame, width: frame.width + dx, height: frame.height + dy }
133
+ : { ...frame, x: frame.x + dx, y: frame.y + dy }, bounds, item.minima, side);
134
+ },
135
+ end: (cancel) => {
136
+ if (cancel)
137
+ state.cancel(item.id, pointer.current);
138
+ else
139
+ state.end(item.id, pointer.current);
140
+ },
141
+ }, participating && !tiled && !state.zoomed);
142
+ return {
143
+ frame: arrangement.frames.get(item.id) ??
144
+ state.frames.get(item.id) ??
145
+ item.frame,
146
+ stackingStyle: { zIndex: Math.max(0, state.order.indexOf(item.id)) },
147
+ visible: arrangement.frames.has(item.id),
148
+ active: participating && state.order.at(-1) === item.id,
149
+ props,
150
+ focus: () => {
151
+ if (participating)
152
+ state.focus(item.id);
153
+ },
154
+ snap: (side) => {
155
+ if (participating && !tiled)
156
+ state.snap(item.id, side, bounds, item.minima);
157
+ },
158
+ restore: () => {
159
+ if (participating && !tiled)
160
+ state.restoreFrame(item.id, bounds, item.minima);
161
+ },
162
+ };
163
+ }
package/dist/snap.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { Bounds, Point, SnapSide } from "./layout.js";
2
+ /** MaoMao v1.1.0 edge zones, measured in the owning container rather than viewport. */
3
+ export declare function detectSnap(point: Point, bounds: Bounds): SnapSide | null;
package/dist/snap.js ADDED
@@ -0,0 +1,19 @@
1
+ /** MaoMao v1.1.0 edge zones, measured in the owning container rather than viewport. */
2
+ export function detectSnap(point, bounds) {
3
+ const threshold = 20;
4
+ if (point.x < threshold) {
5
+ if (point.y < threshold)
6
+ return "top-left";
7
+ if (point.y > bounds.height - threshold)
8
+ return "bottom-left";
9
+ return "left";
10
+ }
11
+ if (point.x > bounds.width - threshold) {
12
+ if (point.y < threshold)
13
+ return "top-right";
14
+ if (point.y > bounds.height - threshold)
15
+ return "bottom-right";
16
+ return "right";
17
+ }
18
+ return null;
19
+ }
@@ -0,0 +1,19 @@
1
+ import type { Bounds, Frame, Minima } from "./layout.js";
2
+ export type TilingOptions = {
3
+ axis?: "x" | "y";
4
+ mainSize?: number;
5
+ gap?: number;
6
+ };
7
+ export type Divider = {
8
+ axis: "x" | "y";
9
+ position: number;
10
+ min: number;
11
+ max: number;
12
+ };
13
+ export type Arrangement = {
14
+ frames: Map<string, Frame>;
15
+ tabGroups: string[][];
16
+ divider: Divider | null;
17
+ };
18
+ /** Main + companions. The caller renders tabs for groups that cannot fit. */
19
+ export declare function arrangeTiles(order: readonly string[], minima: ReadonlyMap<string, Minima>, recent: readonly string[], bounds: Bounds, options: TilingOptions): Arrangement;
package/dist/tiling.js ADDED
@@ -0,0 +1,78 @@
1
+ /** Main + companions. The caller renders tabs for groups that cannot fit. */
2
+ export function arrangeTiles(order, minima, recent, bounds, options) {
3
+ if (options.axis === "y") {
4
+ const transverse = arrangeAlongX(order, new Map([...minima].map(([id, size]) => [
5
+ id,
6
+ { width: size.height, height: size.width },
7
+ ])), recent, { width: bounds.height, height: bounds.width }, options);
8
+ return {
9
+ frames: new Map([...transverse.frames].map(([id, frame]) => [
10
+ id,
11
+ {
12
+ x: frame.y,
13
+ y: frame.x,
14
+ width: frame.height,
15
+ height: frame.width,
16
+ },
17
+ ])),
18
+ tabGroups: transverse.tabGroups,
19
+ divider: transverse.divider && {
20
+ ...transverse.divider,
21
+ axis: "y",
22
+ },
23
+ };
24
+ }
25
+ return arrangeAlongX(order, minima, recent, bounds, options);
26
+ }
27
+ function arrangeAlongX(order, minima, recent, bounds, options) {
28
+ const frames = new Map();
29
+ const tabGroups = [];
30
+ const whole = { x: 0, y: 0, ...bounds };
31
+ const [main, ...companions] = order;
32
+ const tabs = (ids, frame) => {
33
+ const selected = [...recent].reverse().find((id) => ids.includes(id)) ?? ids[0];
34
+ if (selected)
35
+ frames.set(selected, frame);
36
+ if (ids.length > 1)
37
+ tabGroups.push(ids);
38
+ };
39
+ if (!main)
40
+ return { frames, tabGroups, divider: null };
41
+ if (!companions.length) {
42
+ frames.set(main, whole);
43
+ return { frames, tabGroups, divider: null };
44
+ }
45
+ const gap = Math.max(0, options.gap ?? 8);
46
+ const mainMin = minima.get(main)?.width ?? 0;
47
+ const stackMin = Math.max(...companions.map((id) => minima.get(id)?.width ?? 0));
48
+ const max = bounds.width - gap - stackMin;
49
+ if (mainMin > max) {
50
+ tabs([...order], whole);
51
+ return { frames, tabGroups, divider: null };
52
+ }
53
+ const width = Math.min(max, Math.max(mainMin, options.mainSize ?? bounds.width / 2));
54
+ const divider = {
55
+ axis: "x",
56
+ position: width,
57
+ min: mainMin,
58
+ max,
59
+ };
60
+ frames.set(main, { ...whole, width });
61
+ const stack = { ...whole, x: width + gap, width: bounds.width - width - gap };
62
+ const heights = companions.map((id) => minima.get(id)?.height ?? 0);
63
+ const required = heights.reduce((sum, height) => sum + height, 0) +
64
+ gap * (companions.length - 1);
65
+ if (required > bounds.height) {
66
+ tabs(companions, stack);
67
+ }
68
+ else {
69
+ const spare = (bounds.height - required) / companions.length;
70
+ let y = 0;
71
+ for (const id of companions) {
72
+ const height = (minima.get(id)?.height ?? 0) + spare;
73
+ frames.set(id, { ...stack, y, height });
74
+ y += height + gap;
75
+ }
76
+ }
77
+ return { frames, tabGroups, divider };
78
+ }
package/package.json CHANGED
@@ -1,6 +1,40 @@
1
1
  {
2
2
  "name": "@fungi.computer/maosaico",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.1",
4
+ "author": "Levi <levi@technomancy.dev>",
5
+ "private": false,
6
+ "description": "container-relative layout and pointer ownership",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ },
15
+ "./bento": {
16
+ "types": "./dist/bento.d.ts",
17
+ "import": "./dist/bento.js"
18
+ },
19
+ "./headless": {
20
+ "types": "./dist/headless.d.ts",
21
+ "import": "./dist/headless.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "peerDependencies": {
30
+ "react": ">=17"
31
+ },
32
+ "dependencies": {
33
+ "jotai": "2.20.3"
34
+ },
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/fungi-computer/maosaico.git"
39
+ }
6
40
  }