@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/dist/layers.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createContext, createElement, forwardRef, useEffect, useRef, useImperativeHandle, useCallback, useContext, useMemo, useState, } from "react";
|
|
2
|
+
const layers = [
|
|
3
|
+
"chrome",
|
|
4
|
+
"popover",
|
|
5
|
+
"dialog",
|
|
6
|
+
"toast",
|
|
7
|
+
"tooltip",
|
|
8
|
+
];
|
|
9
|
+
const Context = createContext(null);
|
|
10
|
+
/**
|
|
11
|
+
* Mount inside the shell's positioned container, outside clipped panes.
|
|
12
|
+
* Roots are pointer-transparent; interactive portal content enables pointers.
|
|
13
|
+
* The host supplies Stipe's stylesheet and retains content and focus ownership.
|
|
14
|
+
*/
|
|
15
|
+
export function MaosaicoLayers({ children, onDismiss, }) {
|
|
16
|
+
const [roots, setRoots] = useState(() => new Map());
|
|
17
|
+
const update = useCallback((name, element) => {
|
|
18
|
+
setRoots((current) => {
|
|
19
|
+
if ((current.get(name) ?? null) === element)
|
|
20
|
+
return current;
|
|
21
|
+
const next = new Map(current);
|
|
22
|
+
if (element)
|
|
23
|
+
next.set(name, element);
|
|
24
|
+
else
|
|
25
|
+
next.delete(name);
|
|
26
|
+
return next;
|
|
27
|
+
});
|
|
28
|
+
}, []);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!onDismiss)
|
|
31
|
+
return;
|
|
32
|
+
window.addEventListener("blur", onDismiss);
|
|
33
|
+
return () => {
|
|
34
|
+
window.removeEventListener("blur", onDismiss);
|
|
35
|
+
};
|
|
36
|
+
}, [onDismiss]);
|
|
37
|
+
const refs = useMemo(() => new Map(layers.map((name) => [
|
|
38
|
+
name,
|
|
39
|
+
(element) => {
|
|
40
|
+
update(name, element);
|
|
41
|
+
},
|
|
42
|
+
])), [update]);
|
|
43
|
+
return createElement(Context.Provider, {
|
|
44
|
+
value: useMemo(() => ({ roots, update, dismiss: onDismiss }), [roots, update, onDismiss]),
|
|
45
|
+
}, children, layers.map((name) => createElement("div", {
|
|
46
|
+
key: name,
|
|
47
|
+
ref: refs.get(name),
|
|
48
|
+
"data-maosaico-layer": name,
|
|
49
|
+
style: {
|
|
50
|
+
position: "absolute",
|
|
51
|
+
inset: 0,
|
|
52
|
+
isolation: "isolate",
|
|
53
|
+
pointerEvents: "none",
|
|
54
|
+
zIndex: `var(--stipe-layer-${name})`,
|
|
55
|
+
},
|
|
56
|
+
})));
|
|
57
|
+
}
|
|
58
|
+
/** Null until the shell root mounts (also during SSR); body outside a shell. */
|
|
59
|
+
export function useLayer(layer) {
|
|
60
|
+
const roots = useContext(Context);
|
|
61
|
+
if (roots !== null)
|
|
62
|
+
return roots.roots.get(layer) ?? null;
|
|
63
|
+
return Object.hasOwn(globalThis, "document") ? document.body : null;
|
|
64
|
+
}
|
|
65
|
+
/** The host positions the region; Maosaico owns its isolated layer and modality. */
|
|
66
|
+
export const MaosaicoRegion = forwardRef(function MaosaicoRegion({ layer, style, children, ...props }, forwardedRef) {
|
|
67
|
+
const context = useContext(Context);
|
|
68
|
+
const update = context?.update;
|
|
69
|
+
const dialog = context?.roots.get("dialog");
|
|
70
|
+
const [modal, setModal] = useState(false);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!dialog)
|
|
73
|
+
return;
|
|
74
|
+
const refresh = () => {
|
|
75
|
+
setModal(Array.from(dialog.querySelectorAll("*")).some((element) => {
|
|
76
|
+
const rect = element.getBoundingClientRect();
|
|
77
|
+
return rect.width > 0 && rect.height > 0;
|
|
78
|
+
}));
|
|
79
|
+
};
|
|
80
|
+
refresh();
|
|
81
|
+
const observer = new MutationObserver(refresh);
|
|
82
|
+
observer.observe(dialog, {
|
|
83
|
+
childList: true,
|
|
84
|
+
subtree: true,
|
|
85
|
+
attributes: true,
|
|
86
|
+
});
|
|
87
|
+
return () => {
|
|
88
|
+
observer.disconnect();
|
|
89
|
+
};
|
|
90
|
+
}, [dialog]);
|
|
91
|
+
const elementRef = useRef(null);
|
|
92
|
+
useImperativeHandle(forwardedRef, () => elementRef.current);
|
|
93
|
+
const ref = useCallback((element) => {
|
|
94
|
+
elementRef.current = element;
|
|
95
|
+
update?.(layer, element);
|
|
96
|
+
}, [update, layer]);
|
|
97
|
+
return createElement("div", {
|
|
98
|
+
...props,
|
|
99
|
+
ref,
|
|
100
|
+
onFocusCapture: (event) => {
|
|
101
|
+
// React bubbles focus from portaled overlays through this region; only
|
|
102
|
+
// focus that really lands in a window dismisses open overlays.
|
|
103
|
+
if (layer === "windows" &&
|
|
104
|
+
event.target instanceof Node &&
|
|
105
|
+
event.currentTarget.contains(event.target))
|
|
106
|
+
context?.dismiss?.();
|
|
107
|
+
props.onFocusCapture?.(event);
|
|
108
|
+
},
|
|
109
|
+
"data-maosaico-layer": layer,
|
|
110
|
+
inert: layer === "windows" && modal ? "" : undefined,
|
|
111
|
+
style: {
|
|
112
|
+
position: "relative",
|
|
113
|
+
...style,
|
|
114
|
+
isolation: "isolate",
|
|
115
|
+
zIndex: `var(--stipe-layer-${layer})`,
|
|
116
|
+
},
|
|
117
|
+
}, children);
|
|
118
|
+
});
|
package/dist/layout.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type Point = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
};
|
|
5
|
+
export type Size = {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
};
|
|
9
|
+
export type Frame = Point & Size;
|
|
10
|
+
export type Bounds = Size;
|
|
11
|
+
export type Minima = Size;
|
|
12
|
+
export type SnapSide = "left" | "right" | "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
|
13
|
+
export type Gesture = {
|
|
14
|
+
kind: "frame";
|
|
15
|
+
id: string;
|
|
16
|
+
pointerId: number;
|
|
17
|
+
start: Frame;
|
|
18
|
+
snap?: {
|
|
19
|
+
side: SnapSide;
|
|
20
|
+
frame: Frame;
|
|
21
|
+
} | null;
|
|
22
|
+
} | {
|
|
23
|
+
kind: "divider";
|
|
24
|
+
pointerId: number;
|
|
25
|
+
start: number | undefined;
|
|
26
|
+
};
|
|
27
|
+
export type State = {
|
|
28
|
+
frames: Map<string, Frame>;
|
|
29
|
+
order: string[];
|
|
30
|
+
tiles: string[];
|
|
31
|
+
minima: Map<string, Minima>;
|
|
32
|
+
participating: Set<string>;
|
|
33
|
+
mainSize: number | undefined;
|
|
34
|
+
zoomed: string | null;
|
|
35
|
+
gesture: Gesture | null;
|
|
36
|
+
restore: Map<string, Frame>;
|
|
37
|
+
};
|
|
38
|
+
export declare const clampFrame: (frame: Frame, bounds: Bounds, minima: Minima) => Frame;
|
|
39
|
+
export type MaosaicoState = Readonly<State> & {
|
|
40
|
+
readonly version: number;
|
|
41
|
+
subscribe: (listener: () => void) => () => void;
|
|
42
|
+
register: (id: string, frame: Frame, minima: Minima) => void;
|
|
43
|
+
setParticipation: (id: string, participating: boolean) => void;
|
|
44
|
+
setMinima: (id: string, minima: Minima) => void;
|
|
45
|
+
unregister: (id: string) => void;
|
|
46
|
+
begin: (id: string, pointerId: number) => void;
|
|
47
|
+
move: (id: string, pointerId: number, frame: Frame, bounds: Bounds, minima: Minima, snapSide?: SnapSide | null) => void;
|
|
48
|
+
end: (id: string, pointerId: number) => void;
|
|
49
|
+
cancel: (id: string, pointerId: number) => void;
|
|
50
|
+
snap: (id: string, side: SnapSide, bounds: Bounds, minima: Minima) => void;
|
|
51
|
+
restoreFrame: (id: string, bounds: Bounds, minima: Minima) => void;
|
|
52
|
+
focus: (id: string) => void;
|
|
53
|
+
swap: (first: string, second: string) => void;
|
|
54
|
+
zoom: (id: string | null) => void;
|
|
55
|
+
resizeMain: (size: number) => void;
|
|
56
|
+
beginDivider: (pointerId: number) => boolean;
|
|
57
|
+
finishDivider: (pointerId: number, cancel: boolean) => void;
|
|
58
|
+
destroy: () => void;
|
|
59
|
+
};
|
|
60
|
+
export declare const createMaosaicoState: () => MaosaicoState;
|
package/dist/layout.js
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { atom, createStore } from "jotai/vanilla";
|
|
2
|
+
const stateAtom = atom({
|
|
3
|
+
frames: new Map(),
|
|
4
|
+
order: [],
|
|
5
|
+
tiles: [],
|
|
6
|
+
minima: new Map(),
|
|
7
|
+
participating: new Set(),
|
|
8
|
+
mainSize: undefined,
|
|
9
|
+
zoomed: null,
|
|
10
|
+
gesture: null,
|
|
11
|
+
restore: new Map(),
|
|
12
|
+
});
|
|
13
|
+
const clamp = (value, low, high) => Math.min(Math.max(value, low), high);
|
|
14
|
+
export const clampFrame = (frame, bounds, minima) => {
|
|
15
|
+
const width = clamp(frame.width, minima.width, Math.max(minima.width, bounds.width));
|
|
16
|
+
const height = clamp(frame.height, minima.height, Math.max(minima.height, bounds.height));
|
|
17
|
+
return {
|
|
18
|
+
width,
|
|
19
|
+
height,
|
|
20
|
+
x: clamp(frame.x, 0, Math.max(0, bounds.width - width)),
|
|
21
|
+
y: clamp(frame.y, 0, Math.max(0, bounds.height - height)),
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
const snapFrame = (side, bounds, minima) => {
|
|
25
|
+
const halfWidth = bounds.width / 2;
|
|
26
|
+
const halfHeight = bounds.height / 2;
|
|
27
|
+
const frame = side === "left"
|
|
28
|
+
? { x: 0, y: 0, width: halfWidth, height: bounds.height }
|
|
29
|
+
: side === "right"
|
|
30
|
+
? { x: halfWidth, y: 0, width: halfWidth, height: bounds.height }
|
|
31
|
+
: side === "top-left"
|
|
32
|
+
? { x: 0, y: 0, width: halfWidth, height: halfHeight }
|
|
33
|
+
: side === "top-right"
|
|
34
|
+
? { x: halfWidth, y: 0, width: halfWidth, height: halfHeight }
|
|
35
|
+
: side === "bottom-left"
|
|
36
|
+
? { x: 0, y: halfHeight, width: halfWidth, height: halfHeight }
|
|
37
|
+
: {
|
|
38
|
+
x: halfWidth,
|
|
39
|
+
y: halfHeight,
|
|
40
|
+
width: halfWidth,
|
|
41
|
+
height: halfHeight,
|
|
42
|
+
};
|
|
43
|
+
return clampFrame(frame, bounds, minima);
|
|
44
|
+
};
|
|
45
|
+
export const createMaosaicoState = () => {
|
|
46
|
+
const store = createStore();
|
|
47
|
+
const read = () => store.get(stateAtom);
|
|
48
|
+
let version = 0;
|
|
49
|
+
const update = (change) => {
|
|
50
|
+
version += 1;
|
|
51
|
+
store.set(stateAtom, change(read()));
|
|
52
|
+
};
|
|
53
|
+
const touch = (id, frame, minima) => {
|
|
54
|
+
update((state) => {
|
|
55
|
+
const frames = new Map(state.frames);
|
|
56
|
+
frames.set(id, frame);
|
|
57
|
+
return {
|
|
58
|
+
...state,
|
|
59
|
+
frames,
|
|
60
|
+
participating: new Set(state.participating).add(id),
|
|
61
|
+
tiles: state.tiles.includes(id) ? state.tiles : [...state.tiles, id],
|
|
62
|
+
minima: new Map(state.minima).set(id, minima),
|
|
63
|
+
order: state.order.includes(id)
|
|
64
|
+
? state.order
|
|
65
|
+
: [
|
|
66
|
+
...state.order.filter((value) => value !== state.zoomed),
|
|
67
|
+
id,
|
|
68
|
+
...(state.zoomed ? [state.zoomed] : []),
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
const registered = (id) => read().frames.has(id);
|
|
74
|
+
const ownsFrame = (id, pointerId) => {
|
|
75
|
+
const gesture = read().gesture;
|
|
76
|
+
return (gesture?.kind === "frame" &&
|
|
77
|
+
gesture.id === id &&
|
|
78
|
+
gesture.pointerId === pointerId);
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
get version() {
|
|
82
|
+
return version;
|
|
83
|
+
},
|
|
84
|
+
get frames() {
|
|
85
|
+
return read().frames;
|
|
86
|
+
},
|
|
87
|
+
get order() {
|
|
88
|
+
return read().order;
|
|
89
|
+
},
|
|
90
|
+
get tiles() {
|
|
91
|
+
return read().tiles;
|
|
92
|
+
},
|
|
93
|
+
get minima() {
|
|
94
|
+
return read().minima;
|
|
95
|
+
},
|
|
96
|
+
get participating() {
|
|
97
|
+
return read().participating;
|
|
98
|
+
},
|
|
99
|
+
get mainSize() {
|
|
100
|
+
return read().mainSize;
|
|
101
|
+
},
|
|
102
|
+
get zoomed() {
|
|
103
|
+
return read().zoomed;
|
|
104
|
+
},
|
|
105
|
+
get gesture() {
|
|
106
|
+
return read().gesture;
|
|
107
|
+
},
|
|
108
|
+
get restore() {
|
|
109
|
+
return read().restore;
|
|
110
|
+
},
|
|
111
|
+
subscribe: (listener) => store.sub(stateAtom, listener),
|
|
112
|
+
register: touch,
|
|
113
|
+
setParticipation: (id, participating) => {
|
|
114
|
+
if (!read().frames.has(id))
|
|
115
|
+
return;
|
|
116
|
+
update((state) => {
|
|
117
|
+
const current = state.participating.has(id);
|
|
118
|
+
if (current === participating)
|
|
119
|
+
return state;
|
|
120
|
+
const participants = new Set(state.participating);
|
|
121
|
+
if (participating)
|
|
122
|
+
participants.add(id);
|
|
123
|
+
else
|
|
124
|
+
participants.delete(id);
|
|
125
|
+
const tiles = participating
|
|
126
|
+
? state.tiles.includes(id)
|
|
127
|
+
? state.tiles
|
|
128
|
+
: [...state.tiles, id]
|
|
129
|
+
: state.tiles.filter((value) => value !== id);
|
|
130
|
+
const order = participating
|
|
131
|
+
? state.order.includes(id)
|
|
132
|
+
? state.order
|
|
133
|
+
: [...state.order, id]
|
|
134
|
+
: state.order.filter((value) => value !== id);
|
|
135
|
+
return {
|
|
136
|
+
...state,
|
|
137
|
+
participating: participants,
|
|
138
|
+
tiles,
|
|
139
|
+
order,
|
|
140
|
+
zoomed: participating || state.zoomed !== id ? state.zoomed : null,
|
|
141
|
+
gesture: !participating &&
|
|
142
|
+
state.gesture?.kind === "frame" &&
|
|
143
|
+
state.gesture.id === id
|
|
144
|
+
? null
|
|
145
|
+
: state.gesture,
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
setMinima: (id, minima) => {
|
|
150
|
+
const previous = read().minima.get(id);
|
|
151
|
+
if (!previous ||
|
|
152
|
+
(previous.width === minima.width && previous.height === minima.height))
|
|
153
|
+
return;
|
|
154
|
+
update((state) => ({
|
|
155
|
+
...state,
|
|
156
|
+
minima: new Map(state.minima).set(id, minima),
|
|
157
|
+
}));
|
|
158
|
+
},
|
|
159
|
+
unregister: (id) => {
|
|
160
|
+
update((state) => {
|
|
161
|
+
if (!state.frames.has(id))
|
|
162
|
+
return state;
|
|
163
|
+
const frames = new Map(state.frames);
|
|
164
|
+
frames.delete(id);
|
|
165
|
+
const restore = new Map(state.restore);
|
|
166
|
+
restore.delete(id);
|
|
167
|
+
const order = state.order.filter((value) => value !== id);
|
|
168
|
+
const tiles = state.tiles.filter((value) => value !== id);
|
|
169
|
+
const minima = new Map(state.minima);
|
|
170
|
+
minima.delete(id);
|
|
171
|
+
const participating = new Set(state.participating);
|
|
172
|
+
participating.delete(id);
|
|
173
|
+
const gesture = state.gesture?.kind === "frame" && state.gesture.id === id
|
|
174
|
+
? null
|
|
175
|
+
: state.gesture;
|
|
176
|
+
return {
|
|
177
|
+
...state,
|
|
178
|
+
frames,
|
|
179
|
+
restore,
|
|
180
|
+
order,
|
|
181
|
+
tiles,
|
|
182
|
+
minima,
|
|
183
|
+
participating,
|
|
184
|
+
gesture,
|
|
185
|
+
zoomed: state.zoomed === id ? null : state.zoomed,
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
begin: (id, pointerId) => {
|
|
190
|
+
if (!registered(id) || !read().participating.has(id))
|
|
191
|
+
return;
|
|
192
|
+
update((state) => ({
|
|
193
|
+
...state,
|
|
194
|
+
gesture: {
|
|
195
|
+
kind: "frame",
|
|
196
|
+
id,
|
|
197
|
+
pointerId,
|
|
198
|
+
start: state.frames.get(id) ?? { x: 0, y: 0, width: 0, height: 0 },
|
|
199
|
+
},
|
|
200
|
+
order: [...state.order.filter((value) => value !== id), id],
|
|
201
|
+
}));
|
|
202
|
+
},
|
|
203
|
+
move: (id, pointerId, frame, bounds, minima, snapSide = null) => {
|
|
204
|
+
if (registered(id) &&
|
|
205
|
+
read().participating.has(id) &&
|
|
206
|
+
ownsFrame(id, pointerId)) {
|
|
207
|
+
update((state) => ({
|
|
208
|
+
...state,
|
|
209
|
+
frames: new Map(state.frames).set(id, clampFrame(frame, bounds, minima)),
|
|
210
|
+
gesture: state.gesture?.kind === "frame"
|
|
211
|
+
? {
|
|
212
|
+
...state.gesture,
|
|
213
|
+
snap: snapSide
|
|
214
|
+
? {
|
|
215
|
+
side: snapSide,
|
|
216
|
+
frame: snapFrame(snapSide, bounds, minima),
|
|
217
|
+
}
|
|
218
|
+
: null,
|
|
219
|
+
}
|
|
220
|
+
: state.gesture,
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
end: (id, pointerId) => {
|
|
225
|
+
if (ownsFrame(id, pointerId))
|
|
226
|
+
update((state) => {
|
|
227
|
+
const gesture = state.gesture;
|
|
228
|
+
if (gesture?.kind !== "frame" || !gesture.snap)
|
|
229
|
+
return { ...state, gesture: null };
|
|
230
|
+
return {
|
|
231
|
+
...state,
|
|
232
|
+
frames: new Map(state.frames).set(id, gesture.snap.frame),
|
|
233
|
+
restore: new Map(state.restore).set(id, state.restore.get(id) ?? gesture.start),
|
|
234
|
+
gesture: null,
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
cancel: (id, pointerId) => {
|
|
239
|
+
const gesture = read().gesture;
|
|
240
|
+
if (gesture?.kind === "frame" &&
|
|
241
|
+
gesture.id === id &&
|
|
242
|
+
gesture.pointerId === pointerId)
|
|
243
|
+
update((state) => {
|
|
244
|
+
const frames = new Map(state.frames);
|
|
245
|
+
frames.set(id, gesture.start);
|
|
246
|
+
return { ...state, frames, gesture: null };
|
|
247
|
+
});
|
|
248
|
+
},
|
|
249
|
+
snap: (id, side, bounds, minima) => {
|
|
250
|
+
if (!registered(id) || !read().participating.has(id))
|
|
251
|
+
return;
|
|
252
|
+
update((state) => {
|
|
253
|
+
const restore = new Map(state.restore);
|
|
254
|
+
restore.set(id, state.frames.get(id) ?? snapFrame(side, bounds, minima));
|
|
255
|
+
const frames = new Map(state.frames);
|
|
256
|
+
frames.set(id, snapFrame(side, bounds, minima));
|
|
257
|
+
return {
|
|
258
|
+
...state,
|
|
259
|
+
frames,
|
|
260
|
+
restore,
|
|
261
|
+
order: [...state.order.filter((value) => value !== id), id],
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
},
|
|
265
|
+
restoreFrame: (id, bounds, minima) => {
|
|
266
|
+
if (!registered(id) || !read().participating.has(id))
|
|
267
|
+
return;
|
|
268
|
+
update((state) => {
|
|
269
|
+
const frame = state.restore.get(id);
|
|
270
|
+
if (!frame)
|
|
271
|
+
return state;
|
|
272
|
+
const frames = new Map(state.frames);
|
|
273
|
+
frames.set(id, clampFrame(frame, bounds, minima));
|
|
274
|
+
const restore = new Map(state.restore);
|
|
275
|
+
restore.delete(id);
|
|
276
|
+
return { ...state, frames, restore };
|
|
277
|
+
});
|
|
278
|
+
},
|
|
279
|
+
focus: (id) => {
|
|
280
|
+
if (!registered(id))
|
|
281
|
+
return;
|
|
282
|
+
update((state) => ({
|
|
283
|
+
...state,
|
|
284
|
+
order: [...state.order.filter((value) => value !== id), id],
|
|
285
|
+
zoomed: state.zoomed === null ? null : id,
|
|
286
|
+
}));
|
|
287
|
+
},
|
|
288
|
+
swap: (first, second) => {
|
|
289
|
+
if (!registered(first) ||
|
|
290
|
+
!registered(second) ||
|
|
291
|
+
!read().participating.has(first) ||
|
|
292
|
+
!read().participating.has(second))
|
|
293
|
+
return;
|
|
294
|
+
update((state) => ({
|
|
295
|
+
...state,
|
|
296
|
+
tiles: state.tiles.map((id) => id === first ? second : id === second ? first : id),
|
|
297
|
+
}));
|
|
298
|
+
},
|
|
299
|
+
zoom: (id) => {
|
|
300
|
+
if (id !== null && (!registered(id) || !read().participating.has(id)))
|
|
301
|
+
return;
|
|
302
|
+
update((state) => ({
|
|
303
|
+
...state,
|
|
304
|
+
zoomed: id,
|
|
305
|
+
order: id === null
|
|
306
|
+
? state.order
|
|
307
|
+
: [...state.order.filter((value) => value !== id), id],
|
|
308
|
+
}));
|
|
309
|
+
},
|
|
310
|
+
resizeMain: (size) => {
|
|
311
|
+
update((state) => ({ ...state, mainSize: size }));
|
|
312
|
+
},
|
|
313
|
+
beginDivider: (pointerId) => {
|
|
314
|
+
if (read().gesture)
|
|
315
|
+
return false;
|
|
316
|
+
update((state) => ({
|
|
317
|
+
...state,
|
|
318
|
+
gesture: { kind: "divider", pointerId, start: state.mainSize },
|
|
319
|
+
}));
|
|
320
|
+
return true;
|
|
321
|
+
},
|
|
322
|
+
finishDivider: (pointerId, cancel) => {
|
|
323
|
+
const gesture = read().gesture;
|
|
324
|
+
if (gesture?.kind !== "divider" || gesture.pointerId !== pointerId)
|
|
325
|
+
return;
|
|
326
|
+
update((state) => ({
|
|
327
|
+
...state,
|
|
328
|
+
gesture: null,
|
|
329
|
+
mainSize: cancel ? gesture.start : state.mainSize,
|
|
330
|
+
}));
|
|
331
|
+
},
|
|
332
|
+
destroy: () => {
|
|
333
|
+
update((state) => ({ ...state, gesture: null, restore: new Map() }));
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DOMAttributes, PointerEvent } from "react";
|
|
2
|
+
/** One captured pointer; cancellation and unmount share the same release path. */
|
|
3
|
+
export declare function usePointerDrag(handlers: {
|
|
4
|
+
begin: (event: PointerEvent<HTMLElement>) => boolean;
|
|
5
|
+
move: (dx: number, dy: number, event: PointerEvent<HTMLElement>) => void;
|
|
6
|
+
end: (cancel: boolean) => void;
|
|
7
|
+
}, enabled?: boolean, signal?: AbortSignal): DOMAttributes<HTMLElement>;
|
package/dist/pointer.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
/** One captured pointer; cancellation and unmount share the same release path. */
|
|
3
|
+
export function usePointerDrag(handlers, enabled = true, signal) {
|
|
4
|
+
const latest = useRef(handlers);
|
|
5
|
+
latest.current = handlers;
|
|
6
|
+
const pointer = useRef(null);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (!enabled)
|
|
9
|
+
pointer.current?.release(true);
|
|
10
|
+
return () => pointer.current?.release(true);
|
|
11
|
+
}, [enabled]);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const cancel = () => {
|
|
14
|
+
pointer.current?.release(true);
|
|
15
|
+
};
|
|
16
|
+
signal?.addEventListener("abort", cancel);
|
|
17
|
+
return () => {
|
|
18
|
+
signal?.removeEventListener("abort", cancel);
|
|
19
|
+
};
|
|
20
|
+
}, [signal]);
|
|
21
|
+
return {
|
|
22
|
+
onPointerDown: (event) => {
|
|
23
|
+
if (!enabled ||
|
|
24
|
+
signal?.aborted ||
|
|
25
|
+
pointer.current ||
|
|
26
|
+
(event.pointerType === "mouse" && event.button !== 0))
|
|
27
|
+
return;
|
|
28
|
+
const { currentTarget: element, pointerId: id } = event;
|
|
29
|
+
// Capture before mutating geometry, so a failed capture cannot strand a gesture.
|
|
30
|
+
element.setPointerCapture(id);
|
|
31
|
+
if (!latest.current.begin(event)) {
|
|
32
|
+
element.releasePointerCapture(id);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const end = latest.current.end;
|
|
36
|
+
const lost = () => {
|
|
37
|
+
release(true);
|
|
38
|
+
};
|
|
39
|
+
const release = (cancel) => {
|
|
40
|
+
element.removeEventListener("lostpointercapture", lost);
|
|
41
|
+
pointer.current = null;
|
|
42
|
+
end(cancel);
|
|
43
|
+
if (element.hasPointerCapture(id))
|
|
44
|
+
element.releasePointerCapture(id);
|
|
45
|
+
};
|
|
46
|
+
pointer.current = {
|
|
47
|
+
id,
|
|
48
|
+
element,
|
|
49
|
+
x: event.clientX,
|
|
50
|
+
y: event.clientY,
|
|
51
|
+
release,
|
|
52
|
+
};
|
|
53
|
+
element.addEventListener("lostpointercapture", lost);
|
|
54
|
+
},
|
|
55
|
+
onPointerMove: (event) => {
|
|
56
|
+
const current = pointer.current;
|
|
57
|
+
if (current?.id !== event.pointerId)
|
|
58
|
+
return;
|
|
59
|
+
latest.current.move(event.clientX - current.x, event.clientY - current.y, event);
|
|
60
|
+
current.x = event.clientX;
|
|
61
|
+
current.y = event.clientY;
|
|
62
|
+
},
|
|
63
|
+
onPointerUp: (event) => {
|
|
64
|
+
if (pointer.current?.id === event.pointerId)
|
|
65
|
+
pointer.current.release(false);
|
|
66
|
+
},
|
|
67
|
+
onPointerCancel: (event) => {
|
|
68
|
+
if (pointer.current?.id === event.pointerId)
|
|
69
|
+
pointer.current.release(true);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import { type Bounds, type Frame, type Minima, type SnapSide } from "./layout.js";
|
|
3
|
+
import { type TilingOptions } from "./tiling.js";
|
|
4
|
+
export type MaosaicoItem = {
|
|
5
|
+
id: string;
|
|
6
|
+
frame: Frame;
|
|
7
|
+
minima: Minima;
|
|
8
|
+
};
|
|
9
|
+
export type MaosaicoItemOptions = {
|
|
10
|
+
kind?: "drag" | "resize";
|
|
11
|
+
participating?: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type MaosaicoItemBinding = {
|
|
14
|
+
frame: Frame;
|
|
15
|
+
/** Merge into the window's style, inside the windows layer. */
|
|
16
|
+
stackingStyle: Pick<React.CSSProperties, "zIndex">;
|
|
17
|
+
visible: boolean;
|
|
18
|
+
active: boolean;
|
|
19
|
+
props: React.DOMAttributes<HTMLElement>;
|
|
20
|
+
focus: () => void;
|
|
21
|
+
snap: (side: SnapSide) => void;
|
|
22
|
+
restore: () => void;
|
|
23
|
+
};
|
|
24
|
+
/** Omit tiling to retain floating frames. Width is a preference, in pixels. */
|
|
25
|
+
export declare function MaosaicoRoot({ bounds, tiling, containerRef, children, }: {
|
|
26
|
+
bounds: Bounds;
|
|
27
|
+
tiling?: TilingOptions | undefined;
|
|
28
|
+
/** The positioned workspace element; supplying it enables container-relative edge snapping. */
|
|
29
|
+
containerRef?: React.RefObject<HTMLElement | null>;
|
|
30
|
+
children?: React.ReactNode;
|
|
31
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
32
|
+
/** Layout commands keep content and DOM ownership with the caller. */
|
|
33
|
+
export declare function useMaosaicoLayout(): {
|
|
34
|
+
/** Decorations rank immediately above every window, inside the windows region. */
|
|
35
|
+
stackingStyle: {
|
|
36
|
+
zIndex: number;
|
|
37
|
+
};
|
|
38
|
+
mode: "tiled" | "floating";
|
|
39
|
+
order: readonly string[];
|
|
40
|
+
interacting: boolean;
|
|
41
|
+
snapPreview: {
|
|
42
|
+
side: SnapSide;
|
|
43
|
+
frame: Frame;
|
|
44
|
+
} | null;
|
|
45
|
+
activeId: string | null;
|
|
46
|
+
zoomedId: string | null;
|
|
47
|
+
tabGroups: string[][];
|
|
48
|
+
divider: {
|
|
49
|
+
props: React.DOMAttributes<HTMLElement>;
|
|
50
|
+
axis: "x" | "y";
|
|
51
|
+
position: number;
|
|
52
|
+
min: number;
|
|
53
|
+
max: number;
|
|
54
|
+
} | null;
|
|
55
|
+
focus: (id: string) => void;
|
|
56
|
+
swap: (first: string, second: string) => void;
|
|
57
|
+
zoom: (id: string | null) => void;
|
|
58
|
+
resizeMain: (size: number) => void;
|
|
59
|
+
};
|
|
60
|
+
export declare function useMaosaicoItem(item: MaosaicoItem, options?: MaosaicoItemOptions): MaosaicoItemBinding;
|