@1agh/maude 0.22.2 → 0.24.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/README.md +34 -1
- package/cli/bin/maude.mjs +3 -0
- package/cli/commands/cache.mjs +181 -0
- package/cli/commands/cache.test.mjs +166 -0
- package/cli/commands/design-link.test.mjs +84 -2
- package/cli/commands/design.mjs +95 -4
- package/cli/commands/design.test.mjs +56 -0
- package/cli/commands/help.mjs +34 -0
- package/cli/commands/hub.mjs +255 -30
- package/cli/commands/hub.test.mjs +135 -10
- package/cli/commands/init.mjs +3 -0
- package/cli/commands/preflight.mjs +11 -0
- package/cli/commands/preflight.test.mjs +46 -0
- package/cli/commands/scenario-report.mjs +45 -0
- package/cli/lib/cache.mjs +407 -0
- package/cli/lib/cache.test.mjs +303 -0
- package/cli/lib/design-link.mjs +222 -11
- package/cli/lib/flow-design-integration.test.mjs +165 -0
- package/cli/lib/gitignore-block.mjs +90 -0
- package/cli/lib/gitignore-block.test.mjs +123 -0
- package/cli/lib/hubs-config.mjs +42 -4
- package/cli/lib/plugin-cli-reachability.test.mjs +56 -0
- package/cli/lib/preflight.mjs +41 -10
- package/package.json +8 -8
- package/plugins/design/dev-server/ai-banner.tsx +2 -2
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +349 -112
- package/plugins/design/dev-server/annotations-layer.tsx +906 -137
- package/plugins/design/dev-server/api.ts +109 -4
- package/plugins/design/dev-server/bin/preflight.sh +21 -10
- package/plugins/design/dev-server/bin/prep.sh +211 -0
- package/plugins/design/dev-server/bin/scenario-report.mjs +209 -0
- package/plugins/design/dev-server/bin/screenshot.sh +18 -1
- package/plugins/design/dev-server/bin/smoke.sh +94 -11
- package/plugins/design/dev-server/build.ts +69 -3
- package/plugins/design/dev-server/canvas-comment-mount.tsx +384 -0
- package/plugins/design/dev-server/canvas-cursors.ts +125 -0
- package/plugins/design/dev-server/canvas-icons.tsx +130 -0
- package/plugins/design/dev-server/canvas-lib.tsx +47 -27
- package/plugins/design/dev-server/canvas-meta.schema.json +20 -0
- package/plugins/design/dev-server/canvas-shell.tsx +358 -245
- package/plugins/design/dev-server/client/app.jsx +191 -21
- package/plugins/design/dev-server/client/styles/3-shell.css +10 -0
- package/plugins/design/dev-server/collab/awareness-bridge.ts +77 -0
- package/plugins/design/dev-server/collab/index.ts +87 -9
- package/plugins/design/dev-server/collab/persistence.ts +34 -3
- package/plugins/design/dev-server/collab/registry.ts +131 -2
- package/plugins/design/dev-server/collab/room.ts +21 -8
- package/plugins/design/dev-server/config.schema.json +20 -0
- package/plugins/design/dev-server/context-menu.tsx +167 -23
- package/plugins/design/dev-server/context.ts +31 -0
- package/plugins/design/dev-server/contextual-toolbar.tsx +7 -7
- package/plugins/design/dev-server/dist/client.bundle.js +144 -22
- package/plugins/design/dev-server/dist/comment-mount.js +1868 -0
- package/plugins/design/dev-server/dist/styles.css +16 -0
- package/plugins/design/dev-server/dom-selection.ts +156 -0
- package/plugins/design/dev-server/equal-spacing-handles.tsx +1 -1
- package/plugins/design/dev-server/export-dialog.tsx +19 -17
- package/plugins/design/dev-server/fs-watch.ts +1 -0
- package/plugins/design/dev-server/hmr-broadcast.ts +51 -20
- package/plugins/design/dev-server/http.ts +260 -20
- package/plugins/design/dev-server/input-router.tsx +125 -64
- package/plugins/design/dev-server/participants-chrome.tsx +10 -10
- package/plugins/design/dev-server/server.ts +141 -1
- package/plugins/design/dev-server/sync/agent.ts +418 -0
- package/plugins/design/dev-server/sync/atomic-write.ts +103 -0
- package/plugins/design/dev-server/sync/codec.ts +324 -0
- package/plugins/design/dev-server/sync/connection-state.ts +203 -0
- package/plugins/design/dev-server/sync/echo-guard.ts +108 -0
- package/plugins/design/dev-server/sync/fs-mirror.ts +160 -0
- package/plugins/design/dev-server/sync/hubs-config.ts +87 -0
- package/plugins/design/dev-server/sync/index.ts +918 -0
- package/plugins/design/dev-server/sync/materialize.ts +62 -0
- package/plugins/design/dev-server/sync/migrate-seed.ts +163 -0
- package/plugins/design/dev-server/sync/origins.ts +57 -0
- package/plugins/design/dev-server/sync/projection.ts +368 -0
- package/plugins/design/dev-server/sync/status.ts +115 -0
- package/plugins/design/dev-server/sync/untrusted.ts +153 -0
- package/plugins/design/dev-server/test/_helpers.ts +6 -2
- package/plugins/design/dev-server/test/annotations-layer.test.ts +231 -0
- package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +276 -0
- package/plugins/design/dev-server/test/canvas-cursors.test.ts +73 -0
- package/plugins/design/dev-server/test/canvas-origin-gate.test.ts +76 -0
- package/plugins/design/dev-server/test/collab-awareness-bridge.test.ts +223 -0
- package/plugins/design/dev-server/test/collab-reseed-guard.test.ts +78 -0
- package/plugins/design/dev-server/test/collab-room.test.ts +21 -10
- package/plugins/design/dev-server/test/comment-mount.test.ts +87 -0
- package/plugins/design/dev-server/test/csp-canvas-shell.test.ts +46 -0
- package/plugins/design/dev-server/test/fixtures/phase-20-annotations.svg +1 -0
- package/plugins/design/dev-server/test/hmr-classify.test.ts +70 -0
- package/plugins/design/dev-server/test/input-router.test.ts +21 -0
- package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +100 -0
- package/plugins/design/dev-server/test/shared-doc-convergence.test.ts +265 -0
- package/plugins/design/dev-server/test/shared-doc-foundation.test.ts +63 -0
- package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +160 -0
- package/plugins/design/dev-server/test/shared-doc-projection.test.ts +238 -0
- package/plugins/design/dev-server/test/sync-agent.test.ts +278 -0
- package/plugins/design/dev-server/test/sync-atomic-write.test.ts +63 -0
- package/plugins/design/dev-server/test/sync-codec.test.ts +165 -0
- package/plugins/design/dev-server/test/sync-connection-state.test.ts +146 -0
- package/plugins/design/dev-server/test/sync-echo-guard.test.ts +96 -0
- package/plugins/design/dev-server/test/sync-fs-mirror.test.ts +182 -0
- package/plugins/design/dev-server/test/sync-hardening.test.ts +520 -0
- package/plugins/design/dev-server/test/sync-hubs-config.test.ts +130 -0
- package/plugins/design/dev-server/test/sync-meta-codec.test.ts +123 -0
- package/plugins/design/dev-server/test/sync-runtime.test.ts +812 -0
- package/plugins/design/dev-server/test/sync-status.test.ts +80 -0
- package/plugins/design/dev-server/test/sync-untrusted.test.ts +104 -0
- package/plugins/design/dev-server/test/use-collab.test.ts +0 -0
- package/plugins/design/dev-server/test/use-tool-mode.test.tsx +38 -10
- package/plugins/design/dev-server/tool-palette.tsx +18 -16
- package/plugins/design/dev-server/undo-hud.tsx +4 -4
- package/plugins/design/dev-server/undo-stack.ts +20 -4
- package/plugins/design/dev-server/use-annotation-resize.tsx +16 -5
- package/plugins/design/dev-server/use-collab.tsx +157 -13
- package/plugins/design/dev-server/use-selection-set.tsx +12 -0
- package/plugins/design/dev-server/use-tool-mode.tsx +27 -12
- package/plugins/design/dev-server/ws.ts +40 -1
- package/plugins/design/templates/_shell.html +63 -5
- package/plugins/design/templates/canvas.tsx.template +13 -0
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +15 -7
- package/plugins/flow/.claude-plugin/config.schema.json +5 -0
- package/plugins/flow/templates/ai-skeleton/README.md +1 -1
- package/plugins/flow/templates/ai-skeleton/gitignore +10 -0
- package/plugins/flow/templates/ai-skeleton/scenarios/README.md +3 -1
- package/plugins/flow/templates/ai-skeleton/workflows.config.json +2 -1
|
@@ -36,6 +36,15 @@ export interface PersistenceDeps {
|
|
|
36
36
|
fileForSlug: (slug: string) => Promise<string | null>;
|
|
37
37
|
/** Best-effort cache primer — see fileForSlug above. */
|
|
38
38
|
noteFile?: (file: string) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Phase 9.2 (DDR-064) — when this returns false for a slug, `seed` is a no-op
|
|
41
|
+
* for it. The shared-doc path passes a predicate that returns false for
|
|
42
|
+
* pinned (provider-attached) slugs, so the local file-seed can't push fresh
|
|
43
|
+
* Y.Array items that would DUPLICATE the hub's canonical items on merge
|
|
44
|
+
* (Risk 1). The migrate-seed + provider own initial population for those
|
|
45
|
+
* slugs. Absent → always seed (flag-OFF behavior, unchanged).
|
|
46
|
+
*/
|
|
47
|
+
shouldSeed?: (slug: string) => boolean;
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
/**
|
|
@@ -50,6 +59,16 @@ export function createPersistence(deps: PersistenceDeps): RoomCallbacks {
|
|
|
50
59
|
const { ctx, api, fileForSlug } = deps;
|
|
51
60
|
const stateDir = ensureStateDir(ctx.paths.designRoot);
|
|
52
61
|
|
|
62
|
+
// Per-slug latch: have we ever projected a NON-empty comments array for this
|
|
63
|
+
// canvas? We write an empty `[]` to disk only AFTER content has been seen — a
|
|
64
|
+
// genuine delete-all — never from a doc that was never populated (cold start
|
|
65
|
+
// before seed/migrate completes), which would clobber a non-empty local file.
|
|
66
|
+
// This closes the receiving-peer gap where a delete-all on one peer left
|
|
67
|
+
// stragglers on the other: the old `arr.length > 0` guard skipped the write
|
|
68
|
+
// when the array emptied, so the deletion never reached the peer's JSON file
|
|
69
|
+
// (the file the sidebar reads), even though the Y.Doc had converged (DDR-064).
|
|
70
|
+
const seenComments = new Set<string>();
|
|
71
|
+
|
|
53
72
|
function ydocBinPath(slug: string): string {
|
|
54
73
|
return path.join(stateDir, `${slug}.ydoc.bin`);
|
|
55
74
|
}
|
|
@@ -66,6 +85,13 @@ export function createPersistence(deps: PersistenceDeps): RoomCallbacks {
|
|
|
66
85
|
}
|
|
67
86
|
|
|
68
87
|
async function seed(slug: string, doc: Y.Doc): Promise<void> {
|
|
88
|
+
// Phase 9.2 (DDR-064) — under sharedDoc the migrate-seed + hub provider own
|
|
89
|
+
// initial population for a pinned slug; a local file-seed here would push
|
|
90
|
+
// fresh Y.Array items that DUPLICATE the hub's canonical items on merge
|
|
91
|
+
// (Risk 1). Skip seeding when the predicate says so. Flag-OFF / unpinned →
|
|
92
|
+
// always seeds (predicate absent or returns true).
|
|
93
|
+
if (deps.shouldSeed && !deps.shouldSeed(slug)) return;
|
|
94
|
+
|
|
69
95
|
// Step 1 — try the binary cache.
|
|
70
96
|
const binary = await readBinary(slug);
|
|
71
97
|
if (binary && binary.byteLength > 0) {
|
|
@@ -100,10 +126,15 @@ export function createPersistence(deps: PersistenceDeps): RoomCallbacks {
|
|
|
100
126
|
const file = await fileForSlug(slug);
|
|
101
127
|
if (!file) return;
|
|
102
128
|
|
|
103
|
-
// Comments — Y.Array projection back to JSON.
|
|
129
|
+
// Comments — Y.Array projection back to JSON. Write whenever the doc holds
|
|
130
|
+
// comments, OR when it just emptied after previously holding some (so a
|
|
131
|
+
// delete-all materializes on EVERY peer's disk, not just the originator's).
|
|
132
|
+
// The seenComments latch keeps a never-populated doc (cold start) from
|
|
133
|
+
// clobbering a non-empty local file with [].
|
|
104
134
|
const arr = doc.getArray(Y_TYPES.comments);
|
|
105
|
-
|
|
106
|
-
|
|
135
|
+
const list = arr.toArray() as Parameters<Api['saveCommentsForFile']>[1];
|
|
136
|
+
if (list.length > 0) seenComments.add(slug);
|
|
137
|
+
if (list.length > 0 || seenComments.has(slug)) {
|
|
107
138
|
await api.saveCommentsForFile(file, list);
|
|
108
139
|
}
|
|
109
140
|
|
|
@@ -6,15 +6,39 @@
|
|
|
6
6
|
// call into (Phase 8 Task 7) to force-snapshot every dirty room before a reload
|
|
7
7
|
// prompt — see DDR-051 §3.
|
|
8
8
|
|
|
9
|
+
import type { Awareness } from 'y-protocols/awareness';
|
|
10
|
+
|
|
11
|
+
import { bridgeAwareness } from './awareness-bridge.ts';
|
|
9
12
|
import { Y_TYPES } from './persistence.ts';
|
|
10
13
|
import type { Room, RoomCallbacks } from './room.ts';
|
|
11
14
|
import { createRoom } from './room.ts';
|
|
12
15
|
|
|
16
|
+
/** Structural equality via canonical JSON — used by the syncRoomFrom* no-op
|
|
17
|
+
* guards. Comment lists are small; stringify is cheap + dependency-free. */
|
|
18
|
+
function jsonEqual(a: unknown, b: unknown): boolean {
|
|
19
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
export interface Registry {
|
|
14
23
|
/** Get-or-create. Reuses an existing room for the same slug. */
|
|
15
24
|
get(slug: string): Room;
|
|
16
25
|
/** Existence check — returns the live room if any, else null. NEVER creates. */
|
|
17
26
|
peek(slug: string): Room | null;
|
|
27
|
+
/**
|
|
28
|
+
* Phase 9.2 (DDR-064) — the single cached `Y.Doc` for a slug, creating the
|
|
29
|
+
* owning Room if necessary. This is the seam the shared-doc path attaches its
|
|
30
|
+
* hub-facing `HocuspocusProvider` to (Phase B): the doc must exist
|
|
31
|
+
* independent of a live browser connection so the provider can attach at
|
|
32
|
+
* serve start. Repeated calls return the SAME instance (the room's doc) —
|
|
33
|
+
* the y-websocket-server `getYDoc(name)` single-cache pattern.
|
|
34
|
+
*
|
|
35
|
+
* Phase A: defined but unused (the flag-OFF two-doc path never calls it), so
|
|
36
|
+
* adding it is behavior-neutral. NOTE: a room created via `getDoc` (no
|
|
37
|
+
* browser conn) is NOT auto-dropped by the `size()===0` check in `drop` until
|
|
38
|
+
* Phase B teaches the lifecycle that an attached provider keeps it alive;
|
|
39
|
+
* for Phase A nothing calls `getDoc`, so no room leaks.
|
|
40
|
+
*/
|
|
41
|
+
getDoc(slug: string): import('yjs').Doc;
|
|
18
42
|
/**
|
|
19
43
|
* Phase 8 Task 3 bridge — inspector-channel writes (REST `/_api/comments*`
|
|
20
44
|
* or the legacy WS comments-add path) call this so the live Y.Array sees
|
|
@@ -34,6 +58,32 @@ export interface Registry {
|
|
|
34
58
|
* updated stroke set without waiting for a cold-open re-seed.
|
|
35
59
|
*/
|
|
36
60
|
syncRoomFromAnnotations(slug: string, svg: string): void;
|
|
61
|
+
/**
|
|
62
|
+
* Phase 9 Task 5 — attach the hub-side Awareness (from a sync provider) for
|
|
63
|
+
* a slug so the Room's Awareness (browser peers) is bridged bidirectionally
|
|
64
|
+
* to the hub. While attached, cursors / selections / viewport relay
|
|
65
|
+
* cross-machine through Hocuspocus. Idempotent per slug; returns a detach
|
|
66
|
+
* fn. If a room is already live the bridge wires immediately, otherwise it
|
|
67
|
+
* wires when the room is next created. Awareness is ephemeral — this writes
|
|
68
|
+
* no files (see awareness-bridge.ts on why F14 is untouched).
|
|
69
|
+
*/
|
|
70
|
+
attachHubAwareness(slug: string, awareness: Awareness): () => void;
|
|
71
|
+
/**
|
|
72
|
+
* Phase 9.2 (DDR-064) — pin a room so `drop` won't destroy it when the last
|
|
73
|
+
* browser leaves. The shared-doc path pins a slug while a hub
|
|
74
|
+
* `HocuspocusProvider` is attached to its `getDoc(slug)`: destroying the
|
|
75
|
+
* room (→ `doc.destroy()`) would pull the doc out from under the live
|
|
76
|
+
* provider. Self-gating — only the flag-ON sync runtime calls `pin`, so the
|
|
77
|
+
* flag-OFF `drop` lifecycle is byte-for-byte unchanged. Idempotent.
|
|
78
|
+
*/
|
|
79
|
+
pin(slug: string): void;
|
|
80
|
+
/** Release a pin (provider detached on runtime stop). Idempotent. */
|
|
81
|
+
unpin(slug: string): void;
|
|
82
|
+
/** True when a slug is pinned (a shared-doc provider is attached). Used to
|
|
83
|
+
* disable the room's local file-seed for that slug under sharedDoc — the
|
|
84
|
+
* migrate-seed + provider own initial population, so a duplicate file-seed
|
|
85
|
+
* can't re-introduce items (DDR-064 Risk 1). */
|
|
86
|
+
isPinned(slug: string): boolean;
|
|
37
87
|
/** Flush every dirty room synchronously. DDR-051 branch-switch path. */
|
|
38
88
|
flushAll(): Promise<void>;
|
|
39
89
|
/** Tear down everything (e.g. on server shutdown). */
|
|
@@ -46,25 +96,87 @@ export interface Registry {
|
|
|
46
96
|
|
|
47
97
|
export function createRegistry(callbacks: RoomCallbacks): Registry {
|
|
48
98
|
const rooms = new Map<string, Room>();
|
|
99
|
+
// Hub-side Awareness per slug (lives as long as the sync provider). Rooms
|
|
100
|
+
// churn as browser tabs come and go; the bridge is re-wired each time a room
|
|
101
|
+
// is (re)created for a slug that has an attached hub Awareness.
|
|
102
|
+
const hubAwareness = new Map<string, Awareness>();
|
|
103
|
+
const bridges = new Map<string, () => void>();
|
|
104
|
+
// Phase 9.2 (DDR-064) — slugs whose room must survive the last-browser-leaves
|
|
105
|
+
// drop because a shared-doc hub provider is attached to its doc. Only the
|
|
106
|
+
// flag-ON sync runtime ever adds to this; flag-OFF leaves it empty so `drop`
|
|
107
|
+
// is unchanged.
|
|
108
|
+
const pinned = new Set<string>();
|
|
109
|
+
|
|
110
|
+
function wireBridge(slug: string, room: Room): void {
|
|
111
|
+
if (bridges.has(slug)) return;
|
|
112
|
+
const hub = hubAwareness.get(slug);
|
|
113
|
+
if (!hub) return;
|
|
114
|
+
bridges.set(slug, bridgeAwareness(room.awareness, hub));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function teardownBridge(slug: string): void {
|
|
118
|
+
const detach = bridges.get(slug);
|
|
119
|
+
if (detach) {
|
|
120
|
+
detach();
|
|
121
|
+
bridges.delete(slug);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
49
124
|
|
|
50
125
|
function get(slug: string): Room {
|
|
51
126
|
let room = rooms.get(slug);
|
|
52
127
|
if (!room) {
|
|
53
128
|
room = createRoom(slug, callbacks);
|
|
54
129
|
rooms.set(slug, room);
|
|
130
|
+
wireBridge(slug, room);
|
|
55
131
|
}
|
|
56
132
|
return room;
|
|
57
133
|
}
|
|
58
134
|
|
|
135
|
+
function attachHubAwareness(slug: string, awareness: Awareness): () => void {
|
|
136
|
+
hubAwareness.set(slug, awareness);
|
|
137
|
+
const room = rooms.get(slug);
|
|
138
|
+
if (room) wireBridge(slug, room);
|
|
139
|
+
return () => {
|
|
140
|
+
teardownBridge(slug);
|
|
141
|
+
if (hubAwareness.get(slug) === awareness) hubAwareness.delete(slug);
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
59
145
|
function peek(slug: string): Room | null {
|
|
60
146
|
return rooms.get(slug) ?? null;
|
|
61
147
|
}
|
|
62
148
|
|
|
149
|
+
function getDoc(slug: string): import('yjs').Doc {
|
|
150
|
+
// Reuse get-or-create so the doc, awareness, persistence schedule, and
|
|
151
|
+
// (idempotent) awareness-bridge wiring are all set up exactly once. The
|
|
152
|
+
// room's doc IS the single shared doc for this slug.
|
|
153
|
+
return get(slug).doc;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function pin(slug: string): void {
|
|
157
|
+
pinned.add(slug);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function unpin(slug: string): void {
|
|
161
|
+
pinned.delete(slug);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isPinned(slug: string): boolean {
|
|
165
|
+
return pinned.has(slug);
|
|
166
|
+
}
|
|
167
|
+
|
|
63
168
|
function syncRoomFromComments(slug: string, comments: readonly unknown[]): void {
|
|
64
169
|
const room = rooms.get(slug);
|
|
65
170
|
if (!room) return;
|
|
171
|
+
const arr = room.doc.getArray<unknown>(Y_TYPES.comments);
|
|
172
|
+
// No-op guard (load-bearing): skip when the room already holds this exact
|
|
173
|
+
// list. The wholesale delete+push always emits a doc update, which schedules
|
|
174
|
+
// a persist → file write → fs event → re-seed … so without this equality
|
|
175
|
+
// short-circuit, re-seeding the live room from a disk change (sync-agent or
|
|
176
|
+
// design:edit write — see createCollab's fs hook) would spin an 800ms
|
|
177
|
+
// persist storm. Equality breaks the loop after a single convergence.
|
|
178
|
+
if (jsonEqual(arr.toArray(), comments)) return;
|
|
66
179
|
room.doc.transact(() => {
|
|
67
|
-
const arr = room.doc.getArray<unknown>(Y_TYPES.comments);
|
|
68
180
|
if (arr.length > 0) arr.delete(0, arr.length);
|
|
69
181
|
if (comments.length > 0) arr.push(comments as unknown[]);
|
|
70
182
|
}, 'inspector-write');
|
|
@@ -73,8 +185,9 @@ export function createRegistry(callbacks: RoomCallbacks): Registry {
|
|
|
73
185
|
function syncRoomFromAnnotations(slug: string, svg: string): void {
|
|
74
186
|
const room = rooms.get(slug);
|
|
75
187
|
if (!room) return;
|
|
188
|
+
const map = room.doc.getMap<string>(Y_TYPES.annotations);
|
|
189
|
+
if (map.get('svg') === svg) return; // no-op guard — same rationale as comments
|
|
76
190
|
room.doc.transact(() => {
|
|
77
|
-
const map = room.doc.getMap<string>(Y_TYPES.annotations);
|
|
78
191
|
map.set('svg', svg);
|
|
79
192
|
}, 'inspector-write');
|
|
80
193
|
}
|
|
@@ -87,11 +200,22 @@ export function createRegistry(callbacks: RoomCallbacks): Registry {
|
|
|
87
200
|
const room = rooms.get(slug);
|
|
88
201
|
if (!room) return;
|
|
89
202
|
if (room.size() > 0) return; // still active, leave it
|
|
203
|
+
// Phase 9.2 (DDR-064) — a pinned room has a shared-doc hub provider attached
|
|
204
|
+
// to its doc; destroying it would yank the doc out from under the provider.
|
|
205
|
+
// Leave it (with zero browser conns) until runtime stop / server shutdown.
|
|
206
|
+
// Empty in the flag-OFF path → no behavior change.
|
|
207
|
+
if (pinned.has(slug)) return;
|
|
208
|
+
// Tear the bridge down before room.destroy() runs awareness.destroy() —
|
|
209
|
+
// a late relay must not fire against a dead Awareness. The hub Awareness
|
|
210
|
+
// stays registered, so a reconnecting browser re-wires via get().
|
|
211
|
+
teardownBridge(slug);
|
|
90
212
|
rooms.delete(slug);
|
|
91
213
|
await room.destroy();
|
|
92
214
|
}
|
|
93
215
|
|
|
94
216
|
async function destroyAll(): Promise<void> {
|
|
217
|
+
for (const slug of Array.from(bridges.keys())) teardownBridge(slug);
|
|
218
|
+
hubAwareness.clear();
|
|
95
219
|
const all = Array.from(rooms.values());
|
|
96
220
|
rooms.clear();
|
|
97
221
|
await Promise.all(all.map((r) => r.destroy()));
|
|
@@ -100,8 +224,13 @@ export function createRegistry(callbacks: RoomCallbacks): Registry {
|
|
|
100
224
|
return {
|
|
101
225
|
get,
|
|
102
226
|
peek,
|
|
227
|
+
getDoc,
|
|
103
228
|
syncRoomFromComments,
|
|
104
229
|
syncRoomFromAnnotations,
|
|
230
|
+
attachHubAwareness,
|
|
231
|
+
pin,
|
|
232
|
+
unpin,
|
|
233
|
+
isPinned,
|
|
105
234
|
flushAll,
|
|
106
235
|
destroyAll,
|
|
107
236
|
drop,
|
|
@@ -73,6 +73,14 @@ export function createRoom(slug: string, callbacks: RoomCallbacks): Room {
|
|
|
73
73
|
const awareness = new Awareness(doc);
|
|
74
74
|
|
|
75
75
|
const conns = new Map<string, RoomConn>();
|
|
76
|
+
// clientID → owning conn.id, learned from the origin of each awareness update.
|
|
77
|
+
// Server-authoritative: we do NOT trust a client-supplied `__connId` (awareness
|
|
78
|
+
// is attacker-influenceable through a semi-trusted hub — DDR-054). On disconnect
|
|
79
|
+
// we remove exactly the clientIDs this conn published so other peers drop the
|
|
80
|
+
// avatar immediately, instead of waiting out the ~30s awareness timeout (which
|
|
81
|
+
// let phantom peers pile up on rapid reconnects — the cross-origin repro that
|
|
82
|
+
// surfaced this latent bug).
|
|
83
|
+
const connByClient = new Map<number, string>();
|
|
76
84
|
let dirty = false;
|
|
77
85
|
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
78
86
|
let destroyed = false;
|
|
@@ -138,6 +146,13 @@ export function createRoom(slug: string, callbacks: RoomCallbacks): Room {
|
|
|
138
146
|
if (changed.length === 0) return;
|
|
139
147
|
const except =
|
|
140
148
|
origin && typeof origin === 'object' && 'id' in origin ? (origin as RoomConn) : undefined;
|
|
149
|
+
// Learn which clientIDs belong to which conn from the update origin, so
|
|
150
|
+
// disconnect() can evict them precisely.
|
|
151
|
+
if (except) {
|
|
152
|
+
for (const id of added) connByClient.set(id, except.id);
|
|
153
|
+
for (const id of updated) connByClient.set(id, except.id);
|
|
154
|
+
for (const id of removed) connByClient.delete(id);
|
|
155
|
+
}
|
|
141
156
|
broadcast(encodeAwarenessFrame(awareness, changed), except);
|
|
142
157
|
}
|
|
143
158
|
);
|
|
@@ -161,16 +176,14 @@ export function createRoom(slug: string, callbacks: RoomCallbacks): Room {
|
|
|
161
176
|
|
|
162
177
|
function disconnect(conn: RoomConn): void {
|
|
163
178
|
conns.delete(conn.id);
|
|
164
|
-
//
|
|
165
|
-
// cursor renderers
|
|
166
|
-
|
|
179
|
+
// Remove the awareness states this conn published (tracked by update origin
|
|
180
|
+
// in connByClient) so other peers' cursor renderers drop the avatar now,
|
|
181
|
+
// not after the ~30s awareness timeout.
|
|
167
182
|
const stale: number[] = [];
|
|
168
|
-
for (const clientId of
|
|
169
|
-
|
|
170
|
-
// matching conn.id; remove states whose owning conn just left.
|
|
171
|
-
const state = states.get(clientId) as { __connId?: string } | undefined;
|
|
172
|
-
if (state && state.__connId === conn.id) stale.push(clientId);
|
|
183
|
+
for (const [clientId, connId] of connByClient) {
|
|
184
|
+
if (connId === conn.id) stale.push(clientId);
|
|
173
185
|
}
|
|
186
|
+
for (const clientId of stale) connByClient.delete(clientId);
|
|
174
187
|
if (stale.length) removeAwarenessStates(awareness, stale, conn);
|
|
175
188
|
}
|
|
176
189
|
|
|
@@ -180,6 +180,26 @@
|
|
|
180
180
|
"type": ["string", "null"],
|
|
181
181
|
"description": "Name of the DS used when a canvas's .meta.json has no 'designSystem' field. For single-DS projects, this is the only DS. For multi-DS, pick the most common.",
|
|
182
182
|
"default": null
|
|
183
|
+
},
|
|
184
|
+
"linkedHub": {
|
|
185
|
+
"type": "object",
|
|
186
|
+
"description": "Pairs this repo with a Maude hub for cross-machine file sync (Phase 9 Task 3+4). Written by 'maude design link <url> --token <hex>'. Token NEVER lives in this committed config — it's stored per-machine in ~/.config/maude/hubs.json. When present, 'maude design serve' starts the bidirectional Yjs↔fs sync agent (Task 4).",
|
|
187
|
+
"required": ["url", "linkedAt"],
|
|
188
|
+
"properties": {
|
|
189
|
+
"url": {
|
|
190
|
+
"type": "string",
|
|
191
|
+
"description": "Hub base URL (normalized — trailing slash trimmed, scheme + host lowercased). The sync agent constructs ws(s)://<host> from this for the HocuspocusProvider connection."
|
|
192
|
+
},
|
|
193
|
+
"linkedAt": {
|
|
194
|
+
"type": "number",
|
|
195
|
+
"description": "Unix-ms timestamp from when 'maude design link' was first run on this repo."
|
|
196
|
+
},
|
|
197
|
+
"adopt": {
|
|
198
|
+
"type": "boolean",
|
|
199
|
+
"description": "When true, the first sync after boot pushes local disk state up to the hub unconditionally (use case: bootstrapping the hub from a populated repo, or hub-was-wiped recovery). Cleared after first successful adopt."
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
"additionalProperties": false
|
|
183
203
|
}
|
|
184
204
|
}
|
|
185
205
|
}
|
|
@@ -55,6 +55,15 @@ export interface MenuItem {
|
|
|
55
55
|
shortcut?: string;
|
|
56
56
|
destructive?: boolean;
|
|
57
57
|
disabled?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Optional nested flyout (e.g. `Theme ▸ DS default / Light / Dark / Follow
|
|
60
|
+
* chrome`). When present the row opens a submenu on hover / ArrowRight /
|
|
61
|
+
* click and `onSelect` on THIS item is not invoked — only the chosen leaf's
|
|
62
|
+
* `onSelect` fires. A disabled leaf carries `disabledHint` for its title.
|
|
63
|
+
*/
|
|
64
|
+
submenu?: MenuItem[];
|
|
65
|
+
/** Hover/title hint shown when the item is `disabled` (a11y affordance). */
|
|
66
|
+
disabledHint?: string;
|
|
58
67
|
onSelect: (target: ContextTarget) => void;
|
|
59
68
|
}
|
|
60
69
|
|
|
@@ -158,20 +167,20 @@ const MENU_CSS = `
|
|
|
158
167
|
.dc-context-menu {
|
|
159
168
|
position: fixed;
|
|
160
169
|
z-index: 7;
|
|
161
|
-
background: var(--
|
|
162
|
-
border: 1px solid var(--
|
|
170
|
+
background: var(--maude-chrome-bg-0, #fff);
|
|
171
|
+
border: 1px solid var(--maude-chrome-fg-0, #1c1917);
|
|
163
172
|
border-radius: 8px;
|
|
164
|
-
box-shadow: 0 6px 24px color-mix(in oklab,
|
|
173
|
+
box-shadow: 0 6px 24px var(--maude-chrome-shadow, color-mix(in oklab, #1c1917 10%, transparent));
|
|
165
174
|
padding: 4px;
|
|
166
175
|
min-width: 220px;
|
|
167
|
-
font-family: var(--
|
|
176
|
+
font-family: var(--maude-chrome-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
168
177
|
font-size: 12px;
|
|
169
|
-
color: var(--
|
|
178
|
+
color: var(--maude-chrome-fg-0, rgba(20,15,10,0.92));
|
|
170
179
|
user-select: none;
|
|
171
180
|
}
|
|
172
181
|
.dc-context-menu .dc-menu-sep {
|
|
173
182
|
height: 1px;
|
|
174
|
-
background: var(--border
|
|
183
|
+
background: var(--maude-chrome-border, rgba(0,0,0,0.08));
|
|
175
184
|
margin: 4px -4px;
|
|
176
185
|
}
|
|
177
186
|
.dc-context-menu .dc-menu-item {
|
|
@@ -191,7 +200,7 @@ const MENU_CSS = `
|
|
|
191
200
|
}
|
|
192
201
|
.dc-context-menu .dc-menu-item:hover,
|
|
193
202
|
.dc-context-menu .dc-menu-item:focus-visible {
|
|
194
|
-
background:
|
|
203
|
+
background: color-mix(in oklab, var(--maude-chrome-fg-0, #1c1917) 8%, transparent);
|
|
195
204
|
outline: none;
|
|
196
205
|
}
|
|
197
206
|
.dc-context-menu .dc-menu-item[disabled] {
|
|
@@ -204,11 +213,28 @@ const MENU_CSS = `
|
|
|
204
213
|
color: var(--accent-fg, #fff);
|
|
205
214
|
}
|
|
206
215
|
.dc-context-menu .dc-menu-shortcut {
|
|
207
|
-
color: var(--fg-
|
|
216
|
+
color: var(--maude-chrome-fg-1, rgba(40,30,20,0.55));
|
|
208
217
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
209
218
|
font-size: 10px;
|
|
210
219
|
font-variant-numeric: tabular-nums;
|
|
211
220
|
}
|
|
221
|
+
/* Submenu flyout (Theme ▸ …). Anchored to the right of its parent row; flips
|
|
222
|
+
left near the viewport edge via .is-flip. Reuses .dc-menu-item styling. */
|
|
223
|
+
.dc-context-menu .dc-menu-sub { position: relative; }
|
|
224
|
+
.dc-context-menu .dc-menu-caret { color: var(--maude-chrome-fg-1, rgba(40,30,20,0.55)); font-size: 11px; }
|
|
225
|
+
.dc-context-menu .dc-menu-flyout {
|
|
226
|
+
position: absolute;
|
|
227
|
+
top: -5px;
|
|
228
|
+
left: calc(100% + 3px);
|
|
229
|
+
min-width: 196px;
|
|
230
|
+
padding: 4px;
|
|
231
|
+
background: var(--maude-chrome-bg-0, #fff);
|
|
232
|
+
border: 1px solid var(--maude-chrome-fg-0, #1c1917);
|
|
233
|
+
border-radius: 8px;
|
|
234
|
+
box-shadow: 0 6px 24px var(--maude-chrome-shadow, color-mix(in oklab, #1c1917 10%, transparent));
|
|
235
|
+
z-index: 8;
|
|
236
|
+
}
|
|
237
|
+
.dc-context-menu .dc-menu-flyout.is-flip { left: auto; right: calc(100% + 3px); }
|
|
212
238
|
`.trim();
|
|
213
239
|
|
|
214
240
|
function ensureMenuStyles(): void {
|
|
@@ -346,21 +372,7 @@ function ContextMenuView({
|
|
|
346
372
|
<div key={sectionKey} role="group">
|
|
347
373
|
{si > 0 ? <div className="dc-menu-sep" aria-hidden="true" /> : null}
|
|
348
374
|
{section.map((item) => (
|
|
349
|
-
<
|
|
350
|
-
key={item.id}
|
|
351
|
-
type="button"
|
|
352
|
-
role="menuitem"
|
|
353
|
-
disabled={item.disabled}
|
|
354
|
-
className={`dc-menu-item${item.destructive ? ' is-destructive' : ''}`}
|
|
355
|
-
onClick={() => {
|
|
356
|
-
if (item.disabled) return;
|
|
357
|
-
item.onSelect(target);
|
|
358
|
-
onClose();
|
|
359
|
-
}}
|
|
360
|
-
>
|
|
361
|
-
<span>{item.label}</span>
|
|
362
|
-
{item.shortcut ? <span className="dc-menu-shortcut">{item.shortcut}</span> : null}
|
|
363
|
-
</button>
|
|
375
|
+
<MenuItemRow key={item.id} item={item} target={target} onClose={onClose} />
|
|
364
376
|
))}
|
|
365
377
|
</div>
|
|
366
378
|
);
|
|
@@ -368,3 +380,135 @@ function ContextMenuView({
|
|
|
368
380
|
</div>
|
|
369
381
|
);
|
|
370
382
|
}
|
|
383
|
+
|
|
384
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
385
|
+
// MenuItemRow — a single menu row. Plain rows render a button; rows with a
|
|
386
|
+
// `submenu` render a flyout (opens on hover / ArrowRight / click). Additive:
|
|
387
|
+
// existing flat registries (no `submenu`) take the plain-button path unchanged.
|
|
388
|
+
|
|
389
|
+
function MenuItemRow({
|
|
390
|
+
item,
|
|
391
|
+
target,
|
|
392
|
+
onClose,
|
|
393
|
+
}: {
|
|
394
|
+
item: MenuItem;
|
|
395
|
+
target: ContextTarget;
|
|
396
|
+
onClose: () => void;
|
|
397
|
+
}) {
|
|
398
|
+
const [subOpen, setSubOpen] = useState(false);
|
|
399
|
+
const [flip, setFlip] = useState(false);
|
|
400
|
+
const btnRef = useRef<HTMLButtonElement>(null);
|
|
401
|
+
const flyoutRef = useRef<HTMLDivElement>(null);
|
|
402
|
+
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
403
|
+
|
|
404
|
+
if (!item.submenu || item.submenu.length === 0) {
|
|
405
|
+
return (
|
|
406
|
+
<button
|
|
407
|
+
type="button"
|
|
408
|
+
role="menuitem"
|
|
409
|
+
disabled={item.disabled}
|
|
410
|
+
title={item.disabled ? item.disabledHint : undefined}
|
|
411
|
+
className={`dc-menu-item${item.destructive ? ' is-destructive' : ''}`}
|
|
412
|
+
onClick={() => {
|
|
413
|
+
if (item.disabled) return;
|
|
414
|
+
item.onSelect(target);
|
|
415
|
+
onClose();
|
|
416
|
+
}}
|
|
417
|
+
>
|
|
418
|
+
<span>{item.label}</span>
|
|
419
|
+
{item.shortcut ? <span className="dc-menu-shortcut">{item.shortcut}</span> : null}
|
|
420
|
+
</button>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const cancelClose = () => {
|
|
425
|
+
if (closeTimer.current) {
|
|
426
|
+
clearTimeout(closeTimer.current);
|
|
427
|
+
closeTimer.current = null;
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
const open = (focusFirst = false) => {
|
|
431
|
+
cancelClose();
|
|
432
|
+
const r = btnRef.current?.getBoundingClientRect();
|
|
433
|
+
if (r && typeof window !== 'undefined') setFlip(r.right + 200 > window.innerWidth);
|
|
434
|
+
setSubOpen(true);
|
|
435
|
+
if (focusFirst) {
|
|
436
|
+
setTimeout(() => {
|
|
437
|
+
flyoutRef.current
|
|
438
|
+
?.querySelector<HTMLButtonElement>('button.dc-menu-item:not([disabled])')
|
|
439
|
+
?.focus();
|
|
440
|
+
}, 0);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
const scheduleClose = () => {
|
|
444
|
+
cancelClose();
|
|
445
|
+
closeTimer.current = setTimeout(() => setSubOpen(false), 140);
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<div className="dc-menu-sub" onMouseEnter={() => open()} onMouseLeave={scheduleClose}>
|
|
450
|
+
<button
|
|
451
|
+
ref={btnRef}
|
|
452
|
+
type="button"
|
|
453
|
+
role="menuitem"
|
|
454
|
+
aria-haspopup="menu"
|
|
455
|
+
aria-expanded={subOpen}
|
|
456
|
+
disabled={item.disabled}
|
|
457
|
+
className="dc-menu-item"
|
|
458
|
+
onClick={() => (subOpen ? setSubOpen(false) : open())}
|
|
459
|
+
onKeyDown={(e) => {
|
|
460
|
+
if (e.key === 'ArrowRight' || e.key === 'Enter' || e.key === ' ') {
|
|
461
|
+
e.preventDefault();
|
|
462
|
+
open(true);
|
|
463
|
+
} else if (e.key === 'ArrowLeft' || e.key === 'Escape') {
|
|
464
|
+
if (subOpen) {
|
|
465
|
+
e.stopPropagation();
|
|
466
|
+
setSubOpen(false);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}}
|
|
470
|
+
>
|
|
471
|
+
<span>{item.label}</span>
|
|
472
|
+
<span className="dc-menu-caret" aria-hidden="true">
|
|
473
|
+
▸
|
|
474
|
+
</span>
|
|
475
|
+
</button>
|
|
476
|
+
{subOpen ? (
|
|
477
|
+
<div
|
|
478
|
+
ref={flyoutRef}
|
|
479
|
+
className={`dc-menu-flyout${flip ? ' is-flip' : ''}`}
|
|
480
|
+
role="menu"
|
|
481
|
+
onMouseEnter={cancelClose}
|
|
482
|
+
onMouseLeave={scheduleClose}
|
|
483
|
+
>
|
|
484
|
+
{item.submenu.map((sub) => (
|
|
485
|
+
<button
|
|
486
|
+
key={sub.id}
|
|
487
|
+
type="button"
|
|
488
|
+
role="menuitem"
|
|
489
|
+
disabled={sub.disabled}
|
|
490
|
+
title={sub.disabled ? sub.disabledHint : undefined}
|
|
491
|
+
className={`dc-menu-item${sub.destructive ? ' is-destructive' : ''}`}
|
|
492
|
+
onClick={() => {
|
|
493
|
+
if (sub.disabled) return;
|
|
494
|
+
sub.onSelect(target);
|
|
495
|
+
onClose();
|
|
496
|
+
}}
|
|
497
|
+
onKeyDown={(e) => {
|
|
498
|
+
if (e.key === 'ArrowLeft' || e.key === 'Escape') {
|
|
499
|
+
e.preventDefault();
|
|
500
|
+
e.stopPropagation();
|
|
501
|
+
setSubOpen(false);
|
|
502
|
+
btnRef.current?.focus();
|
|
503
|
+
}
|
|
504
|
+
}}
|
|
505
|
+
>
|
|
506
|
+
<span>{sub.label}</span>
|
|
507
|
+
{sub.shortcut ? <span className="dc-menu-shortcut">{sub.shortcut}</span> : null}
|
|
508
|
+
</button>
|
|
509
|
+
))}
|
|
510
|
+
</div>
|
|
511
|
+
) : null}
|
|
512
|
+
</div>
|
|
513
|
+
);
|
|
514
|
+
}
|
|
@@ -25,6 +25,12 @@ export interface DesignSystemEntry {
|
|
|
25
25
|
newComponentDir?: string;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export interface LinkedHub {
|
|
29
|
+
url: string;
|
|
30
|
+
linkedAt: number;
|
|
31
|
+
adopt?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
export interface DevServerConfig {
|
|
29
35
|
name: string;
|
|
30
36
|
projectLabel: string | null;
|
|
@@ -39,6 +45,7 @@ export interface DevServerConfig {
|
|
|
39
45
|
handoffTargets: unknown[];
|
|
40
46
|
newCanvasDir: string;
|
|
41
47
|
newComponentDir: string;
|
|
48
|
+
linkedHub?: LinkedHub;
|
|
42
49
|
_source: ConfigSource;
|
|
43
50
|
}
|
|
44
51
|
|
|
@@ -110,6 +117,30 @@ export interface Context {
|
|
|
110
117
|
projectLabel: string;
|
|
111
118
|
paths: Paths;
|
|
112
119
|
bus: Bus;
|
|
120
|
+
/**
|
|
121
|
+
* T2 (9.1-A) — origin of the segregated canvas-content server (e.g.
|
|
122
|
+
* `http://localhost:51234`), set by server.ts once the second listener binds.
|
|
123
|
+
* The client reads it via /_config to build absolute, cross-origin iframe
|
|
124
|
+
* URLs. Undefined in tests / before the canvas listener boots.
|
|
125
|
+
*/
|
|
126
|
+
canvasOrigin?: string;
|
|
127
|
+
/**
|
|
128
|
+
* T2 (9.1-A) — origin of the MAIN dev-server listener (e.g.
|
|
129
|
+
* `http://localhost:4399`), set by server.ts once the primary listener binds.
|
|
130
|
+
* Used by `cspForCanvasShell` to allowlist the legit embedder in the canvas
|
|
131
|
+
* origin's `frame-ancestors`. Undefined in tests / before boot.
|
|
132
|
+
*/
|
|
133
|
+
mainOrigin?: string;
|
|
134
|
+
/**
|
|
135
|
+
* Phase 9.2 (DDR-064) — `MAUDE_SHARED_DOC` feature flag. When true, the
|
|
136
|
+
* collab room's Y.Doc becomes the SINGLE shared doc per canvas: the
|
|
137
|
+
* hub-facing HocuspocusProvider attaches to it directly (no second doc, no
|
|
138
|
+
* disk-mediated reconcile). Default `false`/undefined = the proven two-doc
|
|
139
|
+
* path = zero regression. Set by server.ts from the env; tests set it
|
|
140
|
+
* directly on the Context they construct. Threaded through Phase A; the flag
|
|
141
|
+
* gates behavior starting in Phase B.
|
|
142
|
+
*/
|
|
143
|
+
sharedDoc?: boolean;
|
|
113
144
|
}
|
|
114
145
|
|
|
115
146
|
function resolveRepoRoot(): string {
|
|
@@ -41,14 +41,14 @@ const CTX_TOOLBAR_CSS = `
|
|
|
41
41
|
align-items: stretch;
|
|
42
42
|
gap: 2px;
|
|
43
43
|
padding: 4px;
|
|
44
|
-
background: var(--
|
|
45
|
-
border: 1px solid var(--
|
|
44
|
+
background: var(--maude-chrome-bg-0, #ffffff);
|
|
45
|
+
border: 1px solid var(--maude-chrome-fg-0, #1c1917);
|
|
46
46
|
border-radius: 8px;
|
|
47
|
-
box-shadow: 0 6px 24px color-mix(in oklab,
|
|
48
|
-
font-family: var(--
|
|
47
|
+
box-shadow: 0 6px 24px var(--maude-chrome-shadow, color-mix(in oklab, #1c1917 10%, transparent));
|
|
48
|
+
font-family: var(--maude-chrome-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
49
49
|
font-size: 11px;
|
|
50
50
|
letter-spacing: 0.02em;
|
|
51
|
-
color: var(--
|
|
51
|
+
color: var(--maude-chrome-fg-0, #1a1a1a);
|
|
52
52
|
user-select: none;
|
|
53
53
|
opacity: 0;
|
|
54
54
|
transition: opacity 100ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
@@ -72,8 +72,8 @@ const CTX_TOOLBAR_CSS = `
|
|
|
72
72
|
}
|
|
73
73
|
.dc-elem-ctx-tb .dc-elem-ctx-count {
|
|
74
74
|
padding: 4px 8px 4px 10px;
|
|
75
|
-
color: var(--fg-1, rgba(40,30,20,0.7));
|
|
76
|
-
border-right: 1px solid var(--
|
|
75
|
+
color: var(--maude-chrome-fg-1, rgba(40,30,20,0.7));
|
|
76
|
+
border-right: 1px solid var(--maude-chrome-border, rgba(0,0,0,0.08));
|
|
77
77
|
margin-right: 2px;
|
|
78
78
|
font-variant-numeric: tabular-nums;
|
|
79
79
|
}
|