@1agh/maude 0.20.0 → 0.21.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 +350 -0
- package/cli/commands/doctor.test.mjs +185 -0
- package/cli/commands/help.mjs +24 -0
- package/cli/commands/hub.mjs +231 -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/y-protocols_awareness.js +587 -0
- package/plugins/design/dev-server/dist/runtime/y-protocols_sync.js +257 -0
- package/plugins/design/dev-server/dist/runtime/yjs.js +7107 -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 +8 -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 +35 -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
|
@@ -140,6 +140,12 @@ export interface Api {
|
|
|
140
140
|
commentsDelete(id: string): Promise<boolean>;
|
|
141
141
|
commentsAddReply(id: string, payload: { body: string; author?: string }): Promise<Comment | null>;
|
|
142
142
|
gitCommitters(): Promise<GitCommitter[]>;
|
|
143
|
+
/**
|
|
144
|
+
* Phase 8 — local `git config user.name`, cached for the process lifetime.
|
|
145
|
+
* Used by the collab client to derive a stable color hash per peer.
|
|
146
|
+
* Empty string when git is unset; the client falls back to `anonymous-<pid>`.
|
|
147
|
+
*/
|
|
148
|
+
gitCurrentUser(): Promise<string>;
|
|
143
149
|
parseMentions(text: string): string[];
|
|
144
150
|
// Canvas state
|
|
145
151
|
loadCanvasState(file: string): Promise<Record<string, unknown> | null>;
|
|
@@ -161,7 +167,15 @@ export interface Api {
|
|
|
161
167
|
appendExportHistory(entry: ExportHistoryEntry): Promise<void>;
|
|
162
168
|
}
|
|
163
169
|
|
|
164
|
-
export
|
|
170
|
+
export interface ApiHooks {
|
|
171
|
+
onCommentsChanged: (file: string) => void;
|
|
172
|
+
/** Phase 8 Task 5 — fires after a successful PUT /_api/annotations write. */
|
|
173
|
+
onAnnotationsChanged?: (file: string, svg: string) => void;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
177
|
+
const onCommentsChanged = hooks.onCommentsChanged;
|
|
178
|
+
const onAnnotationsChanged = hooks.onAnnotationsChanged;
|
|
165
179
|
const { paths, cfg } = ctx;
|
|
166
180
|
|
|
167
181
|
function fileSlug(file: string): string {
|
|
@@ -576,6 +590,7 @@ export function createApi(ctx: Context, onCommentsChanged: (file: string) => voi
|
|
|
576
590
|
// content fully, so we don't try to sanitize beyond a tag check.
|
|
577
591
|
if (!/^\s*<svg[\s>]/i.test(svg)) return false;
|
|
578
592
|
await Bun.write(annotationsPath(file), svg);
|
|
593
|
+
onAnnotationsChanged?.(file, svg);
|
|
579
594
|
return true;
|
|
580
595
|
}
|
|
581
596
|
|
|
@@ -936,6 +951,7 @@ export function createApi(ctx: Context, onCommentsChanged: (file: string) => voi
|
|
|
936
951
|
commentsDelete,
|
|
937
952
|
commentsAddReply,
|
|
938
953
|
gitCommitters,
|
|
954
|
+
gitCurrentUser,
|
|
939
955
|
parseMentions,
|
|
940
956
|
loadCanvasState,
|
|
941
957
|
saveCanvasState,
|
|
@@ -27,8 +27,8 @@ const MARQUEE_CSS = `
|
|
|
27
27
|
position: fixed;
|
|
28
28
|
pointer-events: none;
|
|
29
29
|
z-index: 5;
|
|
30
|
-
border: 1px solid var(--accent, #0d99ff);
|
|
31
|
-
background: color-mix(in oklab, var(--accent, #0d99ff) 8%, transparent);
|
|
30
|
+
border: 1px solid var(--maude-hud-accent, #0d99ff);
|
|
31
|
+
background: color-mix(in oklab, var(--maude-hud-accent, #0d99ff) 8%, transparent);
|
|
32
32
|
display: none;
|
|
33
33
|
}
|
|
34
34
|
`.trim();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# preflight.sh — design plugin dependency health check.
|
|
3
|
+
# Thin shell wrapper over `node cli/lib/preflight.mjs --plugin design`.
|
|
4
|
+
# Keeps the detection logic in one place (the .mjs lib) while exposing the
|
|
5
|
+
# bash-friendly modes (--shell-export, --warn-only) that init.md and the
|
|
6
|
+
# SessionStart hook need.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# preflight.sh # text table (humans)
|
|
10
|
+
# preflight.sh --json # machine-readable envelope
|
|
11
|
+
# preflight.sh --shell-export # `export DEPS_OK=... DEPS_MISSING=...`
|
|
12
|
+
# preflight.sh --quiet # silent on success, one-liner on miss
|
|
13
|
+
# preflight.sh --warn-only # same as --quiet (SessionStart hook)
|
|
14
|
+
#
|
|
15
|
+
# Resolution (DDR-045): never compute paths from `dirname $0` inside a
|
|
16
|
+
# `bun --compile` standalone binary — that maps to /$bunfs/root. The CLI
|
|
17
|
+
# entry stays Node, so we resolve via $CLAUDE_PLUGIN_ROOT first, with the
|
|
18
|
+
# script-dir fallback only used when running uncompiled from the repo.
|
|
19
|
+
#
|
|
20
|
+
# Exit: 0 if all hard deps pass; 1 otherwise.
|
|
21
|
+
|
|
22
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
23
|
+
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
24
|
+
PKG_ROOT="$(cd "$PLUGIN_ROOT/../.." && pwd)"
|
|
25
|
+
|
|
26
|
+
# The Node lib needs cwd to be the package root (it resolves dependencies.json
|
|
27
|
+
# via `plugins/<plugin>/dependencies.json`). Subshell so we don't mutate the
|
|
28
|
+
# caller's cwd.
|
|
29
|
+
(
|
|
30
|
+
cd "$PKG_ROOT" || exit 1
|
|
31
|
+
exec node "$PKG_ROOT/cli/lib/preflight.mjs" --plugin design "$@"
|
|
32
|
+
)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# runtime-health.sh — verify the dev-server is serving the on-disk pre-built
|
|
3
|
+
# runtime bundles, not a defective dynamic Bun.build output.
|
|
4
|
+
#
|
|
5
|
+
# Why: a stale dev-server process can cache a broken dynamic build of
|
|
6
|
+
# /_canvas-runtime/<slug>.js (e.g. a 409-line motion_react.js with a hoisting
|
|
7
|
+
# bug instead of the 10056-line pre-built). The canvas TSX parses cleanly,
|
|
8
|
+
# server returns HTTP 200, but the iframe throws at module-eval time with
|
|
9
|
+
# `ReferenceError: AcceleratedAnimation is not defined` (or similar).
|
|
10
|
+
#
|
|
11
|
+
# Parse-clean ≠ run-clean. This helper closes that gap: probe every URL the
|
|
12
|
+
# canvas-lib pulls in, compare byte-count to the disk pre-built, fail loud
|
|
13
|
+
# when the served body is suspiciously small.
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# runtime-health.sh [--port N] [--root <repo>] [--threshold 0.5]
|
|
17
|
+
# [--restart] [--quiet]
|
|
18
|
+
#
|
|
19
|
+
# Behavior:
|
|
20
|
+
# --restart → on detected defect, kill the server PID from _server.json
|
|
21
|
+
# and respawn via server-up.sh. Exit 0 if the restart heals.
|
|
22
|
+
# --quiet → silence per-bundle PASS lines; only print failures + summary.
|
|
23
|
+
#
|
|
24
|
+
# Exit codes:
|
|
25
|
+
# 0 all bundles healthy (or --restart healed the defect)
|
|
26
|
+
# 1 server unreachable / no _server.json
|
|
27
|
+
# 2 bad args
|
|
28
|
+
# 3 defective bundle detected (and either no --restart, or restart did not heal)
|
|
29
|
+
|
|
30
|
+
PORT=""
|
|
31
|
+
REPO=""
|
|
32
|
+
THRESHOLD="0.5"
|
|
33
|
+
RESTART=0
|
|
34
|
+
QUIET=0
|
|
35
|
+
|
|
36
|
+
while [ $# -gt 0 ]; do
|
|
37
|
+
case "$1" in
|
|
38
|
+
--port) PORT="$2"; shift 2 ;;
|
|
39
|
+
--root) REPO="$2"; shift 2 ;;
|
|
40
|
+
--threshold) THRESHOLD="$2"; shift 2 ;;
|
|
41
|
+
--restart) RESTART=1; shift ;;
|
|
42
|
+
--quiet) QUIET=1; shift ;;
|
|
43
|
+
--help|-h)
|
|
44
|
+
sed -n '2,30p' "$0" | sed 's/^# \?//'
|
|
45
|
+
exit 0
|
|
46
|
+
;;
|
|
47
|
+
*)
|
|
48
|
+
echo "runtime-health.sh: unknown arg '$1' (try --help)" >&2
|
|
49
|
+
exit 2
|
|
50
|
+
;;
|
|
51
|
+
esac
|
|
52
|
+
done
|
|
53
|
+
|
|
54
|
+
# ---------- repo + plugin root resolution ----------
|
|
55
|
+
if [ -z "$REPO" ]; then
|
|
56
|
+
REPO="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
57
|
+
fi
|
|
58
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
59
|
+
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
60
|
+
PREBUILT_DIR="$PLUGIN_ROOT/dev-server/dist/runtime"
|
|
61
|
+
if [ ! -d "$PREBUILT_DIR" ]; then
|
|
62
|
+
PREBUILT_DIR="$SCRIPT_DIR/../dist/runtime"
|
|
63
|
+
fi
|
|
64
|
+
if [ ! -d "$PREBUILT_DIR" ]; then
|
|
65
|
+
echo "runtime-health.sh: pre-built runtime dir not found at $PREBUILT_DIR" >&2
|
|
66
|
+
echo " (canvas runtime bundles ship in <plugin>/dev-server/dist/runtime/)" >&2
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# ---------- resolve port from _server.json if not given ----------
|
|
71
|
+
DESIGN_ROOT="$REPO/.design"
|
|
72
|
+
STATE="$DESIGN_ROOT/_server.json"
|
|
73
|
+
if [ -z "$PORT" ]; then
|
|
74
|
+
if [ ! -f "$STATE" ]; then
|
|
75
|
+
echo "runtime-health.sh: no --port given and $STATE not found (run server-up.sh first)" >&2
|
|
76
|
+
exit 1
|
|
77
|
+
fi
|
|
78
|
+
if command -v jq >/dev/null 2>&1; then
|
|
79
|
+
PORT=$(jq -r .port "$STATE" 2>/dev/null)
|
|
80
|
+
else
|
|
81
|
+
PORT=$(sed -nE 's/.*"port"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/p' "$STATE" | head -n1)
|
|
82
|
+
fi
|
|
83
|
+
fi
|
|
84
|
+
[ -z "$PORT" ] && { echo "runtime-health.sh: could not resolve port" >&2; exit 1; }
|
|
85
|
+
|
|
86
|
+
BASE="http://localhost:$PORT"
|
|
87
|
+
|
|
88
|
+
# Confirm server alive before probing individual bundles.
|
|
89
|
+
if ! curl -fs "$BASE/_health" >/dev/null 2>&1; then
|
|
90
|
+
echo "runtime-health.sh: server not responding at $BASE/_health" >&2
|
|
91
|
+
exit 1
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# ---------- per-bundle probe ----------
|
|
95
|
+
probe_one() {
|
|
96
|
+
local slug="$1"
|
|
97
|
+
local disk_size="$2"
|
|
98
|
+
local url="$BASE/_canvas-runtime/$slug"
|
|
99
|
+
local served
|
|
100
|
+
served=$(curl -fs -o /dev/null -w '%{size_download}' "$url" 2>/dev/null) || {
|
|
101
|
+
echo "✗ $slug — served HTTP error (curl failed for $url)" >&2
|
|
102
|
+
return 1
|
|
103
|
+
}
|
|
104
|
+
# Floor at 256 bytes — any working ESM bundle is bigger than that.
|
|
105
|
+
if [ "$served" -lt 256 ]; then
|
|
106
|
+
echo "✗ $slug — served body $served B < 256 B floor (server returned empty)" >&2
|
|
107
|
+
return 1
|
|
108
|
+
fi
|
|
109
|
+
# Threshold ratio: served must be ≥ threshold × disk.
|
|
110
|
+
# awk handles the fractional math without bc dependency.
|
|
111
|
+
local ratio
|
|
112
|
+
ratio=$(awk -v s="$served" -v d="$disk_size" 'BEGIN{ if(d==0){print 0}else{printf "%.3f", s/d} }')
|
|
113
|
+
local ok
|
|
114
|
+
ok=$(awk -v r="$ratio" -v t="$THRESHOLD" 'BEGIN{print (r >= t) ? 1 : 0}')
|
|
115
|
+
if [ "$ok" = "1" ]; then
|
|
116
|
+
[ $QUIET -eq 0 ] && echo "✓ $slug — $served B / $disk_size B disk (ratio $ratio)" >&2
|
|
117
|
+
return 0
|
|
118
|
+
fi
|
|
119
|
+
echo "✗ $slug — served $served B / $disk_size B disk (ratio $ratio < $THRESHOLD) — defective dynamic build" >&2
|
|
120
|
+
return 1
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
FAIL_COUNT=0
|
|
124
|
+
FAIL_LIST=""
|
|
125
|
+
for f in "$PREBUILT_DIR"/*.js; do
|
|
126
|
+
[ -f "$f" ] || continue
|
|
127
|
+
SLUG=$(basename "$f")
|
|
128
|
+
DISK_SIZE=$(wc -c < "$f" | tr -d ' ')
|
|
129
|
+
if ! probe_one "$SLUG" "$DISK_SIZE"; then
|
|
130
|
+
FAIL_COUNT=$((FAIL_COUNT + 1))
|
|
131
|
+
FAIL_LIST="$FAIL_LIST $SLUG"
|
|
132
|
+
fi
|
|
133
|
+
done
|
|
134
|
+
|
|
135
|
+
if [ "$FAIL_COUNT" -eq 0 ]; then
|
|
136
|
+
[ $QUIET -eq 0 ] && echo "✓ runtime-health OK — all bundles match disk pre-built (threshold $THRESHOLD)" >&2
|
|
137
|
+
exit 0
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
echo "" >&2
|
|
141
|
+
echo "✗ runtime-health FAIL — $FAIL_COUNT bundle(s) below threshold:$FAIL_LIST" >&2
|
|
142
|
+
echo " These bundles look like defective dynamic Bun.build output." >&2
|
|
143
|
+
echo " The canvas TSX will parse + serve cleanly, but the iframe will throw at runtime." >&2
|
|
144
|
+
|
|
145
|
+
if [ "$RESTART" -eq 1 ]; then
|
|
146
|
+
echo "→ --restart given; killing server and respawning via server-up.sh" >&2
|
|
147
|
+
if [ -f "$STATE" ]; then
|
|
148
|
+
if command -v jq >/dev/null 2>&1; then
|
|
149
|
+
PID=$(jq -r .pid "$STATE" 2>/dev/null)
|
|
150
|
+
else
|
|
151
|
+
PID=$(sed -nE 's/.*"pid"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/p' "$STATE" | head -n1)
|
|
152
|
+
fi
|
|
153
|
+
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
|
|
154
|
+
kill "$PID" 2>/dev/null
|
|
155
|
+
sleep 1
|
|
156
|
+
kill -0 "$PID" 2>/dev/null && kill -9 "$PID" 2>/dev/null
|
|
157
|
+
fi
|
|
158
|
+
rm -f "$STATE"
|
|
159
|
+
fi
|
|
160
|
+
NEW_PORT=$(bash "$SCRIPT_DIR/server-up.sh" --root "$REPO")
|
|
161
|
+
[ -z "$NEW_PORT" ] && { echo "✗ server-up.sh did not return a port after restart" >&2; exit 3; }
|
|
162
|
+
echo "→ server restarted port=$NEW_PORT; re-probing once" >&2
|
|
163
|
+
# Re-probe via tail-recursive invocation with no --restart (avoid infinite loop).
|
|
164
|
+
bash "$0" --port "$NEW_PORT" --root "$REPO" --threshold "$THRESHOLD" $( [ $QUIET -eq 1 ] && echo --quiet )
|
|
165
|
+
exit $?
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
echo " Remediation: rerun with --restart, OR manually 'kill $(cat $STATE 2>/dev/null | sed -nE 's/.*pid.*:.*([0-9]+).*/\1/p') && bash $SCRIPT_DIR/server-up.sh'" >&2
|
|
169
|
+
exit 3
|
|
@@ -93,6 +93,7 @@ import {
|
|
|
93
93
|
diffLayoutPositions,
|
|
94
94
|
} from './commands/move-artboards-command.ts';
|
|
95
95
|
import { type DragState, useArtboardDrag } from './use-artboard-drag.tsx';
|
|
96
|
+
import { CollabProvider, canvasSlugFromPath } from './use-collab.tsx';
|
|
96
97
|
import { useSelectionSetOptional } from './use-selection-set.tsx';
|
|
97
98
|
import { ToolProvider, useToolModeOptional } from './use-tool-mode.tsx';
|
|
98
99
|
import { UndoStackProvider, useUndoSinks, useUndoStackOptional } from './use-undo-stack.tsx';
|
|
@@ -227,7 +228,7 @@ button.dc-artboard-label {
|
|
|
227
228
|
display: block;
|
|
228
229
|
width: 100%;
|
|
229
230
|
}
|
|
230
|
-
button.dc-artboard-label:focus-visible { outline: 2px solid var(--accent, #d63b1f); outline-offset: -2px; }
|
|
231
|
+
button.dc-artboard-label:focus-visible { outline: 2px solid var(--maude-hud-accent, #d63b1f); outline-offset: -2px; }
|
|
231
232
|
/* Active-artboard ring is in canvas-shell HALO_CSS (subtle 1 px tint). */
|
|
232
233
|
/* Phase 4.2 — drag chrome. */
|
|
233
234
|
.dc-canvas[data-active-tool="move"] .dc-artboard-label { cursor: grab; }
|
|
@@ -1170,11 +1171,20 @@ export function DesignCanvas(props: DesignCanvasProps) {
|
|
|
1170
1171
|
// `window.top.__maude_undo_stacks` so it survives canvas switches —
|
|
1171
1172
|
// close Foo.tsx, open Bar.tsx, come back to Foo.tsx → history intact.
|
|
1172
1173
|
const canvasFile = readCanvasMetaFile() ?? undefined;
|
|
1174
|
+
// Phase 8 / DDR-051 — open a Yjs collab session for this canvas iff we can
|
|
1175
|
+
// derive a stable slug. The slug must match `api.fileSlug` server-side so
|
|
1176
|
+
// both ends agree on the room key. When the canvas was opened via a URL
|
|
1177
|
+
// that doesn't yield a slug (e.g. preview iframes without `canvas=`),
|
|
1178
|
+
// CollabProvider is omitted; useCollab() falls back gracefully to null.
|
|
1179
|
+
const collabSlug = canvasSlugFromPath(canvasFile);
|
|
1180
|
+
const inner = (
|
|
1181
|
+
<ToolProvider>
|
|
1182
|
+
<DesignCanvasInner {...props} />
|
|
1183
|
+
</ToolProvider>
|
|
1184
|
+
);
|
|
1173
1185
|
return (
|
|
1174
1186
|
<UndoStackProvider canvasFile={canvasFile}>
|
|
1175
|
-
<
|
|
1176
|
-
<DesignCanvasInner {...props} />
|
|
1177
|
-
</ToolProvider>
|
|
1187
|
+
{collabSlug ? <CollabProvider slug={collabSlug}>{inner}</CollabProvider> : inner}
|
|
1178
1188
|
</UndoStackProvider>
|
|
1179
1189
|
);
|
|
1180
1190
|
}
|
|
@@ -1228,6 +1238,22 @@ function DesignCanvasInner({ children, controls }: DesignCanvasProps) {
|
|
|
1228
1238
|
setArtboards(initialArtboards());
|
|
1229
1239
|
}, [initialArtboards]);
|
|
1230
1240
|
|
|
1241
|
+
// Phase 8 — foreign canvas-meta change. The shell-level HMR client
|
|
1242
|
+
// re-fetches `<canvas>.meta.json` and dispatches `maude:meta-refreshed`
|
|
1243
|
+
// when *another* tab PATCHed the layout (drag, distribute, align). We
|
|
1244
|
+
// re-apply positions in-place — no full reload — so the user's tool mode,
|
|
1245
|
+
// undo stack, scroll, and selection state survive. Self-writes are
|
|
1246
|
+
// suppressed at the dispatch site via the `__maude_last_meta_self_write_at`
|
|
1247
|
+
// echo timestamp.
|
|
1248
|
+
useEffect(() => {
|
|
1249
|
+
if (typeof document === 'undefined') return;
|
|
1250
|
+
const onRefresh = () => {
|
|
1251
|
+
setArtboards(initialArtboards());
|
|
1252
|
+
};
|
|
1253
|
+
document.addEventListener('maude:meta-refreshed', onRefresh);
|
|
1254
|
+
return () => document.removeEventListener('maude:meta-refreshed', onRefresh);
|
|
1255
|
+
}, [initialArtboards]);
|
|
1256
|
+
|
|
1231
1257
|
// Stable refs so the controller's callbacks always see the latest values.
|
|
1232
1258
|
const artboardsRef = useRef(artboards);
|
|
1233
1259
|
artboardsRef.current = artboards;
|
|
@@ -1736,8 +1762,8 @@ const OVERLAY_CSS = `
|
|
|
1736
1762
|
outline-only. Reads from a glance as "what slice of the world you're on". */
|
|
1737
1763
|
.dc-mm-vp {
|
|
1738
1764
|
position: absolute;
|
|
1739
|
-
background: color-mix(in oklab, var(--accent, #d63b1f) 12%, transparent);
|
|
1740
|
-
border: 1.5px solid var(--accent, #d63b1f);
|
|
1765
|
+
background: color-mix(in oklab, var(--maude-hud-accent, #d63b1f) 12%, transparent);
|
|
1766
|
+
border: 1.5px solid var(--maude-hud-accent, #d63b1f);
|
|
1741
1767
|
border-radius: 1px;
|
|
1742
1768
|
pointer-events: none;
|
|
1743
1769
|
}
|
|
@@ -1773,7 +1799,7 @@ const OVERLAY_CSS = `
|
|
|
1773
1799
|
}
|
|
1774
1800
|
.dc-zoom-tb button:last-child { border-right: 0; }
|
|
1775
1801
|
.dc-zoom-tb button:hover { background: color-mix(in oklab, var(--fg-0, #1c1917) 5%, transparent); }
|
|
1776
|
-
.dc-zoom-tb button:focus-visible { outline: 2px solid var(--accent, #d63b1f); outline-offset: -2px; }
|
|
1802
|
+
.dc-zoom-tb button:focus-visible { outline: 2px solid var(--maude-hud-accent, #d63b1f); outline-offset: -2px; }
|
|
1777
1803
|
.dc-zoom-tb-pct { font-variant-numeric: tabular-nums; min-width: 52px; }
|
|
1778
1804
|
`.trim();
|
|
1779
1805
|
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
useState,
|
|
37
37
|
} from 'react';
|
|
38
38
|
|
|
39
|
+
import { AiBanner } from './ai-banner.tsx';
|
|
39
40
|
import { AnnotationsLayer } from './annotations-layer.tsx';
|
|
40
41
|
import { ArtboardMarqueeOverlay } from './artboard-marquee.tsx';
|
|
41
42
|
import {
|
|
@@ -57,17 +58,21 @@ import {
|
|
|
57
58
|
useContextMenu,
|
|
58
59
|
} from './context-menu.tsx';
|
|
59
60
|
import { ContextualToolbar } from './contextual-toolbar.tsx';
|
|
61
|
+
import { CursorsOverlay } from './cursors-overlay.tsx';
|
|
60
62
|
import { EqualSpacingHandles } from './equal-spacing-handles.tsx';
|
|
61
63
|
import { ExportDialogProvider } from './export-dialog.tsx';
|
|
62
64
|
import { type HoverTarget, resolveHoverTarget, useInputRouter } from './input-router.tsx';
|
|
63
65
|
import { ElementMarqueeOverlay } from './marquee-overlay.tsx';
|
|
66
|
+
import { ParticipantsChrome } from './participants-chrome.tsx';
|
|
64
67
|
import { ToolPalette } from './tool-palette.tsx';
|
|
65
68
|
import { UndoHud } from './undo-hud.tsx';
|
|
66
69
|
import {
|
|
67
70
|
AnnotationSelectionProvider,
|
|
68
71
|
useAnnotationSelection,
|
|
72
|
+
useAnnotationSelectionOptional,
|
|
69
73
|
} from './use-annotation-selection.tsx';
|
|
70
74
|
import { AnnotationsVisibilityProvider } from './use-annotations-visibility.tsx';
|
|
75
|
+
import { useCollab } from './use-collab.tsx';
|
|
71
76
|
import { useCursorModifiers } from './use-cursor-modifiers.tsx';
|
|
72
77
|
import { useKeyboardDiscipline } from './use-keyboard-discipline.tsx';
|
|
73
78
|
import { type Selection, SelectionSetProvider, useSelectionSet } from './use-selection-set.tsx';
|
|
@@ -81,6 +86,28 @@ import { useUndoStack } from './use-undo-stack.tsx';
|
|
|
81
86
|
// plane would otherwise scale a 2 px outline to 0.84 px at 42 % zoom (subpixel
|
|
82
87
|
// = invisible). No per-element class stamping is used.
|
|
83
88
|
|
|
89
|
+
// HUD-namespace token block. System-review 2026-05-27 (D-4) flagged that the
|
|
90
|
+
// dev-server chrome (toolbar + minimap + halos + marquee + AI banner) used
|
|
91
|
+
// `var(--accent, …)` which inherited the canvas DS palette — a violet StudyFi
|
|
92
|
+
// canvas turned the floating cursor toolbar violet. The HUD owns its own
|
|
93
|
+
// `--maude-hud-*` token family, set on `:root` of the canvas iframe document
|
|
94
|
+
// here. Canvas DSs do NOT define `--maude-hud-*`, so HUD CSS resolves against
|
|
95
|
+
// this block regardless of what the imported `:root { --accent: … }` looks like.
|
|
96
|
+
//
|
|
97
|
+
// Defaults match the existing inline fallback color (`#d63b1f`, Maude brand
|
|
98
|
+
// orange-rust) so no visual change to the default theme. Users who want to
|
|
99
|
+
// re-theme the HUD can set `--maude-hud-accent` etc. via a `<style>` block
|
|
100
|
+
// AFTER this one (CSS cascade — later wins).
|
|
101
|
+
const HUD_TOKENS_CSS = `
|
|
102
|
+
:root {
|
|
103
|
+
--maude-hud-accent: #d63b1f;
|
|
104
|
+
--maude-hud-accent-hover: #b8331b;
|
|
105
|
+
--maude-hud-accent-active: #962a16;
|
|
106
|
+
--maude-hud-accent-fg: #ffffff;
|
|
107
|
+
--maude-hud-accent-tint: color-mix(in oklab, #d63b1f 14%, transparent);
|
|
108
|
+
}
|
|
109
|
+
`;
|
|
110
|
+
|
|
84
111
|
// DDR-046 — Three-state halo language. Each state has its own border weight,
|
|
85
112
|
// color treatment, and geometric idiom so 8+ semantic states (hover / selected
|
|
86
113
|
// / member-of-multi / group / snap-sibling / snap-grid / marquee / annotation
|
|
@@ -98,20 +125,20 @@ const HALO_CSS = `
|
|
|
98
125
|
/* Hover — lighter 1.5px tinted line + white inner ring for contrast on dark
|
|
99
126
|
elements. NO ring, NO ticks. Synchronous paint (no debounce). */
|
|
100
127
|
.dc-cv-halo--hover {
|
|
101
|
-
border: 1.5px solid color-mix(in oklab, var(--accent, #0d99ff) 60%, transparent);
|
|
128
|
+
border: 1.5px solid color-mix(in oklab, var(--maude-hud-accent, #0d99ff) 60%, transparent);
|
|
102
129
|
box-shadow: inset 0 0 0 1px var(--bg-0, #ffffff);
|
|
103
130
|
}
|
|
104
131
|
/* Selected (single) — 2px solid + 18% ring halo + 4 filled corner ticks.
|
|
105
132
|
Ticks are <i class="tick tick-*"> children at inset:-3px, 8x8, accent fill. */
|
|
106
133
|
.dc-cv-halo--selected {
|
|
107
|
-
border: 2px solid var(--accent, #0d99ff);
|
|
108
|
-
box-shadow: 0 0 0 4px color-mix(in oklab, var(--accent, #0d99ff) 18%, transparent);
|
|
134
|
+
border: 2px solid var(--maude-hud-accent, #0d99ff);
|
|
135
|
+
box-shadow: 0 0 0 4px color-mix(in oklab, var(--maude-hud-accent, #0d99ff) 18%, transparent);
|
|
109
136
|
}
|
|
110
137
|
.dc-cv-halo--selected .tick {
|
|
111
138
|
position: absolute;
|
|
112
139
|
width: 8px;
|
|
113
140
|
height: 8px;
|
|
114
|
-
background: var(--accent, #0d99ff);
|
|
141
|
+
background: var(--maude-hud-accent, #0d99ff);
|
|
115
142
|
border-radius: 1px;
|
|
116
143
|
box-shadow: 0 0 0 1px var(--bg-0, #ffffff);
|
|
117
144
|
}
|
|
@@ -125,7 +152,7 @@ const HALO_CSS = `
|
|
|
125
152
|
as "draft / placeholder" and members would melt away inside artboards once
|
|
126
153
|
the artboard border itself is 22%-tinted (T15). */
|
|
127
154
|
.dc-cv-halo--selected-member {
|
|
128
|
-
border: 1.5px solid var(--accent, #0d99ff);
|
|
155
|
+
border: 1.5px solid var(--maude-hud-accent, #0d99ff);
|
|
129
156
|
}
|
|
130
157
|
/* Group bbox — 1 px DASHED full accent + four 6 × 6 square corner handles.
|
|
131
158
|
T16 / DDR-046 rev 2 — dashed is the canonical group-container affordance
|
|
@@ -138,14 +165,14 @@ const HALO_CSS = `
|
|
|
138
165
|
position: fixed;
|
|
139
166
|
pointer-events: none;
|
|
140
167
|
z-index: 5;
|
|
141
|
-
border: 1px dashed var(--accent, #0d99ff);
|
|
168
|
+
border: 1px dashed var(--maude-hud-accent, #0d99ff);
|
|
142
169
|
border-radius: 2px;
|
|
143
170
|
}
|
|
144
171
|
.dc-cv-group-bbox .tick {
|
|
145
172
|
position: absolute;
|
|
146
173
|
width: 6px;
|
|
147
174
|
height: 6px;
|
|
148
|
-
background: var(--accent, #0d99ff);
|
|
175
|
+
background: var(--maude-hud-accent, #0d99ff);
|
|
149
176
|
border-radius: 1px;
|
|
150
177
|
box-shadow: 0 0 0 1px var(--bg-0, #ffffff);
|
|
151
178
|
}
|
|
@@ -165,7 +192,7 @@ const HALO_CSS = `
|
|
|
165
192
|
(3 px ring + hard 6×6×0 offset) was readable but visually expensive once
|
|
166
193
|
the frame itself lost its brutalist treatment. A 2 px ring on a 22 %
|
|
167
194
|
tinted hairline reads unambiguous without claiming subject-ness. */
|
|
168
|
-
box-shadow: 0 0 0 2px var(--accent, #0d99ff);
|
|
195
|
+
box-shadow: 0 0 0 2px var(--maude-hud-accent, #0d99ff);
|
|
169
196
|
transition: box-shadow 120ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
170
197
|
}
|
|
171
198
|
/* Respect prefers-reduced-motion across all chrome transitions. */
|
|
@@ -247,6 +274,15 @@ const HALO_CSS = `
|
|
|
247
274
|
|
|
248
275
|
function ensureHaloStyles(): void {
|
|
249
276
|
if (typeof document === 'undefined') return;
|
|
277
|
+
// HUD tokens MUST be injected before HALO_CSS so the cascade resolves
|
|
278
|
+
// `var(--maude-hud-accent, …)` against the dev-server's brand defaults
|
|
279
|
+
// even when the canvas DS's tokens.css later sets `:root { --accent: … }`.
|
|
280
|
+
if (!document.getElementById('dc-cv-hud-tokens-css')) {
|
|
281
|
+
const t = document.createElement('style');
|
|
282
|
+
t.id = 'dc-cv-hud-tokens-css';
|
|
283
|
+
t.textContent = HUD_TOKENS_CSS;
|
|
284
|
+
document.head.appendChild(t);
|
|
285
|
+
}
|
|
250
286
|
if (document.getElementById('dc-cv-halo-css')) return;
|
|
251
287
|
const s = document.createElement('style');
|
|
252
288
|
s.id = 'dc-cv-halo-css';
|
|
@@ -364,6 +400,85 @@ function CanvasCore({
|
|
|
364
400
|
return () => host.removeAttribute('data-cv-zoom-lod');
|
|
365
401
|
}, [hostRef, publishedZoom]);
|
|
366
402
|
|
|
403
|
+
// Phase 8 — publish local cursor (world coords) + viewport to Awareness
|
|
404
|
+
// so foreign peers can render our cursor on their CursorsOverlay. The
|
|
405
|
+
// collab.publishAwareness call is already throttled to ~30 Hz internally
|
|
406
|
+
// (use-collab.tsx) — we just need to compute screen → world per move.
|
|
407
|
+
const collab = useCollab();
|
|
408
|
+
useEffect(() => {
|
|
409
|
+
const host = hostRef.current;
|
|
410
|
+
if (!host || !collab || !controller) return;
|
|
411
|
+
const onMove = (e: MouseEvent) => {
|
|
412
|
+
const v = controller.viewport;
|
|
413
|
+
// screen → world: world = (screen - viewport.{x,y}) / zoom.
|
|
414
|
+
const worldX = (e.clientX - v.x) / Math.max(v.zoom, 0.0001);
|
|
415
|
+
const worldY = (e.clientY - v.y) / Math.max(v.zoom, 0.0001);
|
|
416
|
+
collab.publishAwareness({ cursor: { x: worldX, y: worldY } });
|
|
417
|
+
};
|
|
418
|
+
const onLeave = () => {
|
|
419
|
+
collab.publishAwareness({ cursor: null });
|
|
420
|
+
};
|
|
421
|
+
host.addEventListener('mousemove', onMove);
|
|
422
|
+
host.addEventListener('mouseleave', onLeave);
|
|
423
|
+
return () => {
|
|
424
|
+
host.removeEventListener('mousemove', onMove);
|
|
425
|
+
host.removeEventListener('mouseleave', onLeave);
|
|
426
|
+
};
|
|
427
|
+
}, [hostRef, collab, controller]);
|
|
428
|
+
|
|
429
|
+
// Phase 8 — publish viewport when it settles. The CursorsOverlay only
|
|
430
|
+
// needs the LOCAL viewport (to transform foreign world coords back to
|
|
431
|
+
// screen), but exposing ours over Awareness sets up Task 6's follow-mode.
|
|
432
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: deliberate settle-cadence — re-publish only when x/y/zoom change, not on every viewport field.
|
|
433
|
+
useEffect(() => {
|
|
434
|
+
if (!collab || !controller) return;
|
|
435
|
+
collab.publishAwareness({ viewport: controller.viewport });
|
|
436
|
+
}, [
|
|
437
|
+
collab,
|
|
438
|
+
controller,
|
|
439
|
+
controller?.viewport.x,
|
|
440
|
+
controller?.viewport.y,
|
|
441
|
+
controller?.viewport.zoom,
|
|
442
|
+
]);
|
|
443
|
+
|
|
444
|
+
// Phase 8 — publish local selection so foreign PeerSelection halos can
|
|
445
|
+
// render around what *I* selected. Convert the first entry's selector
|
|
446
|
+
// (CSS path the peer resolves in their own DOM) + current bounds. The
|
|
447
|
+
// bounds are screen-px from publish time — peers re-resolve cssPath
|
|
448
|
+
// when possible and fall back to bounds otherwise.
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
if (!collab) return;
|
|
451
|
+
const first = selSet.selected[0];
|
|
452
|
+
if (!first || !first.selector) {
|
|
453
|
+
collab.publishAwareness({ selection: null });
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const b = first.bounds;
|
|
457
|
+
const bounds =
|
|
458
|
+
b && typeof b === 'object'
|
|
459
|
+
? {
|
|
460
|
+
x: Number(b.x) || 0,
|
|
461
|
+
y: Number(b.y) || 0,
|
|
462
|
+
w: Number(b.w) || 0,
|
|
463
|
+
h: Number(b.h) || 0,
|
|
464
|
+
}
|
|
465
|
+
: { x: 0, y: 0, w: 0, h: 0 };
|
|
466
|
+
collab.publishAwareness({ selection: { cssPath: first.selector, bounds } });
|
|
467
|
+
}, [collab, selSet.selected]);
|
|
468
|
+
|
|
469
|
+
// Phase 8 — publish annotation selection (Phase 5 strokes). Separate from
|
|
470
|
+
// selSet because annotations have their own selection registry. Peers
|
|
471
|
+
// render halos by querying `[data-id="<id>"]` so the same halo follows
|
|
472
|
+
// resize / move (SVG re-emits with the same data-id). `Optional` flavor
|
|
473
|
+
// because CanvasCore is mounted INSIDE the AnnotationSelectionProvider
|
|
474
|
+
// tree but TypeScript / a defensive boot path can't always prove it.
|
|
475
|
+
const annotSelForPublish = useAnnotationSelectionOptional();
|
|
476
|
+
const annotSelectedIds = annotSelForPublish?.selectedIds;
|
|
477
|
+
useEffect(() => {
|
|
478
|
+
if (!collab) return;
|
|
479
|
+
collab.publishAwareness({ annotationSelection: annotSelectedIds ?? [] });
|
|
480
|
+
}, [collab, annotSelectedIds]);
|
|
481
|
+
|
|
367
482
|
/**
|
|
368
483
|
* T24 — distribute the currently-selected artboards evenly on the given
|
|
369
484
|
* axis. Requires ≥ 3 selected artboards. Sort by leading edge, hold the
|
|
@@ -1017,6 +1132,9 @@ function CanvasRouter({
|
|
|
1017
1132
|
/>
|
|
1018
1133
|
<SnapGuideOverlay />
|
|
1019
1134
|
<UndoHud />
|
|
1135
|
+
<CursorsOverlay />
|
|
1136
|
+
<AiBanner />
|
|
1137
|
+
<ParticipantsChrome />
|
|
1020
1138
|
</>
|
|
1021
1139
|
);
|
|
1022
1140
|
}
|
|
@@ -1062,7 +1180,7 @@ const MULTI_TOOLBAR_CSS = `
|
|
|
1062
1180
|
transition: background-color 80ms linear;
|
|
1063
1181
|
}
|
|
1064
1182
|
.dc-multi-artboard-tb button:hover:not(:disabled) {
|
|
1065
|
-
background: color-mix(in oklab, var(--accent, #d63b1f) 8%, transparent);
|
|
1183
|
+
background: color-mix(in oklab, var(--maude-hud-accent, #d63b1f) 8%, transparent);
|
|
1066
1184
|
}
|
|
1067
1185
|
.dc-multi-artboard-tb button:disabled {
|
|
1068
1186
|
cursor: default;
|
|
@@ -1483,6 +1483,10 @@ function App() {
|
|
|
1483
1483
|
const [activePath, setActivePath] = useState(null);
|
|
1484
1484
|
const [selected, setSelected] = useState(null);
|
|
1485
1485
|
const [wsConnected, setWsConnected] = useState(false);
|
|
1486
|
+
// Phase 8 Task 7 — git lifecycle reload prompt. Server has already flushed
|
|
1487
|
+
// every dirty Y.Doc to disk by the time this state populates, so accepting
|
|
1488
|
+
// the reload is data-loss-safe (DDR-051 §3).
|
|
1489
|
+
const [gitLifecycle, setGitLifecycle] = useState(null);
|
|
1486
1490
|
const [search, setSearch] = useState('');
|
|
1487
1491
|
const [systemData, setSystemData] = useState(null);
|
|
1488
1492
|
// Loaded once at boot from /_config — informs canvasUrl() so TSX iframes
|
|
@@ -1658,6 +1662,23 @@ function App() {
|
|
|
1658
1662
|
setSelected(m.selected);
|
|
1659
1663
|
} else if (m.type === 'comments' && typeof m.file === 'string') {
|
|
1660
1664
|
setCommentsByFile(prev => ({ ...prev, [m.file]: m.comments || [] }));
|
|
1665
|
+
} else if (m.type === 'ai-activity' && typeof m.file === 'string') {
|
|
1666
|
+
// Phase 8 Task 4 — relay to every open iframe; each canvas's
|
|
1667
|
+
// AiBanner filters by its own file path. Lightweight broadcast
|
|
1668
|
+
// (one envelope per change, not per iframe count).
|
|
1669
|
+
for (const el of iframesRef.current.values()) {
|
|
1670
|
+
try { el.contentWindow.postMessage({ dgn: 'ai-activity', file: m.file, entry: m.entry }, '*'); } catch {}
|
|
1671
|
+
}
|
|
1672
|
+
} else if (m.type === 'git-lifecycle' && m.payload) {
|
|
1673
|
+
// Phase 8 Task 7 — branch switch / pull mid-session. Server has
|
|
1674
|
+
// already flushed every dirty Y.Doc to JSON; just prompt the user.
|
|
1675
|
+
// Single confirm covers all open iframes — reload reseeds them all.
|
|
1676
|
+
setGitLifecycle(m.payload);
|
|
1677
|
+
// Also relay to iframes so canvas-level "Reload?" UI (if any)
|
|
1678
|
+
// can react. Outer banner is the primary prompt.
|
|
1679
|
+
for (const el of iframesRef.current.values()) {
|
|
1680
|
+
try { el.contentWindow.postMessage({ dgn: 'git-lifecycle', payload: m.payload }, '*'); } catch {}
|
|
1681
|
+
}
|
|
1661
1682
|
}
|
|
1662
1683
|
} catch {}
|
|
1663
1684
|
});
|
|
@@ -2000,6 +2021,57 @@ function App() {
|
|
|
2000
2021
|
className={'app' + (commentsPanelOpen ? ' with-rsidebar' : '') + (sidebarOpen ? '' : ' no-sidebar')}
|
|
2001
2022
|
onContextMenu={onShellContextMenu}
|
|
2002
2023
|
>
|
|
2024
|
+
{gitLifecycle && (
|
|
2025
|
+
<div
|
|
2026
|
+
role="status"
|
|
2027
|
+
aria-live="polite"
|
|
2028
|
+
style={{
|
|
2029
|
+
position: 'fixed',
|
|
2030
|
+
top: 12,
|
|
2031
|
+
left: '50%',
|
|
2032
|
+
transform: 'translateX(-50%)',
|
|
2033
|
+
zIndex: 10002,
|
|
2034
|
+
display: 'flex',
|
|
2035
|
+
alignItems: 'center',
|
|
2036
|
+
gap: 12,
|
|
2037
|
+
padding: '8px 14px',
|
|
2038
|
+
background: '#dbeafe',
|
|
2039
|
+
color: '#1e40af',
|
|
2040
|
+
border: '1px solid #93c5fd',
|
|
2041
|
+
borderRadius: 999,
|
|
2042
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.12)',
|
|
2043
|
+
font: '500 13px/1.2 system-ui, -apple-system, sans-serif',
|
|
2044
|
+
}}
|
|
2045
|
+
>
|
|
2046
|
+
<span>Repo state changed — reload to sync?</span>
|
|
2047
|
+
<button
|
|
2048
|
+
type="button"
|
|
2049
|
+
onClick={() => { try { window.location.reload(); } catch {} }}
|
|
2050
|
+
style={{
|
|
2051
|
+
padding: '3px 10px',
|
|
2052
|
+
background: '#2563eb',
|
|
2053
|
+
color: '#fff',
|
|
2054
|
+
border: 'none',
|
|
2055
|
+
borderRadius: 4,
|
|
2056
|
+
font: '500 11px/1.2 system-ui',
|
|
2057
|
+
cursor: 'pointer',
|
|
2058
|
+
}}
|
|
2059
|
+
>Reload</button>
|
|
2060
|
+
<button
|
|
2061
|
+
type="button"
|
|
2062
|
+
onClick={() => setGitLifecycle(null)}
|
|
2063
|
+
style={{
|
|
2064
|
+
padding: '3px 10px',
|
|
2065
|
+
background: 'transparent',
|
|
2066
|
+
color: '#1e40af',
|
|
2067
|
+
border: '1px solid #93c5fd',
|
|
2068
|
+
borderRadius: 4,
|
|
2069
|
+
font: '500 11px/1.2 system-ui',
|
|
2070
|
+
cursor: 'pointer',
|
|
2071
|
+
}}
|
|
2072
|
+
>Dismiss</button>
|
|
2073
|
+
</div>
|
|
2074
|
+
)}
|
|
2003
2075
|
<Sidebar
|
|
2004
2076
|
groups={groups}
|
|
2005
2077
|
activePath={activePath}
|