@1agh/maude 0.20.0 → 0.22.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 +7 -0
- package/cli/bin/maude.mjs +5 -1
- package/cli/commands/design-link.test.mjs +207 -0
- package/cli/commands/design.mjs +42 -12
- package/cli/commands/doctor.mjs +361 -0
- package/cli/commands/doctor.test.mjs +185 -0
- package/cli/commands/help.mjs +24 -0
- package/cli/commands/hub.mjs +245 -0
- package/cli/commands/hub.test.mjs +87 -0
- package/cli/lib/config-lint.mjs +141 -0
- package/cli/lib/config-lint.test.mjs +117 -0
- package/cli/lib/design-link.mjs +216 -0
- package/cli/lib/hubs-config.mjs +123 -0
- package/cli/lib/hubs-config.test.mjs +100 -0
- package/cli/lib/preflight.mjs +232 -0
- package/cli/lib/stack-detect.mjs +344 -0
- package/cli/lib/stack-detect.test.mjs +121 -0
- package/package.json +16 -8
- package/plugins/design/dependencies.json +147 -0
- package/plugins/design/dependencies.schema.json +107 -0
- package/plugins/design/dev-server/ai-banner.tsx +188 -0
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +5 -5
- package/plugins/design/dev-server/annotations-layer.tsx +52 -12
- package/plugins/design/dev-server/api.ts +17 -1
- package/plugins/design/dev-server/artboard-marquee.tsx +2 -2
- package/plugins/design/dev-server/bin/preflight.sh +32 -0
- package/plugins/design/dev-server/bin/runtime-health.sh +169 -0
- package/plugins/design/dev-server/canvas-lib.tsx +33 -7
- package/plugins/design/dev-server/canvas-shell.tsx +127 -9
- package/plugins/design/dev-server/client/app.jsx +72 -0
- package/plugins/design/dev-server/collab/ai-activity.ts +127 -0
- package/plugins/design/dev-server/collab/git-lifecycle.ts +124 -0
- package/plugins/design/dev-server/collab/index.ts +47 -0
- package/plugins/design/dev-server/collab/persistence.ts +123 -0
- package/plugins/design/dev-server/collab/protocol.ts +108 -0
- package/plugins/design/dev-server/collab/registry.ts +110 -0
- package/plugins/design/dev-server/collab/room.ts +215 -0
- package/plugins/design/dev-server/comments-overlay.tsx +29 -0
- package/plugins/design/dev-server/contextual-toolbar.tsx +1 -1
- package/plugins/design/dev-server/cursors-overlay.tsx +296 -0
- package/plugins/design/dev-server/dist/client.bundle.js +75 -3
- package/plugins/design/dev-server/dist/runtime/lib0_decoding.js +624 -0
- package/plugins/design/dev-server/dist/runtime/lib0_encoding.js +608 -0
- package/plugins/design/dev-server/dist/runtime/y-protocols_awareness.js +380 -0
- package/plugins/design/dev-server/dist/runtime/y-protocols_sync.js +104 -0
- package/plugins/design/dev-server/dist/runtime/yjs.js +6691 -0
- package/plugins/design/dev-server/export-dialog.tsx +1 -1
- package/plugins/design/dev-server/hmr-broadcast.ts +15 -2
- package/plugins/design/dev-server/http.ts +64 -1
- package/plugins/design/dev-server/marquee-overlay.tsx +2 -2
- package/plugins/design/dev-server/participants-chrome.tsx +261 -0
- package/plugins/design/dev-server/runtime-bundle.ts +19 -0
- package/plugins/design/dev-server/server.ts +78 -11
- package/plugins/design/dev-server/test/ai-activity.test.ts +113 -0
- package/plugins/design/dev-server/test/collab-annotations-bridge.test.ts +55 -0
- package/plugins/design/dev-server/test/collab-bridge.test.ts +81 -0
- package/plugins/design/dev-server/test/collab-loopback.test.ts +63 -0
- package/plugins/design/dev-server/test/collab-protocol.test.ts +146 -0
- package/plugins/design/dev-server/test/collab-room.test.ts +182 -0
- package/plugins/design/dev-server/test/collab-stress.test.ts +121 -0
- package/plugins/design/dev-server/test/git-lifecycle.test.ts +101 -0
- package/plugins/design/dev-server/test/participants-chrome.test.ts +30 -0
- package/plugins/design/dev-server/test/use-collab.test.ts +71 -0
- package/plugins/design/dev-server/tool-palette.tsx +7 -7
- package/plugins/design/dev-server/use-annotation-resize.tsx +1 -1
- package/plugins/design/dev-server/use-collab.tsx +478 -0
- package/plugins/design/dev-server/ws.ts +123 -7
- package/plugins/design/templates/_shell.html +37 -1
- package/plugins/flow/.claude-plugin/config.schema.json +12 -0
- package/plugins/flow/dependencies.json +143 -0
- package/plugins/flow/dependencies.schema.json +107 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1agh/maude",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Marketplace of Claude Code plugins by Michal Dovrtěl: `design` (canvas-first design iteration) + `flow` (generic agentic workflow loop with .ai second brain). Ships the `maude` CLI (with `mdcc` legacy alias) to scaffold workspace, run the design dev server, and manage configs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -41,18 +41,22 @@
|
|
|
41
41
|
"prepublishOnly": "bash scripts/check-version-parity.sh"
|
|
42
42
|
},
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"@1agh/maude-darwin-arm64": "0.
|
|
45
|
-
"@1agh/maude-darwin-x64": "0.
|
|
46
|
-
"@1agh/maude-linux-arm64": "0.
|
|
47
|
-
"@1agh/maude-linux-arm64-musl": "0.
|
|
48
|
-
"@1agh/maude-linux-x64": "0.
|
|
49
|
-
"@1agh/maude-linux-x64-musl": "0.
|
|
50
|
-
"@1agh/maude-win32-x64": "0.
|
|
44
|
+
"@1agh/maude-darwin-arm64": "0.22.0",
|
|
45
|
+
"@1agh/maude-darwin-x64": "0.22.0",
|
|
46
|
+
"@1agh/maude-linux-arm64": "0.22.0",
|
|
47
|
+
"@1agh/maude-linux-arm64-musl": "0.22.0",
|
|
48
|
+
"@1agh/maude-linux-x64": "0.22.0",
|
|
49
|
+
"@1agh/maude-linux-x64-musl": "0.22.0",
|
|
50
|
+
"@1agh/maude-win32-x64": "0.22.0"
|
|
51
51
|
},
|
|
52
52
|
"files": [
|
|
53
53
|
"cli",
|
|
54
54
|
"plugins/design/dev-server",
|
|
55
|
+
"plugins/design/dependencies.json",
|
|
56
|
+
"plugins/design/dependencies.schema.json",
|
|
55
57
|
"plugins/design/templates",
|
|
58
|
+
"plugins/flow/dependencies.json",
|
|
59
|
+
"plugins/flow/dependencies.schema.json",
|
|
56
60
|
"plugins/flow/templates",
|
|
57
61
|
"plugins/flow/.claude-plugin/config.schema.json",
|
|
58
62
|
"LICENSE",
|
|
@@ -84,5 +88,9 @@
|
|
|
84
88
|
"@biomejs/biome": "^1.9.4",
|
|
85
89
|
"@changesets/cli": "^2.27.10",
|
|
86
90
|
"@playwright/test": "^1.60.0"
|
|
91
|
+
},
|
|
92
|
+
"dependencies": {
|
|
93
|
+
"ajv": "^8.20.0",
|
|
94
|
+
"ajv-formats": "^3.0.1"
|
|
87
95
|
}
|
|
88
96
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./dependencies.schema.json",
|
|
3
|
+
"version": "1",
|
|
4
|
+
"plugin": "design",
|
|
5
|
+
"dependencies": [
|
|
6
|
+
{
|
|
7
|
+
"id": "node",
|
|
8
|
+
"type": "cli",
|
|
9
|
+
"hardness": "hard",
|
|
10
|
+
"check": {
|
|
11
|
+
"command": "node --version",
|
|
12
|
+
"expectExit": 0,
|
|
13
|
+
"minVersion": "20.0.0"
|
|
14
|
+
},
|
|
15
|
+
"install": {
|
|
16
|
+
"preferred": "https://nodejs.org/en/download",
|
|
17
|
+
"darwin": "brew install node@22",
|
|
18
|
+
"linux": "https://nodejs.org/en/download/package-manager"
|
|
19
|
+
},
|
|
20
|
+
"autoInstall": false,
|
|
21
|
+
"usedBy": ["commands/init.md", "commands/browse.md", "dev-server/server.ts"],
|
|
22
|
+
"docsUrl": "https://nodejs.org"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "git",
|
|
26
|
+
"type": "cli",
|
|
27
|
+
"hardness": "hard",
|
|
28
|
+
"check": {
|
|
29
|
+
"command": "git --version",
|
|
30
|
+
"expectExit": 0
|
|
31
|
+
},
|
|
32
|
+
"install": {
|
|
33
|
+
"darwin": "brew install git",
|
|
34
|
+
"linux": "apt-get install -y git || dnf install -y git"
|
|
35
|
+
},
|
|
36
|
+
"autoInstall": false,
|
|
37
|
+
"usedBy": ["commands/init.md", "dev-server/collab/git-lifecycle.ts"],
|
|
38
|
+
"docsUrl": "https://git-scm.com"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "bun",
|
|
42
|
+
"type": "cli",
|
|
43
|
+
"hardness": "hard",
|
|
44
|
+
"check": {
|
|
45
|
+
"command": "bun --version",
|
|
46
|
+
"expectExit": 0,
|
|
47
|
+
"minVersion": "1.3.0"
|
|
48
|
+
},
|
|
49
|
+
"install": {
|
|
50
|
+
"preferred": "curl -fsSL https://bun.sh/install | bash",
|
|
51
|
+
"darwin": "brew install oven-sh/bun/bun",
|
|
52
|
+
"win32": "powershell -c \"irm bun.sh/install.ps1 | iex\""
|
|
53
|
+
},
|
|
54
|
+
"autoInstall": false,
|
|
55
|
+
"fallbackBehavior": "dev-server is migrating to Bun authoritatively (DDR-009). Some build steps (bun run build.ts, bun test) require Bun and have no Node fallback.",
|
|
56
|
+
"usedBy": ["dev-server/package.json", "dev-server/build.ts", "dev-server/bin/smoke.sh"],
|
|
57
|
+
"docsUrl": "https://bun.com"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "agent-browser",
|
|
61
|
+
"type": "cli",
|
|
62
|
+
"hardness": "soft",
|
|
63
|
+
"check": {
|
|
64
|
+
"command": "agent-browser --version",
|
|
65
|
+
"expectExit": 0
|
|
66
|
+
},
|
|
67
|
+
"install": {
|
|
68
|
+
"preferred": "npm i -g @anthropic-ai/agent-browser"
|
|
69
|
+
},
|
|
70
|
+
"autoInstall": true,
|
|
71
|
+
"fallbackBehavior": "Screenshot helpers fall back to `npx playwright` (slower, less reliable). /design:smoke and several critics need agent-browser for full coverage.",
|
|
72
|
+
"usedBy": [
|
|
73
|
+
"commands/screenshot.md",
|
|
74
|
+
"commands/new.md",
|
|
75
|
+
"commands/edit.md",
|
|
76
|
+
"commands/smoke.md",
|
|
77
|
+
"dev-server/bin/screenshot.sh",
|
|
78
|
+
"dev-server/bin/smoke.sh",
|
|
79
|
+
"agents/design-critic.md",
|
|
80
|
+
"agents/graphic-design-critic.md",
|
|
81
|
+
"agents/a11y-critic.md"
|
|
82
|
+
],
|
|
83
|
+
"docsUrl": "https://github.com/anthropics/agent-browser"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "playwright",
|
|
87
|
+
"type": "cli",
|
|
88
|
+
"hardness": "soft",
|
|
89
|
+
"check": {
|
|
90
|
+
"command": "npx --no-install playwright --version",
|
|
91
|
+
"expectExit": 0
|
|
92
|
+
},
|
|
93
|
+
"install": {
|
|
94
|
+
"preferred": "npm i -g playwright && npx playwright install chromium"
|
|
95
|
+
},
|
|
96
|
+
"autoInstall": false,
|
|
97
|
+
"fallbackBehavior": "Screenshot scripts use playwright as a fallback when agent-browser is missing. Without either, screenshot helpers will fail.",
|
|
98
|
+
"usedBy": ["dev-server/bin/_screenshot-playwright.mjs", "dev-server/bin/screenshot.sh"],
|
|
99
|
+
"docsUrl": "https://playwright.dev"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"id": "maude",
|
|
103
|
+
"type": "cli",
|
|
104
|
+
"hardness": "soft",
|
|
105
|
+
"check": {
|
|
106
|
+
"command": "maude --version",
|
|
107
|
+
"expectExit": 0
|
|
108
|
+
},
|
|
109
|
+
"install": {
|
|
110
|
+
"preferred": "npm i -g @1agh/maude"
|
|
111
|
+
},
|
|
112
|
+
"autoInstall": false,
|
|
113
|
+
"fallbackBehavior": "Slash commands still work; `maude` CLI provides faster scaffolding (init / config / design serve) without going through Claude.",
|
|
114
|
+
"usedBy": ["commands/init.md", "skills/design/SKILL.md"],
|
|
115
|
+
"docsUrl": "https://github.com/1aGh/maude"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"id": "jq",
|
|
119
|
+
"type": "system-tool",
|
|
120
|
+
"hardness": "soft",
|
|
121
|
+
"check": {
|
|
122
|
+
"command": "jq --version",
|
|
123
|
+
"expectExit": 0
|
|
124
|
+
},
|
|
125
|
+
"install": {
|
|
126
|
+
"darwin": "brew install jq",
|
|
127
|
+
"linux": "apt-get install -y jq || dnf install -y jq",
|
|
128
|
+
"win32": "winget install jqlang.jq"
|
|
129
|
+
},
|
|
130
|
+
"autoInstall": false,
|
|
131
|
+
"fallbackBehavior": "Slash commands' bash blocks that read .ai/workflows.config.json via jq degrade to inline node one-liners; harder to read but functional.",
|
|
132
|
+
"usedBy": ["dev-server/bin/bootstrap-check.sh", "dev-server/bin/server-up.sh"]
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"id": "askuserquestion-mcp",
|
|
136
|
+
"type": "mcp",
|
|
137
|
+
"hardness": "soft",
|
|
138
|
+
"check": {
|
|
139
|
+
"mcp": "AskUserQuestion",
|
|
140
|
+
"tool": "ask"
|
|
141
|
+
},
|
|
142
|
+
"fallbackBehavior": "Discovery + critic-panel rounds fall back to numbered-prose chat. Slower, fewer options per round, no preview side-pane.",
|
|
143
|
+
"usedBy": ["skills/design-system/SKILL.md", "commands/setup-ds.md", "commands/new.md"],
|
|
144
|
+
"docsUrl": "https://docs.claude.com/en/docs/claude-code"
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "maude plugin — runtime dependency manifest",
|
|
4
|
+
"description": "Declares CLI binaries, MCP servers, and runtime packages the plugin's commands / agents / skills shell out to. Consumed by maude doctor (cli/commands/doctor.mjs) and preflight.sh / preflight.mjs. Schema is shared between plugins — each plugin ships its own dependencies.json + (symlink to) this schema.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["version", "plugin", "dependencies"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"version": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"const": "1",
|
|
12
|
+
"description": "Manifest schema version. Bumped when the dependency entry shape changes incompatibly."
|
|
13
|
+
},
|
|
14
|
+
"plugin": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"minLength": 1,
|
|
17
|
+
"description": "Plugin name (matches plugins/<plugin>/.claude-plugin/plugin.json `name`)."
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"minItems": 0,
|
|
22
|
+
"items": { "$ref": "#/$defs/Dependency" }
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"$defs": {
|
|
26
|
+
"Dependency": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"additionalProperties": false,
|
|
29
|
+
"required": ["id", "type", "hardness", "check"],
|
|
30
|
+
"properties": {
|
|
31
|
+
"id": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"pattern": "^[a-z0-9][a-z0-9._-]*$",
|
|
34
|
+
"description": "Unique identifier within the manifest. Lowercase, hyphenated."
|
|
35
|
+
},
|
|
36
|
+
"type": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"enum": ["cli", "mcp", "node-package", "bun-package", "system-tool"],
|
|
39
|
+
"description": "What kind of dependency. `cli` = on PATH; `mcp` = MCP server / tool; `node-package` / `bun-package` = installed via npm/bun; `system-tool` = OS-provided (curl, jq)."
|
|
40
|
+
},
|
|
41
|
+
"hardness": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"enum": ["hard", "soft"],
|
|
44
|
+
"description": "`hard` = missing blocks plugin use; `soft` = graceful degradation possible (fallback path, warning only)."
|
|
45
|
+
},
|
|
46
|
+
"check": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"additionalProperties": false,
|
|
49
|
+
"description": "How to detect presence. For CLI/system-tool/package: shell command + expected exit. For MCP: tool name to probe.",
|
|
50
|
+
"properties": {
|
|
51
|
+
"command": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "Shell command executed via `bash -c`. Pipes allowed. Defaults to `<id> --version`."
|
|
54
|
+
},
|
|
55
|
+
"expectExit": {
|
|
56
|
+
"type": "integer",
|
|
57
|
+
"default": 0,
|
|
58
|
+
"description": "Expected exit code. Anything else = missing/broken."
|
|
59
|
+
},
|
|
60
|
+
"minVersion": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"description": "Minimum semver. Comparator extracts `vX.Y.Z` from check.command stdout."
|
|
63
|
+
},
|
|
64
|
+
"mcp": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "MCP server name. For type=mcp only."
|
|
67
|
+
},
|
|
68
|
+
"tool": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"description": "Specific tool name on the MCP server. For type=mcp only."
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"install": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"additionalProperties": false,
|
|
77
|
+
"description": "Install commands per platform. `preferred` is OS-agnostic / fallback; per-platform keys override.",
|
|
78
|
+
"properties": {
|
|
79
|
+
"preferred": { "type": "string" },
|
|
80
|
+
"darwin": { "type": "string" },
|
|
81
|
+
"linux": { "type": "string" },
|
|
82
|
+
"win32": { "type": "string" }
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"autoInstall": {
|
|
86
|
+
"type": "boolean",
|
|
87
|
+
"default": false,
|
|
88
|
+
"description": "If true, `maude doctor --fix` may run install.preferred after per-dep confirmation prompt."
|
|
89
|
+
},
|
|
90
|
+
"fallbackBehavior": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"description": "What the plugin does when this soft dep is missing. Human-readable; surfaces in maude doctor's report."
|
|
93
|
+
},
|
|
94
|
+
"usedBy": {
|
|
95
|
+
"type": "array",
|
|
96
|
+
"items": { "type": "string" },
|
|
97
|
+
"description": "Repo-relative paths (or path globs) of plugin files that reference this dependency. Aids debugging — doctor surfaces this when explaining a miss."
|
|
98
|
+
},
|
|
99
|
+
"docsUrl": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"format": "uri",
|
|
102
|
+
"description": "Where to read more."
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file ai-banner.tsx — yellow "Claude is editing this canvas" banner
|
|
3
|
+
* @scope plugins/design/dev-server/ai-banner.tsx
|
|
4
|
+
* @purpose Mounted by canvas-shell. Subscribes to the inspector WS
|
|
5
|
+
* `ai-activity` event for the canvas's own file path; shows a
|
|
6
|
+
* non-modal yellow banner while AI work is in progress and clears
|
|
7
|
+
* on explicit /end OR 30-second heartbeat-grace expiry.
|
|
8
|
+
*
|
|
9
|
+
* The banner is a soft notice, NOT a lock — the user can still type, click,
|
|
10
|
+
* drag annotations, etc. It exists so the user doesn't unknowingly fight a
|
|
11
|
+
* `/design:edit` that's about to rewrite their HTML.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useEffect, useState } from 'react';
|
|
15
|
+
|
|
16
|
+
function deriveFile(): string | null {
|
|
17
|
+
if (typeof window === 'undefined') return null;
|
|
18
|
+
try {
|
|
19
|
+
const p = window.location.pathname;
|
|
20
|
+
if (p === '/_canvas-shell.html' || p === '/_canvas-shell') {
|
|
21
|
+
const qs = new URLSearchParams(window.location.search);
|
|
22
|
+
const canvas = qs.get('canvas') ?? '';
|
|
23
|
+
const designRel = (qs.get('designRel') ?? '.design').replace(/^\/+|\/+$/g, '');
|
|
24
|
+
return canvas ? `${designRel}/${canvas}` : null;
|
|
25
|
+
}
|
|
26
|
+
return decodeURIComponent(p).replace(/^\//, '');
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const BANNER_CSS = `
|
|
33
|
+
.dc-ai-banner {
|
|
34
|
+
position: fixed;
|
|
35
|
+
top: 14px;
|
|
36
|
+
left: 50%;
|
|
37
|
+
transform: translateX(-50%);
|
|
38
|
+
z-index: 10001;
|
|
39
|
+
padding: 8px 14px;
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 10px;
|
|
43
|
+
background: var(--maude-hud-accent-tint, oklch(92% 0.040 55));
|
|
44
|
+
color: var(--fg-0, #111);
|
|
45
|
+
border: 1px solid var(--border-default, rgba(0,0,0,0.16));
|
|
46
|
+
border-radius: var(--radius-md, 4px);
|
|
47
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
48
|
+
font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
|
|
49
|
+
font-weight: 500;
|
|
50
|
+
font-size: 12px;
|
|
51
|
+
line-height: 1.2;
|
|
52
|
+
letter-spacing: 0.01em;
|
|
53
|
+
pointer-events: none;
|
|
54
|
+
user-select: none;
|
|
55
|
+
}
|
|
56
|
+
.dc-ai-banner__dot {
|
|
57
|
+
width: 7px;
|
|
58
|
+
height: 7px;
|
|
59
|
+
border-radius: 50%;
|
|
60
|
+
background: var(--maude-hud-accent, oklch(56% 0.170 50));
|
|
61
|
+
flex: 0 0 auto;
|
|
62
|
+
animation: dc-ai-banner-pulse 1.4s ease-in-out infinite;
|
|
63
|
+
}
|
|
64
|
+
@keyframes dc-ai-banner-pulse {
|
|
65
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
66
|
+
50% { opacity: 0.5; transform: scale(0.75); }
|
|
67
|
+
}
|
|
68
|
+
@media (prefers-reduced-motion: reduce) {
|
|
69
|
+
.dc-ai-banner__dot { animation: none; }
|
|
70
|
+
}
|
|
71
|
+
`.trim();
|
|
72
|
+
|
|
73
|
+
function ensureBannerStyles(): void {
|
|
74
|
+
if (typeof document === 'undefined') return;
|
|
75
|
+
if (document.getElementById('dc-ai-banner-css')) return;
|
|
76
|
+
const s = document.createElement('style');
|
|
77
|
+
s.id = 'dc-ai-banner-css';
|
|
78
|
+
s.textContent = BANNER_CSS;
|
|
79
|
+
document.head.appendChild(s);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface AiEntry {
|
|
83
|
+
file: string;
|
|
84
|
+
author: string;
|
|
85
|
+
startedAt: number;
|
|
86
|
+
lastHeartbeat: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function AiBanner(): JSX.Element | null {
|
|
90
|
+
ensureBannerStyles();
|
|
91
|
+
const [entry, setEntry] = useState<AiEntry | null>(null);
|
|
92
|
+
const file = deriveFile();
|
|
93
|
+
|
|
94
|
+
// Mount-time backfill — the bus only emits on changes, so a freshly opened
|
|
95
|
+
// tab needs to GET the snapshot to learn that an edit is already in flight.
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!file) return;
|
|
98
|
+
let cancelled = false;
|
|
99
|
+
fetch('/_api/ai')
|
|
100
|
+
.then((r) => r.json())
|
|
101
|
+
.then((j: { entries?: AiEntry[] }) => {
|
|
102
|
+
if (cancelled) return;
|
|
103
|
+
const hit = (j.entries ?? []).find((e) => e.file === file);
|
|
104
|
+
if (hit) setEntry(hit);
|
|
105
|
+
})
|
|
106
|
+
.catch(() => {
|
|
107
|
+
/* offline / restart — silently no-op */
|
|
108
|
+
});
|
|
109
|
+
return () => {
|
|
110
|
+
cancelled = true;
|
|
111
|
+
};
|
|
112
|
+
}, [file]);
|
|
113
|
+
|
|
114
|
+
// Subscribe to the parent shell's WS broadcast — when the inspector channel
|
|
115
|
+
// receives an `ai-activity` event for OUR file path, update local state.
|
|
116
|
+
// The shell relays via postMessage so we don't need our own WS connection
|
|
117
|
+
// (the iframe already piggybacks on the parent's inspector session).
|
|
118
|
+
//
|
|
119
|
+
// FALLBACK — when the canvas is opened standalone (export iframe, _shell.html
|
|
120
|
+
// direct URL), no parent WS exists. We open a thin reader-only inspector WS
|
|
121
|
+
// ourselves and watch for the matching event.
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!file) return;
|
|
124
|
+
if (typeof window === 'undefined') return;
|
|
125
|
+
|
|
126
|
+
// postMessage relay path — parent sends `ai-activity` events.
|
|
127
|
+
const onMessage = (e: MessageEvent) => {
|
|
128
|
+
const m = e.data as { dgn?: string; file?: string; entry?: AiEntry | null } | null;
|
|
129
|
+
if (!m || typeof m !== 'object') return;
|
|
130
|
+
if (m.dgn !== 'ai-activity') return;
|
|
131
|
+
if (m.file !== file) return;
|
|
132
|
+
setEntry(m.entry ?? null);
|
|
133
|
+
};
|
|
134
|
+
window.addEventListener('message', onMessage);
|
|
135
|
+
|
|
136
|
+
// Standalone fallback — if no parent is relaying, dial /_ws directly.
|
|
137
|
+
let standaloneWs: WebSocket | null = null;
|
|
138
|
+
let reconnect: ReturnType<typeof setTimeout> | null = null;
|
|
139
|
+
function connectStandalone() {
|
|
140
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
141
|
+
const ws = new WebSocket(`${proto}//${location.host}/_ws`);
|
|
142
|
+
standaloneWs = ws;
|
|
143
|
+
ws.addEventListener('message', (ev) => {
|
|
144
|
+
try {
|
|
145
|
+
const data = typeof ev.data === 'string' ? ev.data : '';
|
|
146
|
+
if (!data) return;
|
|
147
|
+
const m = JSON.parse(data) as { type?: string; file?: string; entry?: AiEntry | null };
|
|
148
|
+
if (m.type !== 'ai-activity') return;
|
|
149
|
+
if (m.file !== file) return;
|
|
150
|
+
setEntry(m.entry ?? null);
|
|
151
|
+
} catch {
|
|
152
|
+
/* not a JSON frame (binary from collab WS) — ignore */
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
ws.addEventListener('close', () => {
|
|
156
|
+
standaloneWs = null;
|
|
157
|
+
reconnect = setTimeout(connectStandalone, 2000);
|
|
158
|
+
});
|
|
159
|
+
ws.addEventListener('error', () => {
|
|
160
|
+
/* close handler reconnects */
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
// Only open the standalone WS when we're not inside an iframe — the parent
|
|
164
|
+
// already drives the postMessage relay for embedded canvases.
|
|
165
|
+
if (window.parent === window) connectStandalone();
|
|
166
|
+
|
|
167
|
+
return () => {
|
|
168
|
+
window.removeEventListener('message', onMessage);
|
|
169
|
+
if (reconnect) clearTimeout(reconnect);
|
|
170
|
+
if (standaloneWs && standaloneWs.readyState === WebSocket.OPEN) {
|
|
171
|
+
try {
|
|
172
|
+
standaloneWs.close();
|
|
173
|
+
} catch {
|
|
174
|
+
/* ignore */
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}, [file]);
|
|
179
|
+
|
|
180
|
+
if (!entry) return null;
|
|
181
|
+
return (
|
|
182
|
+
// biome-ignore lint/a11y/useSemanticElements: <output> is a form-result element; role="status" on a div is the canonical live-region banner pattern (APG).
|
|
183
|
+
<div className="dc-ai-banner" role="status" aria-live="polite">
|
|
184
|
+
<span className="dc-ai-banner__dot" aria-hidden="true" />
|
|
185
|
+
<span>{entry.author} is editing this canvas — your changes may conflict</span>
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -73,7 +73,7 @@ const TOOLBAR_CSS = `
|
|
|
73
73
|
appearance: none;
|
|
74
74
|
}
|
|
75
75
|
.dc-annot-ctx-sw[aria-pressed="true"] {
|
|
76
|
-
box-shadow: 0 0 0 2px var(--accent, #d63b1f);
|
|
76
|
+
box-shadow: 0 0 0 2px var(--maude-hud-accent, #d63b1f);
|
|
77
77
|
border-color: transparent;
|
|
78
78
|
}
|
|
79
79
|
.dc-annot-ctx-fill--none {
|
|
@@ -103,8 +103,8 @@ const TOOLBAR_CSS = `
|
|
|
103
103
|
cursor: pointer;
|
|
104
104
|
}
|
|
105
105
|
.dc-annot-ctx-btn[aria-pressed="true"] {
|
|
106
|
-
background: var(--accent, #d63b1f);
|
|
107
|
-
color: var(--accent-fg, #fff);
|
|
106
|
+
background: var(--maude-hud-accent, #d63b1f);
|
|
107
|
+
color: var(--maude-hud-accent-fg, #fff);
|
|
108
108
|
border-color: transparent;
|
|
109
109
|
}
|
|
110
110
|
.dc-annot-ctx-btn:hover { background: rgba(0,0,0,0.04); }
|
|
@@ -134,8 +134,8 @@ const TOOLBAR_CSS = `
|
|
|
134
134
|
line-height: 1;
|
|
135
135
|
}
|
|
136
136
|
.dc-annot-ctx-mode-btn[aria-pressed="true"] {
|
|
137
|
-
background: color-mix(in oklab, var(--accent, #d63b1f) 14%, transparent);
|
|
138
|
-
color: var(--accent, #d63b1f);
|
|
137
|
+
background: color-mix(in oklab, var(--maude-hud-accent, #d63b1f) 14%, transparent);
|
|
138
|
+
color: var(--maude-hud-accent, #d63b1f);
|
|
139
139
|
font-weight: 600;
|
|
140
140
|
}
|
|
141
141
|
.dc-annot-ctx-mode-btn:not([aria-pressed="true"]):hover {
|
|
@@ -40,6 +40,7 @@ import { crossedDragThreshold } from './input-router.tsx';
|
|
|
40
40
|
import { AnnotationResizeOverlay } from './use-annotation-resize.tsx';
|
|
41
41
|
import { useAnnotationSelectionOptional } from './use-annotation-selection.tsx';
|
|
42
42
|
import { useAnnotationsVisibility } from './use-annotations-visibility.tsx';
|
|
43
|
+
import { useCollab } from './use-collab.tsx';
|
|
43
44
|
import { useSelectionSetOptional } from './use-selection-set.tsx';
|
|
44
45
|
import { useToolMode } from './use-tool-mode.tsx';
|
|
45
46
|
import { useUndoSinks, useUndoStackOptional } from './use-undo-stack.tsx';
|
|
@@ -483,11 +484,11 @@ const ANNOT_CSS = `
|
|
|
483
484
|
appearance: none;
|
|
484
485
|
}
|
|
485
486
|
.dc-annot-chrome .dc-annot-sw[aria-pressed="true"] {
|
|
486
|
-
box-shadow: 0 0 0 2px var(--accent, #d63b1f);
|
|
487
|
+
box-shadow: 0 0 0 2px var(--maude-hud-accent, #d63b1f);
|
|
487
488
|
border-color: transparent;
|
|
488
489
|
}
|
|
489
490
|
.dc-annot-chrome .dc-annot-sw:focus-visible {
|
|
490
|
-
outline: 2px solid var(--accent, #d63b1f);
|
|
491
|
+
outline: 2px solid var(--maude-hud-accent, #d63b1f);
|
|
491
492
|
outline-offset: 2px;
|
|
492
493
|
}
|
|
493
494
|
.dc-annot-chrome .dc-annot-sep {
|
|
@@ -516,7 +517,7 @@ const ANNOT_CSS = `
|
|
|
516
517
|
linear-gradient(135deg, transparent 47%, #d63b1f 47%, #d63b1f 53%, transparent 53%);
|
|
517
518
|
}
|
|
518
519
|
.dc-annot-chrome .dc-annot-fill[aria-pressed="true"] {
|
|
519
|
-
box-shadow: 0 0 0 2px var(--accent, #d63b1f);
|
|
520
|
+
box-shadow: 0 0 0 2px var(--maude-hud-accent, #d63b1f);
|
|
520
521
|
border-color: transparent;
|
|
521
522
|
}
|
|
522
523
|
.dc-annot-chrome .dc-annot-btn {
|
|
@@ -532,13 +533,13 @@ const ANNOT_CSS = `
|
|
|
532
533
|
text-transform: uppercase;
|
|
533
534
|
}
|
|
534
535
|
.dc-annot-chrome .dc-annot-btn[aria-pressed="true"] {
|
|
535
|
-
background: var(--accent, #d63b1f);
|
|
536
|
-
color: var(--accent-fg, #fff);
|
|
536
|
+
background: var(--maude-hud-accent, #d63b1f);
|
|
537
|
+
color: var(--maude-hud-accent-fg, #fff);
|
|
537
538
|
border-color: transparent;
|
|
538
539
|
}
|
|
539
540
|
.dc-annot-chrome .dc-annot-btn:hover { background: rgba(0,0,0,0.04); }
|
|
540
541
|
.dc-annot-chrome .dc-annot-btn:focus-visible {
|
|
541
|
-
outline: 2px solid var(--accent, #d63b1f);
|
|
542
|
+
outline: 2px solid var(--maude-hud-accent, #d63b1f);
|
|
542
543
|
outline-offset: 2px;
|
|
543
544
|
}
|
|
544
545
|
.dc-annot-input {
|
|
@@ -567,8 +568,8 @@ const ANNOT_CSS = `
|
|
|
567
568
|
/* Drag-select marquee — rendered while user is dragging to select strokes. */
|
|
568
569
|
.dc-annot-marquee {
|
|
569
570
|
pointer-events: none;
|
|
570
|
-
fill: color-mix(in oklab, var(--accent, #d63b1f) 8%, transparent);
|
|
571
|
-
stroke: var(--accent, #d63b1f);
|
|
571
|
+
fill: color-mix(in oklab, var(--maude-hud-accent, #d63b1f) 8%, transparent);
|
|
572
|
+
stroke: var(--maude-hud-accent, #d63b1f);
|
|
572
573
|
stroke-width: 1;
|
|
573
574
|
stroke-dasharray: 4 3;
|
|
574
575
|
}
|
|
@@ -689,6 +690,10 @@ export function AnnotationsLayer() {
|
|
|
689
690
|
const supportsFill = tool === 'rect' || tool === 'ellipse';
|
|
690
691
|
|
|
691
692
|
// Load existing annotations on mount.
|
|
693
|
+
// Phase 8 Task 5 — seed lastAppliedSvgRef so the first Y.Map observe (when
|
|
694
|
+
// collab connects shortly after this fetch lands) doesn't re-apply the
|
|
695
|
+
// same content we just hydrated from REST.
|
|
696
|
+
const lastAppliedSvgRef = useRef<string>('');
|
|
692
697
|
useEffect(() => {
|
|
693
698
|
const file = deriveFile();
|
|
694
699
|
fileRef.current = file;
|
|
@@ -701,7 +706,10 @@ export function AnnotationsLayer() {
|
|
|
701
706
|
.then((text) => {
|
|
702
707
|
if (cancelled) return;
|
|
703
708
|
const loaded = svgToStrokes(text);
|
|
704
|
-
if (loaded.length)
|
|
709
|
+
if (loaded.length) {
|
|
710
|
+
setStrokesState(loaded);
|
|
711
|
+
lastAppliedSvgRef.current = text;
|
|
712
|
+
}
|
|
705
713
|
})
|
|
706
714
|
.catch(() => {
|
|
707
715
|
/* network blip — start with an empty annotation set */
|
|
@@ -711,6 +719,34 @@ export function AnnotationsLayer() {
|
|
|
711
719
|
};
|
|
712
720
|
}, []);
|
|
713
721
|
|
|
722
|
+
// Phase 8 Task 5 — observe the Y.Map.annotations for live updates from
|
|
723
|
+
// other tabs. Bail when the incoming SVG STRING is identical to the one
|
|
724
|
+
// we last applied (covers the local echo round-trip without missing real
|
|
725
|
+
// foreign changes). The prior length+first/last-id check was wrong: a
|
|
726
|
+
// resize / move keeps the same id list, so all three predicates matched
|
|
727
|
+
// even though geometry changed — foreign edits silently disappeared.
|
|
728
|
+
const collab = useCollab();
|
|
729
|
+
useEffect(() => {
|
|
730
|
+
if (!collab) return;
|
|
731
|
+
const map = collab.doc.getMap<string>('annotations');
|
|
732
|
+
const apply = () => {
|
|
733
|
+
const svg = map.get('svg');
|
|
734
|
+
if (typeof svg !== 'string' || !svg) return;
|
|
735
|
+
if (svg === lastAppliedSvgRef.current) return;
|
|
736
|
+
lastAppliedSvgRef.current = svg;
|
|
737
|
+
setStrokesState(svgToStrokes(svg));
|
|
738
|
+
};
|
|
739
|
+
apply();
|
|
740
|
+
map.observe(apply);
|
|
741
|
+
return () => {
|
|
742
|
+
try {
|
|
743
|
+
map.unobserve(apply);
|
|
744
|
+
} catch {
|
|
745
|
+
/* doc destroyed before unmount */
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
}, [collab]);
|
|
749
|
+
|
|
714
750
|
const undoStack = useUndoStackOptional();
|
|
715
751
|
const undoSinks = useUndoSinks();
|
|
716
752
|
const undoStackRef = useRef(undoStack);
|
|
@@ -734,6 +770,10 @@ export function AnnotationsLayer() {
|
|
|
734
770
|
const file = fileRef.current;
|
|
735
771
|
if (!file) return Promise.resolve();
|
|
736
772
|
const svg = strokesToSvg(next);
|
|
773
|
+
// Phase 8 Task 5 — record the SVG we just authored locally so the
|
|
774
|
+
// server-broadcast echo (PUT → onAnnotationsChanged → syncRoom* →
|
|
775
|
+
// Y.Map.observe) doesn't trigger a redundant setStrokesState.
|
|
776
|
+
lastAppliedSvgRef.current = svg;
|
|
737
777
|
return fetch('/_api/annotations', {
|
|
738
778
|
method: 'PUT',
|
|
739
779
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1683,7 +1723,7 @@ function SelectionHalo({
|
|
|
1683
1723
|
width={bbox.w + pad * 2}
|
|
1684
1724
|
height={bbox.h + pad * 2}
|
|
1685
1725
|
fill="none"
|
|
1686
|
-
stroke="var(--accent, #d63b1f)"
|
|
1726
|
+
stroke="var(--maude-hud-accent, #d63b1f)"
|
|
1687
1727
|
strokeWidth={multi ? 1.5 : 2}
|
|
1688
1728
|
vectorEffect="non-scaling-stroke"
|
|
1689
1729
|
pointerEvents="none"
|
|
@@ -1738,7 +1778,7 @@ function AnnotGroupBbox({
|
|
|
1738
1778
|
width={w}
|
|
1739
1779
|
height={h}
|
|
1740
1780
|
fill="none"
|
|
1741
|
-
stroke="var(--accent, #d63b1f)"
|
|
1781
|
+
stroke="var(--maude-hud-accent, #d63b1f)"
|
|
1742
1782
|
strokeWidth={1}
|
|
1743
1783
|
strokeDasharray="4 3"
|
|
1744
1784
|
vectorEffect="non-scaling-stroke"
|
|
@@ -1751,7 +1791,7 @@ function AnnotGroupBbox({
|
|
|
1751
1791
|
y={c.y}
|
|
1752
1792
|
width={handle}
|
|
1753
1793
|
height={handle}
|
|
1754
|
-
fill="var(--accent, #d63b1f)"
|
|
1794
|
+
fill="var(--maude-hud-accent, #d63b1f)"
|
|
1755
1795
|
stroke="var(--bg-0, #ffffff)"
|
|
1756
1796
|
strokeWidth={1}
|
|
1757
1797
|
vectorEffect="non-scaling-stroke"
|