@edv4h/usketch-plugin-portal 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 EdV4H
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.
@@ -0,0 +1,3 @@
1
+ export { createPortalPlugin, type PortalPluginOptions } from "./plugin.js";
2
+ export { createPortalStore, defaultPortalBox, type PortalEntry, type PortalItem, type PortalStore, } from "./portal-store.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EACN,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,WAAW,GAChB,MAAM,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { createPortalPlugin } from "./plugin.js";
2
+ export { createPortalStore, defaultPortalBox, } from "./portal-store.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAA4B,MAAM,aAAa,CAAC;AAC3E,OAAO,EACN,iBAAiB,EACjB,gBAAgB,GAIhB,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { UsketchPlugin } from "@edv4h/usketch-shared";
2
+ import type * as Y from "yjs";
3
+ export interface PortalPluginOptions {
4
+ /** Shared Yjs doc — shared portals live in its `portals` map. */
5
+ doc: Y.Doc;
6
+ /** For the per-user localStorage key. */
7
+ boardId?: string;
8
+ /** For the per-user localStorage key. Defaults to "local". */
9
+ userId?: string;
10
+ }
11
+ /**
12
+ * "Portal" — pin any shape to a fixed on-screen panel that stays visible through
13
+ * pan/zoom (picture-in-picture). Portals are per-user by default (localStorage)
14
+ * and can be toggled to shared (a `portals` Y.Map, synced to everyone). The panel
15
+ * re-renders the shape via its registered definition, so interactive shapes (e.g.
16
+ * the timer) remain operable inside the portal.
17
+ */
18
+ export declare function createPortalPlugin(options: PortalPluginOptions): UsketchPlugin;
19
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA8B,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACvF,OAAO,KAAK,KAAK,CAAC,MAAM,KAAK,CAAC;AAI9B,MAAM,WAAW,mBAAmB;IACnC,iEAAiE;IACjE,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC;IACX,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAID;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,aAAa,CAkE9E"}
package/dist/plugin.js ADDED
@@ -0,0 +1,78 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { PortalLayer } from "./portal-layer.js";
3
+ import { createPortalStore, defaultPortalBox } from "./portal-store.js";
4
+ const LAYER_ID = "portal";
5
+ /**
6
+ * "Portal" — pin any shape to a fixed on-screen panel that stays visible through
7
+ * pan/zoom (picture-in-picture). Portals are per-user by default (localStorage)
8
+ * and can be toggled to shared (a `portals` Y.Map, synced to everyone). The panel
9
+ * re-renders the shape via its registered definition, so interactive shapes (e.g.
10
+ * the timer) remain operable inside the portal.
11
+ */
12
+ export function createPortalPlugin(options) {
13
+ return {
14
+ id: "usketch-plugin-portal",
15
+ name: "ポータル",
16
+ setup(ctx) {
17
+ const store = createPortalStore({
18
+ doc: options.doc,
19
+ userId: options.userId ?? "local",
20
+ boardId: options.boardId,
21
+ });
22
+ ctx.layers.register({
23
+ id: LAYER_ID,
24
+ order: 150,
25
+ fixed: true,
26
+ render: () => _jsx(PortalLayer, { portalStore: store, store: ctx.store, shapes: ctx.shapes }),
27
+ });
28
+ ctx.events.emit("layers:changed", {});
29
+ const boundsOf = (shapeId) => {
30
+ const shape = ctx.store.getShape(shapeId);
31
+ if (!shape)
32
+ return null;
33
+ const def = ctx.shapes.get(shape.type);
34
+ return def
35
+ ? def.getBounds(shape)
36
+ : { x: shape.x, y: shape.y, width: shape.width, height: shape.height };
37
+ };
38
+ const offActions = [
39
+ ctx.actions.register({
40
+ id: "portal:pin-selected",
41
+ label: "📌 選択をポータル",
42
+ group: "Portal",
43
+ order: 0,
44
+ isEnabled: () => ctx.store.getSelection().size >= 1,
45
+ run: () => {
46
+ const already = new Set(store.getAll().map((it) => it.entry.shapeId));
47
+ let i = 0;
48
+ for (const id of ctx.store.getSelection()) {
49
+ if (already.has(id))
50
+ continue;
51
+ const bounds = boundsOf(id);
52
+ if (!bounds)
53
+ continue;
54
+ store.add(id, defaultPortalBox(bounds, i));
55
+ i++;
56
+ }
57
+ },
58
+ }),
59
+ ctx.actions.register({
60
+ id: "portal:clear-mine",
61
+ label: "✕ 自分のポータルを全解除",
62
+ group: "Portal",
63
+ order: 1,
64
+ isEnabled: () => store.getAll().some((it) => !it.shared),
65
+ run: () => store.clearPrivate(),
66
+ }),
67
+ ];
68
+ return () => {
69
+ for (const off of offActions)
70
+ off();
71
+ ctx.layers.unregister(LAYER_ID);
72
+ ctx.events.emit("layers:changed", {});
73
+ store.destroy();
74
+ };
75
+ },
76
+ };
77
+ }
78
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { BoardStore, ShapeRegistry } from "@edv4h/usketch-shared";
2
+ import { type PortalStore } from "./portal-store.js";
3
+ export declare function PortalLayer({ portalStore, store, shapes, }: {
4
+ portalStore: PortalStore;
5
+ store: BoardStore;
6
+ shapes: ShapeRegistry;
7
+ }): import("react").JSX.Element;
8
+ //# sourceMappingURL=portal-layer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portal-layer.d.ts","sourceRoot":"","sources":["../src/portal-layer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAA0B,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE/F,OAAO,EAAqC,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AA+NxF,wBAAgB,WAAW,CAAC,EAC3B,WAAW,EACX,KAAK,EACL,MAAM,GACN,EAAE;IACF,WAAW,EAAE,WAAW,CAAC;IACzB,KAAK,EAAE,UAAU,CAAC;IAClB,MAAM,EAAE,aAAa,CAAC;CACtB,+BAiCA"}
@@ -0,0 +1,148 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useReducer, useSyncExternalStore } from "react";
3
+ import { PORTAL_HEADER_H } from "./portal-store.js";
4
+ // Keep panel pointer/click/wheel from reaching the canvas (which would pan/select).
5
+ // Children (e.g. a timer shape's buttons) still receive their events at the target
6
+ // first, so they keep working; this only stops bubbling up to the canvas.
7
+ const stopCanvas = {
8
+ onPointerDown: (e) => e.stopPropagation(),
9
+ onWheel: (e) => e.stopPropagation(),
10
+ onContextMenu: (e) => e.stopPropagation(),
11
+ };
12
+ const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
13
+ /**
14
+ * Track a pointer drag on `window` until it ends. Cleans up on pointerup AND on
15
+ * pointercancel / window blur, so an interrupted gesture (OS gesture, lost
16
+ * capture, focus loss) never leaves listeners stuck updating the panel.
17
+ */
18
+ function trackPointer(onMove) {
19
+ const end = () => {
20
+ window.removeEventListener("pointermove", onMove);
21
+ window.removeEventListener("pointerup", end);
22
+ window.removeEventListener("pointercancel", end);
23
+ window.removeEventListener("blur", end);
24
+ };
25
+ window.addEventListener("pointermove", onMove);
26
+ window.addEventListener("pointerup", end);
27
+ window.addEventListener("pointercancel", end);
28
+ window.addEventListener("blur", end);
29
+ }
30
+ function ShapeContent({ def, shape, bodyW, bodyH, }) {
31
+ if (!def)
32
+ return null;
33
+ const b = def.getBounds(shape);
34
+ if (def.renderTarget === "html") {
35
+ const k = Math.min(bodyW / Math.max(1, b.width), bodyH / Math.max(1, b.height));
36
+ return (_jsx("div", { style: { width: bodyW, height: bodyH, overflow: "hidden", position: "relative" }, children: _jsx("div", { style: {
37
+ width: b.width,
38
+ height: b.height,
39
+ transform: `scale(${k})`,
40
+ transformOrigin: "top left",
41
+ }, children: def.render(shape) }) }));
42
+ }
43
+ // SVG (default): viewBox does the world→box fit for us.
44
+ return (_jsx("svg", { width: bodyW, height: bodyH, viewBox: `${b.x} ${b.y} ${b.width} ${b.height}`, preserveAspectRatio: "xMidYMid meet", "aria-label": "portal shape", children: def.render(shape) }));
45
+ }
46
+ function PortalPanel({ entry, shared, shape, def, onUpdate, onRemove, onToggleShared, }) {
47
+ const bodyH = Math.max(0, entry.h - PORTAL_HEADER_H);
48
+ const startDrag = (e) => {
49
+ e.stopPropagation();
50
+ e.preventDefault();
51
+ const sx = e.clientX;
52
+ const sy = e.clientY;
53
+ const ox = entry.x;
54
+ const oy = entry.y;
55
+ trackPointer((ev) => {
56
+ const x = clamp(ox + ev.clientX - sx, 0, window.innerWidth - entry.w);
57
+ const y = clamp(oy + ev.clientY - sy, 0, window.innerHeight - entry.h);
58
+ onUpdate(entry.id, { x, y });
59
+ });
60
+ };
61
+ const startResize = (e) => {
62
+ e.stopPropagation();
63
+ e.preventDefault();
64
+ const sx = e.clientX;
65
+ const sy = e.clientY;
66
+ const ow = entry.w;
67
+ const oh = entry.h;
68
+ trackPointer((ev) => {
69
+ // Guard max ≥ min: near the right/bottom edge the available space can be
70
+ // smaller than the min, in which case clamp(min, max<min) would return min
71
+ // and push the panel off-screen.
72
+ const w = clamp(ow + ev.clientX - sx, 140, Math.max(140, window.innerWidth - entry.x));
73
+ const h = clamp(oh + ev.clientY - sy, 100, Math.max(100, window.innerHeight - entry.y));
74
+ onUpdate(entry.id, { w, h });
75
+ });
76
+ };
77
+ return (_jsxs("div", { ...stopCanvas, style: {
78
+ position: "fixed",
79
+ left: entry.x,
80
+ top: entry.y,
81
+ width: entry.w,
82
+ height: entry.h,
83
+ display: "flex",
84
+ flexDirection: "column",
85
+ background: "#fff",
86
+ border: "1px solid #d1d5db",
87
+ borderRadius: 8,
88
+ boxShadow: "0 4px 16px rgba(0,0,0,0.18)",
89
+ overflow: "hidden",
90
+ pointerEvents: "auto",
91
+ fontFamily: "system-ui, sans-serif",
92
+ }, children: [_jsxs("div", { onPointerDown: startDrag, style: {
93
+ height: PORTAL_HEADER_H,
94
+ flex: "0 0 auto",
95
+ display: "flex",
96
+ alignItems: "center",
97
+ gap: 4,
98
+ padding: "0 6px",
99
+ background: "#f3f4f6",
100
+ borderBottom: "1px solid #e5e7eb",
101
+ cursor: "move",
102
+ userSelect: "none",
103
+ }, children: [_jsx("span", { style: {
104
+ flex: 1,
105
+ fontSize: 11,
106
+ fontWeight: 600,
107
+ color: "#374151",
108
+ whiteSpace: "nowrap",
109
+ overflow: "hidden",
110
+ textOverflow: "ellipsis",
111
+ }, children: shape.label || shape.type }), _jsx("button", { type: "button", title: shared ? "全員に共有中(クリックで個人に戻す)" : "個人(クリックで全員に共有)", onPointerDown: (e) => e.stopPropagation(), onClick: () => onToggleShared(entry.id, !shared), style: headerBtn, children: shared ? "👥" : "🔒" }), _jsx("button", { type: "button", title: "\u9589\u3058\u308B", onPointerDown: (e) => e.stopPropagation(), onClick: () => onRemove(entry.id), style: { ...headerBtn, color: "#9ca3af" }, children: "\u2715" })] }), _jsxs("div", { style: { flex: 1, minHeight: 0, position: "relative" }, children: [_jsx(ShapeContent, { def: def, shape: shape, bodyW: entry.w, bodyH: bodyH }), _jsx("div", { onPointerDown: startResize, style: {
112
+ position: "absolute",
113
+ right: 0,
114
+ bottom: 0,
115
+ width: 14,
116
+ height: 14,
117
+ cursor: "nwse-resize",
118
+ background: "linear-gradient(135deg, transparent 50%, rgba(0,0,0,0.25) 50%, rgba(0,0,0,0.25) 60%, transparent 60%)",
119
+ } })] })] }));
120
+ }
121
+ const headerBtn = {
122
+ border: "none",
123
+ background: "transparent",
124
+ cursor: "pointer",
125
+ fontSize: 13,
126
+ lineHeight: 1,
127
+ padding: 2,
128
+ };
129
+ export function PortalLayer({ portalStore, store, shapes, }) {
130
+ const items = useSyncExternalStore(portalStore.subscribe, portalStore.getAll);
131
+ // Re-render on any store mutation so pinned shapes reflect edits.
132
+ const [, tick] = useReducer((n) => n + 1, 0);
133
+ useEffect(() => store.subscribe(tick), [store]);
134
+ // Drop portals whose target shape no longer exists.
135
+ useEffect(() => {
136
+ for (const { entry } of items) {
137
+ if (!store.getShape(entry.shapeId))
138
+ portalStore.remove(entry.id);
139
+ }
140
+ }, [items, store, portalStore]);
141
+ return (_jsx(_Fragment, { children: items.map(({ entry, shared }) => {
142
+ const shape = store.getShape(entry.shapeId);
143
+ if (!shape)
144
+ return null;
145
+ return (_jsx(PortalPanel, { entry: entry, shared: shared, shape: shape, def: shapes.get(shape.type), onUpdate: portalStore.update, onRemove: portalStore.remove, onToggleShared: portalStore.setShared }, entry.id));
146
+ }) }));
147
+ }
148
+ //# sourceMappingURL=portal-layer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portal-layer.js","sourceRoot":"","sources":["../src/portal-layer.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ import type * as Y from "yjs";
2
+ /** A pinned widget: which shape, and where/how big on screen (screen px). */
3
+ export interface PortalEntry {
4
+ id: string;
5
+ shapeId: string;
6
+ x: number;
7
+ y: number;
8
+ w: number;
9
+ h: number;
10
+ }
11
+ /** A portal plus whether it lives in the shared (everyone) or private (me) backend. */
12
+ export interface PortalItem {
13
+ entry: PortalEntry;
14
+ shared: boolean;
15
+ }
16
+ export interface PortalBox {
17
+ x: number;
18
+ y: number;
19
+ w: number;
20
+ h: number;
21
+ }
22
+ export interface PortalStore {
23
+ /** All portals (private + shared), stable reference until the next change. */
24
+ getAll(): PortalItem[];
25
+ /** Pin a shape (private by default). Returns the created entry. */
26
+ add(shapeId: string, box: PortalBox, shared?: boolean): PortalEntry;
27
+ /** Move/resize a portal (screen px). */
28
+ update(id: string, patch: Partial<PortalBox>): void;
29
+ remove(id: string): void;
30
+ /** Move a portal between the private and shared backends (keeps its id). */
31
+ setShared(id: string, shared: boolean): void;
32
+ /** Remove all of this user's private portals. */
33
+ clearPrivate(): void;
34
+ subscribe(cb: () => void): () => void;
35
+ destroy(): void;
36
+ }
37
+ type MinimalStorage = Pick<Storage, "getItem" | "setItem">;
38
+ export interface CreatePortalStoreOptions {
39
+ /** Shared Yjs doc — shared portals live in its `portals` map. */
40
+ doc: Y.Doc;
41
+ userId: string;
42
+ boardId?: string;
43
+ /** Injectable storage (tests). Defaults to `localStorage` when available. */
44
+ storage?: MinimalStorage | null;
45
+ }
46
+ /** Portal panel header height (px) — shared with the layer for body sizing. */
47
+ export declare const PORTAL_HEADER_H = 30;
48
+ /**
49
+ * Default panel box for a freshly pinned shape: fit its bounds into MAX_W×MAX_H
50
+ * preserving aspect — the fit itself only downscales (`k ≤ 1`) — then apply a
51
+ * MIN_W/MIN_H floor (which may enlarge the panel for very small shapes; the
52
+ * renderer scales content to fill). Header height is added and the box cascades
53
+ * from the top-right corner by `index` so multiple pins don't stack exactly.
54
+ */
55
+ export declare function defaultPortalBox(bounds: {
56
+ width: number;
57
+ height: number;
58
+ }, index: number, viewportWidth?: number): PortalBox;
59
+ export declare function createPortalStore(options: CreatePortalStoreOptions): PortalStore;
60
+ export {};
61
+ //# sourceMappingURL=portal-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portal-store.d.ts","sourceRoot":"","sources":["../src/portal-store.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,CAAC,MAAM,KAAK,CAAC;AAE9B,6EAA6E;AAC7E,MAAM,WAAW,WAAW;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACV;AAED,uFAAuF;AACvF,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACzB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACV;AAED,MAAM,WAAW,WAAW;IAC3B,8EAA8E;IAC9E,MAAM,IAAI,UAAU,EAAE,CAAC;IACvB,mEAAmE;IACnE,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,WAAW,CAAC;IACpE,wCAAwC;IACxC,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;IACpD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IAC7C,iDAAiD;IACjD,YAAY,IAAI,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IACtC,OAAO,IAAI,IAAI,CAAC;CAChB;AAED,KAAK,cAAc,GAAG,IAAI,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC,CAAC;AAgB3D,MAAM,WAAW,wBAAwB;IACxC,iEAAiE;IACjE,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;CAChC;AAED,+EAA+E;AAC/E,eAAO,MAAM,eAAe,KAAK,CAAC;AAMlC;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC/B,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACzC,KAAK,EAAE,MAAM,EACb,aAAa,SAA2D,GACtE,SAAS,CASX;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,WAAW,CA4HhF"}
@@ -0,0 +1,161 @@
1
+ import { generateId } from "@edv4h/usketch-shared";
2
+ function storageKey(boardId, userId) {
3
+ return `usketch:portals:${boardId ?? "local"}:${userId}`;
4
+ }
5
+ /** localStorage may be unavailable (SSR / node tests / privacy mode) → in-memory fallback. */
6
+ function safeStorage() {
7
+ try {
8
+ if (typeof localStorage === "undefined")
9
+ return null;
10
+ return localStorage;
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ /** Portal panel header height (px) — shared with the layer for body sizing. */
17
+ export const PORTAL_HEADER_H = 30;
18
+ const MAX_W = 260;
19
+ const MAX_H = 200;
20
+ const MIN_W = 140;
21
+ const MIN_H = 100;
22
+ /**
23
+ * Default panel box for a freshly pinned shape: fit its bounds into MAX_W×MAX_H
24
+ * preserving aspect — the fit itself only downscales (`k ≤ 1`) — then apply a
25
+ * MIN_W/MIN_H floor (which may enlarge the panel for very small shapes; the
26
+ * renderer scales content to fill). Header height is added and the box cascades
27
+ * from the top-right corner by `index` so multiple pins don't stack exactly.
28
+ */
29
+ export function defaultPortalBox(bounds, index, viewportWidth = typeof window !== "undefined" ? window.innerWidth : 1200) {
30
+ const k = Math.min(MAX_W / Math.max(1, bounds.width), MAX_H / Math.max(1, bounds.height), 1);
31
+ const w = Math.max(MIN_W, Math.round(bounds.width * k));
32
+ const h = Math.max(MIN_H, Math.round(bounds.height * k)) + PORTAL_HEADER_H;
33
+ const margin = 16;
34
+ const step = 28;
35
+ const x = Math.max(margin, viewportWidth - w - margin - index * step);
36
+ const y = margin + index * step;
37
+ return { x, y, w, h };
38
+ }
39
+ export function createPortalStore(options) {
40
+ const { doc, userId, boardId } = options;
41
+ const storage = options.storage !== undefined ? options.storage : safeStorage();
42
+ const key = storageKey(boardId, userId);
43
+ const sharedMap = doc.getMap("portals");
44
+ const listeners = new Set();
45
+ let priv = loadPrivate();
46
+ function loadPrivate() {
47
+ if (!storage)
48
+ return [];
49
+ try {
50
+ const raw = storage.getItem(key);
51
+ const parsed = raw ? JSON.parse(raw) : null;
52
+ return Array.isArray(parsed) ? parsed : [];
53
+ }
54
+ catch {
55
+ return [];
56
+ }
57
+ }
58
+ function persistPrivate() {
59
+ if (!storage)
60
+ return;
61
+ try {
62
+ storage.setItem(key, JSON.stringify(priv));
63
+ }
64
+ catch {
65
+ // quota / disabled — keep in-memory copy only
66
+ }
67
+ }
68
+ function computeSnapshot() {
69
+ const shared = [];
70
+ sharedMap.forEach((entry) => {
71
+ if (entry)
72
+ shared.push({ entry, shared: true });
73
+ });
74
+ const mine = priv.map((entry) => ({ entry, shared: false }));
75
+ return [...mine, ...shared].sort((a, b) => a.entry.id.localeCompare(b.entry.id));
76
+ }
77
+ let snapshot = computeSnapshot();
78
+ function refresh() {
79
+ snapshot = computeSnapshot();
80
+ for (const cb of listeners)
81
+ cb();
82
+ }
83
+ const observer = () => refresh();
84
+ sharedMap.observe(observer);
85
+ const findPrivate = (id) => priv.find((e) => e.id === id);
86
+ return {
87
+ getAll: () => snapshot,
88
+ add(shapeId, box, shared = false) {
89
+ const entry = { id: generateId(), shapeId, ...box };
90
+ if (shared) {
91
+ sharedMap.set(entry.id, entry); // observe → refresh
92
+ }
93
+ else {
94
+ priv = [...priv, entry];
95
+ persistPrivate();
96
+ refresh();
97
+ }
98
+ return entry;
99
+ },
100
+ update(id, patch) {
101
+ const p = findPrivate(id);
102
+ if (p) {
103
+ priv = priv.map((e) => (e.id === id ? { ...e, ...patch } : e));
104
+ persistPrivate();
105
+ refresh();
106
+ return;
107
+ }
108
+ const s = sharedMap.get(id);
109
+ if (s)
110
+ sharedMap.set(id, { ...s, ...patch }); // observe → refresh
111
+ },
112
+ remove(id) {
113
+ if (findPrivate(id)) {
114
+ priv = priv.filter((e) => e.id !== id);
115
+ persistPrivate();
116
+ refresh();
117
+ return;
118
+ }
119
+ if (sharedMap.has(id))
120
+ sharedMap.delete(id);
121
+ },
122
+ setShared(id, shared) {
123
+ // In both directions we mutate the *other* backend first (silently), then
124
+ // touch the Y.Map last so its observer fires exactly one refresh over an
125
+ // already-consistent state — no double-notify, no transient frame where
126
+ // the portal is missing or duplicated.
127
+ if (shared) {
128
+ const p = findPrivate(id);
129
+ if (!p)
130
+ return;
131
+ priv = priv.filter((e) => e.id !== id);
132
+ persistPrivate();
133
+ sharedMap.set(id, p); // observer → single refresh
134
+ }
135
+ else {
136
+ const s = sharedMap.get(id);
137
+ if (!s)
138
+ return;
139
+ priv = [...priv, s];
140
+ persistPrivate();
141
+ sharedMap.delete(id); // observer → single refresh
142
+ }
143
+ },
144
+ clearPrivate() {
145
+ if (priv.length === 0)
146
+ return;
147
+ priv = [];
148
+ persistPrivate();
149
+ refresh();
150
+ },
151
+ subscribe(cb) {
152
+ listeners.add(cb);
153
+ return () => listeners.delete(cb);
154
+ },
155
+ destroy() {
156
+ sharedMap.unobserve(observer);
157
+ listeners.clear();
158
+ },
159
+ };
160
+ }
161
+ //# sourceMappingURL=portal-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portal-store.js","sourceRoot":"","sources":["../src/portal-store.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@edv4h/usketch-plugin-portal",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "source": "./src/index.ts",
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "yjs": "^13.6.0",
16
+ "@edv4h/usketch-shared": "4.5.0"
17
+ },
18
+ "peerDependencies": {
19
+ "react": ">=19"
20
+ },
21
+ "devDependencies": {
22
+ "@types/react": "19.2.17",
23
+ "typescript": "7.0.2",
24
+ "vitest": "4.1.10"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md"
32
+ ],
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/EdV4H/usketch.git",
37
+ "directory": "plugins/usketch-plugin-portal"
38
+ },
39
+ "homepage": "https://github.com/EdV4H/usketch#readme",
40
+ "bugs": {
41
+ "url": "https://github.com/EdV4H/usketch/issues"
42
+ },
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.json",
45
+ "typecheck": "tsc --noEmit",
46
+ "test": "vitest run --passWithNoTests",
47
+ "clean": "rm -rf dist"
48
+ }
49
+ }