@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
|
@@ -22,129 +22,176 @@
|
|
|
22
22
|
|
|
23
23
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
24
24
|
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
FILL_PALETTE,
|
|
27
|
+
STROKE_PALETTE,
|
|
28
|
+
type Stroke,
|
|
29
|
+
useStrokesStore,
|
|
30
|
+
} from './annotations-layer.tsx';
|
|
31
|
+
import {
|
|
32
|
+
IconArrowBothHeads,
|
|
33
|
+
IconArrowEndHead,
|
|
34
|
+
IconArrowNone,
|
|
35
|
+
IconArrowStartHead,
|
|
36
|
+
IconCornerPill,
|
|
37
|
+
IconCornerSoft,
|
|
38
|
+
IconCornerSquare,
|
|
39
|
+
IconDash,
|
|
40
|
+
IconLetterA,
|
|
41
|
+
IconLineThick,
|
|
42
|
+
IconLineThin,
|
|
43
|
+
IconTrash,
|
|
44
|
+
} from './canvas-icons.tsx';
|
|
26
45
|
import { useAnnotationSelectionOptional } from './use-annotation-selection.tsx';
|
|
27
46
|
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
'#E03E3E', // red
|
|
36
|
-
'#F26B3F', // orange
|
|
37
|
-
'#F2C744', // yellow
|
|
38
|
-
'#4CAF50', // green
|
|
39
|
-
'#14B8A6', // teal
|
|
40
|
-
'#3B82F6', // blue
|
|
41
|
-
'#8B5CF6', // purple
|
|
42
|
-
'#EC4899', // pink
|
|
43
|
-
'#FFFFFF', // white
|
|
44
|
-
] as const;
|
|
45
|
-
|
|
46
|
-
const FILL_PALETTE = PALETTE;
|
|
47
|
+
// Phase 21 — the swatch palettes come from annotations-layer so the draw-time
|
|
48
|
+
// chrome and this per-selection toolbar share ONE hue family. STROKE mode shows
|
|
49
|
+
// saturated inks; FILL mode shows the index-paired light tints (FigJam: a
|
|
50
|
+
// saturated outline over a pale wash of the same hue). They're referenced
|
|
51
|
+
// INSIDE the component (render time) — never at module top-level — because
|
|
52
|
+
// annotations-layer ↔ this file form an import cycle and a top-level read would
|
|
53
|
+
// hit the TDZ before STROKE_PALETTE initializes.
|
|
47
54
|
|
|
55
|
+
// Phase 21 — dark "property bar" matching FigJam's selection toolbar. A
|
|
56
|
+
// near-black rounded pill that floats above the selection; swatches + icons sit
|
|
57
|
+
// on dark so colour reads true. Fixed dark values (not canvas tokens) so the
|
|
58
|
+
// bar looks identical on any canvas / DS.
|
|
59
|
+
const CTX_SURFACE = '#26262b';
|
|
48
60
|
const TOOLBAR_CSS = `
|
|
49
61
|
.dc-annot-ctx {
|
|
50
62
|
position: fixed;
|
|
51
63
|
z-index: 7;
|
|
52
64
|
display: flex;
|
|
53
65
|
align-items: center;
|
|
54
|
-
gap:
|
|
55
|
-
background:
|
|
56
|
-
border: 1px solid
|
|
57
|
-
border-radius:
|
|
58
|
-
padding:
|
|
59
|
-
box-shadow: 0
|
|
66
|
+
gap: 2px;
|
|
67
|
+
background: ${CTX_SURFACE};
|
|
68
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
69
|
+
border-radius: 12px;
|
|
70
|
+
padding: 5px 7px;
|
|
71
|
+
box-shadow: 0 8px 28px rgba(0,0,0,0.34), 0 2px 6px rgba(0,0,0,0.22);
|
|
60
72
|
font-family: var(--u-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
61
73
|
font-size: 12px;
|
|
62
|
-
color:
|
|
74
|
+
color: rgba(255,255,255,0.85);
|
|
63
75
|
user-select: none;
|
|
64
76
|
pointer-events: auto;
|
|
65
77
|
}
|
|
78
|
+
/* Swatches sit in a tight touching band so the colour row reads as ONE
|
|
79
|
+
control, not a loose ramp (graphic-critic blocker 1). 22px hit target. */
|
|
80
|
+
.dc-annot-ctx-swrow {
|
|
81
|
+
display: inline-flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
gap: 1px;
|
|
84
|
+
}
|
|
66
85
|
.dc-annot-ctx-sw {
|
|
67
|
-
width:
|
|
68
|
-
height:
|
|
69
|
-
border-radius:
|
|
70
|
-
border: 1px solid rgba(
|
|
86
|
+
width: 20px;
|
|
87
|
+
height: 20px;
|
|
88
|
+
border-radius: 50%;
|
|
89
|
+
border: 1px solid rgba(255,255,255,0.16);
|
|
71
90
|
cursor: pointer;
|
|
72
91
|
padding: 0;
|
|
92
|
+
margin: 0;
|
|
73
93
|
appearance: none;
|
|
94
|
+
transition: transform 80ms ease;
|
|
74
95
|
}
|
|
96
|
+
.dc-annot-ctx-sw:hover { transform: scale(1.1); }
|
|
75
97
|
.dc-annot-ctx-sw[aria-pressed="true"] {
|
|
76
|
-
box-shadow: 0 0 0 2px
|
|
98
|
+
box-shadow: 0 0 0 2px ${CTX_SURFACE}, 0 0 0 3px rgba(255,255,255,0.92);
|
|
77
99
|
border-color: transparent;
|
|
78
100
|
}
|
|
101
|
+
.dc-annot-ctx-sw:focus-visible {
|
|
102
|
+
outline: 2px solid #ffffff;
|
|
103
|
+
outline-offset: 1px;
|
|
104
|
+
}
|
|
79
105
|
.dc-annot-ctx-fill--none {
|
|
80
|
-
background: #
|
|
106
|
+
background: #3a3a40;
|
|
81
107
|
position: relative;
|
|
82
108
|
}
|
|
83
109
|
.dc-annot-ctx-fill--none::after {
|
|
84
110
|
content: "";
|
|
85
|
-
position: absolute; inset:
|
|
111
|
+
position: absolute; inset: 4px;
|
|
112
|
+
border-radius: 50%;
|
|
86
113
|
background:
|
|
87
|
-
linear-gradient(135deg, transparent
|
|
114
|
+
linear-gradient(135deg, transparent 44%, rgba(255,255,255,0.55) 44%, rgba(255,255,255,0.55) 56%, transparent 56%);
|
|
88
115
|
}
|
|
89
116
|
.dc-annot-ctx-sep {
|
|
90
117
|
width: 1px;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
.dc-annot-ctx-btn {
|
|
96
|
-
appearance: none;
|
|
97
|
-
background: transparent;
|
|
98
|
-
border: 1px solid var(--u-fg-0, rgba(0,0,0,0.6));
|
|
99
|
-
border-radius: 0;
|
|
100
|
-
padding: 3px 8px;
|
|
101
|
-
font: inherit;
|
|
102
|
-
color: inherit;
|
|
103
|
-
cursor: pointer;
|
|
104
|
-
}
|
|
105
|
-
.dc-annot-ctx-btn[aria-pressed="true"] {
|
|
106
|
-
background: var(--maude-hud-accent, #d63b1f);
|
|
107
|
-
color: var(--maude-hud-accent-fg, #fff);
|
|
108
|
-
border-color: transparent;
|
|
118
|
+
height: 16px;
|
|
119
|
+
align-self: center;
|
|
120
|
+
background: rgba(255,255,255,0.09);
|
|
121
|
+
margin: 0 4px;
|
|
109
122
|
}
|
|
110
|
-
|
|
111
|
-
.dc-annot-ctx-btn--danger { color: #b3271a; }
|
|
112
|
-
.dc-annot-ctx-btn--danger:hover { background: rgba(214,59,31,0.08); }
|
|
113
|
-
|
|
114
|
-
/* T30 / G_S1 — collapsed Stroke|Fill mode toggle. Shown only when the
|
|
115
|
-
selection supports both stroke + fill (rect / ellipse). When the toggle is
|
|
116
|
-
present the swatch row beneath shows ONE palette at a time. */
|
|
123
|
+
/* Collapsed Stroke|Fill mode toggle (rect / ellipse selections). */
|
|
117
124
|
.dc-annot-ctx-mode {
|
|
118
125
|
display: inline-flex;
|
|
119
|
-
border-radius:
|
|
126
|
+
border-radius: 7px;
|
|
120
127
|
overflow: hidden;
|
|
121
|
-
|
|
128
|
+
background: rgba(255,255,255,0.07);
|
|
129
|
+
padding: 2px;
|
|
130
|
+
gap: 2px;
|
|
122
131
|
}
|
|
123
132
|
.dc-annot-ctx-mode-btn {
|
|
124
133
|
appearance: none;
|
|
125
134
|
background: transparent;
|
|
126
135
|
border: 0;
|
|
127
|
-
|
|
136
|
+
border-radius: 5px;
|
|
137
|
+
padding: 3px 9px;
|
|
128
138
|
font: inherit;
|
|
129
|
-
color:
|
|
139
|
+
color: rgba(255,255,255,0.6);
|
|
130
140
|
cursor: pointer;
|
|
131
141
|
font-size: 11px;
|
|
132
142
|
letter-spacing: 0.02em;
|
|
133
|
-
text-transform: uppercase;
|
|
134
143
|
line-height: 1;
|
|
135
144
|
}
|
|
136
145
|
.dc-annot-ctx-mode-btn[aria-pressed="true"] {
|
|
137
|
-
background:
|
|
138
|
-
color:
|
|
146
|
+
background: rgba(255,255,255,0.16);
|
|
147
|
+
color: #ffffff;
|
|
139
148
|
font-weight: 600;
|
|
140
149
|
}
|
|
141
150
|
.dc-annot-ctx-mode-btn:not([aria-pressed="true"]):hover {
|
|
142
|
-
|
|
151
|
+
color: rgba(255,255,255,0.9);
|
|
152
|
+
}
|
|
153
|
+
.dc-annot-ctx-mode-btn:focus-visible {
|
|
154
|
+
outline: 2px solid #ffffff;
|
|
155
|
+
outline-offset: -1px;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Icon buttons — light glyph on dark, white-tint hover, white-tint active.
|
|
159
|
+
26px to sit closer to the 20px swatch rhythm (graphic-critic blocker 2). */
|
|
160
|
+
.dc-annot-ctx-ibtn {
|
|
161
|
+
appearance: none;
|
|
162
|
+
background: transparent;
|
|
163
|
+
border: 0;
|
|
164
|
+
border-radius: 7px;
|
|
165
|
+
width: 26px;
|
|
166
|
+
height: 26px;
|
|
167
|
+
display: inline-flex;
|
|
168
|
+
align-items: center;
|
|
169
|
+
justify-content: center;
|
|
170
|
+
color: rgba(255,255,255,0.78);
|
|
171
|
+
cursor: pointer;
|
|
172
|
+
padding: 0;
|
|
173
|
+
position: relative;
|
|
174
|
+
transition: background-color 80ms linear, color 80ms linear;
|
|
175
|
+
}
|
|
176
|
+
.dc-annot-ctx-ibtn:hover {
|
|
177
|
+
background: rgba(255,255,255,0.1);
|
|
178
|
+
color: #ffffff;
|
|
179
|
+
}
|
|
180
|
+
.dc-annot-ctx-ibtn[aria-pressed="true"] {
|
|
181
|
+
background: rgba(255,255,255,0.18);
|
|
182
|
+
color: #ffffff;
|
|
183
|
+
}
|
|
184
|
+
.dc-annot-ctx-ibtn:focus-visible {
|
|
185
|
+
outline: 2px solid #ffffff;
|
|
186
|
+
outline-offset: -2px;
|
|
187
|
+
}
|
|
188
|
+
.dc-annot-ctx-ibtn--danger { color: rgba(255,255,255,0.7); }
|
|
189
|
+
.dc-annot-ctx-ibtn--danger:hover {
|
|
190
|
+
background: color-mix(in oklab, #ff5a4d 26%, transparent);
|
|
191
|
+
color: #ffffff;
|
|
143
192
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
.dc-annot-ctx-overflow {
|
|
147
|
-
margin-left: auto;
|
|
193
|
+
@media (prefers-reduced-motion: reduce) {
|
|
194
|
+
.dc-annot-ctx-ibtn, .dc-annot-ctx-sw { transition: none; }
|
|
148
195
|
}
|
|
149
196
|
`.trim();
|
|
150
197
|
|
|
@@ -202,19 +249,36 @@ export function AnnotationContextToolbar() {
|
|
|
202
249
|
// Capabilities — intersection across selected types.
|
|
203
250
|
const caps = useMemo(() => {
|
|
204
251
|
if (selectedStrokes.length === 0) {
|
|
205
|
-
return {
|
|
252
|
+
return {
|
|
253
|
+
color: false,
|
|
254
|
+
fill: false,
|
|
255
|
+
thickness: false,
|
|
256
|
+
fontSize: false,
|
|
257
|
+
cornerRadius: false,
|
|
258
|
+
arrowDir: false,
|
|
259
|
+
dash: false,
|
|
260
|
+
};
|
|
206
261
|
}
|
|
207
262
|
const allFillable = selectedStrokes.every((s) => s.tool === 'rect' || s.tool === 'ellipse');
|
|
208
263
|
// T20 — rect + ellipse now carry stroke weight too.
|
|
209
264
|
const allThickness = selectedStrokes.every(
|
|
210
265
|
(s) => s.tool === 'pen' || s.tool === 'arrow' || s.tool === 'rect' || s.tool === 'ellipse'
|
|
211
266
|
);
|
|
212
|
-
|
|
267
|
+
// Phase 21 — fontSize applies to text + sticky; cornerRadius to rect +
|
|
268
|
+
// sticky; arrow direction + dash to arrows.
|
|
269
|
+
const fontSizeApplicable = selectedStrokes.some(
|
|
270
|
+
(s) => s.tool === 'text' || s.tool === 'sticky'
|
|
271
|
+
);
|
|
272
|
+
const allRectOrSticky = selectedStrokes.every((s) => s.tool === 'rect' || s.tool === 'sticky');
|
|
273
|
+
const allArrow = selectedStrokes.every((s) => s.tool === 'arrow');
|
|
213
274
|
return {
|
|
214
275
|
color: true,
|
|
215
276
|
fill: allFillable,
|
|
216
277
|
thickness: allThickness,
|
|
217
|
-
fontSize:
|
|
278
|
+
fontSize: fontSizeApplicable,
|
|
279
|
+
cornerRadius: allRectOrSticky,
|
|
280
|
+
arrowDir: allArrow,
|
|
281
|
+
dash: allArrow,
|
|
218
282
|
};
|
|
219
283
|
}, [selectedStrokes]);
|
|
220
284
|
|
|
@@ -299,13 +363,50 @@ export function AnnotationContextToolbar() {
|
|
|
299
363
|
(sz: number) => {
|
|
300
364
|
if (!store) return;
|
|
301
365
|
for (const s of selectedStrokes) {
|
|
302
|
-
|
|
366
|
+
// Phase 21 — sticky carries fontSize too.
|
|
367
|
+
if (s.tool === 'text' || s.tool === 'sticky') {
|
|
303
368
|
store.updateStroke(s.id, { fontSize: sz } as Partial<Stroke>);
|
|
304
369
|
}
|
|
305
370
|
}
|
|
306
371
|
},
|
|
307
372
|
[store, selectedStrokes]
|
|
308
373
|
);
|
|
374
|
+
// Phase 21 — corner radius (rect + sticky).
|
|
375
|
+
const setCornerRadius = useCallback(
|
|
376
|
+
(r: number) => {
|
|
377
|
+
if (!store) return;
|
|
378
|
+
for (const s of selectedStrokes) {
|
|
379
|
+
if (s.tool === 'rect' || s.tool === 'sticky') {
|
|
380
|
+
store.updateStroke(s.id, { cornerRadius: r } as Partial<Stroke>);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
[store, selectedStrokes]
|
|
385
|
+
);
|
|
386
|
+
// Phase 21 — arrow head direction (None / Start / End / Both).
|
|
387
|
+
const setArrowDir = useCallback(
|
|
388
|
+
(startHead: 'none' | 'triangle', endHead: 'none' | 'triangle') => {
|
|
389
|
+
if (!store) return;
|
|
390
|
+
for (const s of selectedStrokes) {
|
|
391
|
+
if (s.tool === 'arrow') {
|
|
392
|
+
store.updateStroke(s.id, { startHead, endHead } as Partial<Stroke>);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
[store, selectedStrokes]
|
|
397
|
+
);
|
|
398
|
+
// Phase 21 — arrow dash toggle.
|
|
399
|
+
const setDashed = useCallback(
|
|
400
|
+
(dashed: boolean) => {
|
|
401
|
+
if (!store) return;
|
|
402
|
+
for (const s of selectedStrokes) {
|
|
403
|
+
if (s.tool === 'arrow') {
|
|
404
|
+
store.updateStroke(s.id, { dashed } as Partial<Stroke>);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
[store, selectedStrokes]
|
|
409
|
+
);
|
|
309
410
|
const remove = useCallback(() => {
|
|
310
411
|
if (!annotSel || !store) return;
|
|
311
412
|
store.deleteStrokes(annotSel.selectedIds);
|
|
@@ -333,14 +434,47 @@ export function AnnotationContextToolbar() {
|
|
|
333
434
|
)
|
|
334
435
|
: undefined;
|
|
335
436
|
const uniqFontSize = caps.fontSize
|
|
336
|
-
? uniformValue(
|
|
437
|
+
? uniformValue(
|
|
438
|
+
selectedStrokes.map((s) =>
|
|
439
|
+
s.tool === 'text' || s.tool === 'sticky' ? s.fontSize : undefined
|
|
440
|
+
)
|
|
441
|
+
)
|
|
442
|
+
: undefined;
|
|
443
|
+
// Phase 21 — uniform corner radius across rect/sticky (default per type: rect
|
|
444
|
+
// sharp = 0, sticky soft = 8).
|
|
445
|
+
const uniqRadius = caps.cornerRadius
|
|
446
|
+
? uniformValue(
|
|
447
|
+
selectedStrokes.map((s) =>
|
|
448
|
+
s.tool === 'rect'
|
|
449
|
+
? (s.cornerRadius ?? 0)
|
|
450
|
+
: s.tool === 'sticky'
|
|
451
|
+
? (s.cornerRadius ?? 8)
|
|
452
|
+
: undefined
|
|
453
|
+
)
|
|
454
|
+
)
|
|
455
|
+
: undefined;
|
|
456
|
+
// Phase 21 — uniform arrow head pair (default start none / end triangle).
|
|
457
|
+
const uniqStartHead = caps.arrowDir
|
|
458
|
+
? uniformValue(
|
|
459
|
+
selectedStrokes.map((s) => (s.tool === 'arrow' ? (s.startHead ?? 'none') : undefined))
|
|
460
|
+
)
|
|
461
|
+
: undefined;
|
|
462
|
+
const uniqEndHead = caps.arrowDir
|
|
463
|
+
? uniformValue(
|
|
464
|
+
selectedStrokes.map((s) => (s.tool === 'arrow' ? (s.endHead ?? 'triangle') : undefined))
|
|
465
|
+
)
|
|
466
|
+
: undefined;
|
|
467
|
+
const uniqDashed = caps.dash
|
|
468
|
+
? uniformValue(
|
|
469
|
+
selectedStrokes.map((s) => (s.tool === 'arrow' ? (s.dashed ?? false) : undefined))
|
|
470
|
+
)
|
|
337
471
|
: undefined;
|
|
338
472
|
|
|
339
473
|
// T30 / G_S1 — when caps.fill is false we never enter fill mode. The
|
|
340
474
|
// useEffect below could call setSwatchMode('stroke') but reading the
|
|
341
475
|
// effective mode inline avoids an extra render cycle.
|
|
342
476
|
const effectiveMode: SwatchMode = caps.fill ? swatchMode : 'stroke';
|
|
343
|
-
const showPalette = effectiveMode === 'stroke' ?
|
|
477
|
+
const showPalette = effectiveMode === 'stroke' ? STROKE_PALETTE : FILL_PALETTE;
|
|
344
478
|
const onSwatchClick =
|
|
345
479
|
effectiveMode === 'stroke' ? (c: string) => setColor(c) : (c: string) => setFill(c);
|
|
346
480
|
const activeValue = effectiveMode === 'stroke' ? uniqColor : (uniqFill ?? null);
|
|
@@ -376,48 +510,52 @@ export function AnnotationContextToolbar() {
|
|
|
376
510
|
<div className="dc-annot-ctx-sep" />
|
|
377
511
|
</>
|
|
378
512
|
) : null}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
513
|
+
<div className="dc-annot-ctx-swrow" role="radiogroup" aria-label="Color">
|
|
514
|
+
{effectiveMode === 'fill' ? (
|
|
515
|
+
<button
|
|
516
|
+
type="button"
|
|
517
|
+
className="dc-annot-ctx-sw dc-annot-ctx-fill--none"
|
|
518
|
+
aria-label="No fill"
|
|
519
|
+
aria-pressed={uniqFill == null}
|
|
520
|
+
title="No fill"
|
|
521
|
+
onClick={() => setFill(null)}
|
|
522
|
+
/>
|
|
523
|
+
) : null}
|
|
524
|
+
{showPalette.map((c) => (
|
|
525
|
+
<button
|
|
526
|
+
key={c}
|
|
527
|
+
type="button"
|
|
528
|
+
className="dc-annot-ctx-sw"
|
|
529
|
+
aria-label={`${effectiveMode === 'stroke' ? 'Color' : 'Fill'} ${c}`}
|
|
530
|
+
aria-pressed={activeValue === c}
|
|
531
|
+
title={`${effectiveMode === 'stroke' ? 'Color' : 'Fill'} ${c}`}
|
|
532
|
+
style={{ background: c }}
|
|
533
|
+
onClick={() => onSwatchClick(c)}
|
|
534
|
+
/>
|
|
535
|
+
))}
|
|
536
|
+
</div>
|
|
401
537
|
{caps.thickness && effectiveMode === 'stroke' ? (
|
|
402
538
|
<>
|
|
403
539
|
<div className="dc-annot-ctx-sep" />
|
|
404
540
|
<button
|
|
405
541
|
type="button"
|
|
406
|
-
className="dc-annot-ctx-
|
|
542
|
+
className="dc-annot-ctx-ibtn"
|
|
543
|
+
aria-label="Thin stroke"
|
|
407
544
|
aria-pressed={uniqThickness === 3}
|
|
408
545
|
title="Thin (3px)"
|
|
409
546
|
onClick={() => setThickness(3)}
|
|
410
547
|
>
|
|
411
|
-
|
|
548
|
+
<IconLineThin />
|
|
412
549
|
</button>
|
|
413
550
|
<button
|
|
414
551
|
type="button"
|
|
415
|
-
className="dc-annot-ctx-
|
|
552
|
+
className="dc-annot-ctx-ibtn"
|
|
553
|
+
aria-label="Thick stroke"
|
|
416
554
|
aria-pressed={uniqThickness === 6}
|
|
417
555
|
title="Thick (6px)"
|
|
418
556
|
onClick={() => setThickness(6)}
|
|
419
557
|
>
|
|
420
|
-
|
|
558
|
+
<IconLineThick />
|
|
421
559
|
</button>
|
|
422
560
|
</>
|
|
423
561
|
) : null}
|
|
@@ -426,41 +564,140 @@ export function AnnotationContextToolbar() {
|
|
|
426
564
|
<div className="dc-annot-ctx-sep" />
|
|
427
565
|
<button
|
|
428
566
|
type="button"
|
|
429
|
-
className="dc-annot-ctx-
|
|
567
|
+
className="dc-annot-ctx-ibtn"
|
|
568
|
+
aria-label="Small text"
|
|
430
569
|
aria-pressed={uniqFontSize === 12}
|
|
431
570
|
title="Small (12px)"
|
|
432
571
|
onClick={() => setFontSize(12)}
|
|
433
572
|
>
|
|
434
|
-
|
|
573
|
+
<IconLetterA size={13} />
|
|
435
574
|
</button>
|
|
436
575
|
<button
|
|
437
576
|
type="button"
|
|
438
|
-
className="dc-annot-ctx-
|
|
577
|
+
className="dc-annot-ctx-ibtn"
|
|
578
|
+
aria-label="Medium text"
|
|
439
579
|
aria-pressed={uniqFontSize === 14}
|
|
440
580
|
title="Medium (14px)"
|
|
441
581
|
onClick={() => setFontSize(14)}
|
|
442
582
|
>
|
|
443
|
-
|
|
583
|
+
<IconLetterA size={16} />
|
|
444
584
|
</button>
|
|
445
585
|
<button
|
|
446
586
|
type="button"
|
|
447
|
-
className="dc-annot-ctx-
|
|
587
|
+
className="dc-annot-ctx-ibtn"
|
|
588
|
+
aria-label="Large text"
|
|
448
589
|
aria-pressed={uniqFontSize === 20}
|
|
449
590
|
title="Large (20px)"
|
|
450
591
|
onClick={() => setFontSize(20)}
|
|
451
592
|
>
|
|
452
|
-
|
|
593
|
+
<IconLetterA size={19} />
|
|
594
|
+
</button>
|
|
595
|
+
</>
|
|
596
|
+
) : null}
|
|
597
|
+
{caps.cornerRadius ? (
|
|
598
|
+
<>
|
|
599
|
+
<div className="dc-annot-ctx-sep" />
|
|
600
|
+
<button
|
|
601
|
+
type="button"
|
|
602
|
+
className="dc-annot-ctx-ibtn"
|
|
603
|
+
aria-label="Square corners"
|
|
604
|
+
aria-pressed={uniqRadius === 0}
|
|
605
|
+
title="Square corners"
|
|
606
|
+
onClick={() => setCornerRadius(0)}
|
|
607
|
+
>
|
|
608
|
+
<IconCornerSquare />
|
|
609
|
+
</button>
|
|
610
|
+
<button
|
|
611
|
+
type="button"
|
|
612
|
+
className="dc-annot-ctx-ibtn"
|
|
613
|
+
aria-label="Soft corners"
|
|
614
|
+
aria-pressed={uniqRadius === 8}
|
|
615
|
+
title="Soft corners"
|
|
616
|
+
onClick={() => setCornerRadius(8)}
|
|
617
|
+
>
|
|
618
|
+
<IconCornerSoft />
|
|
619
|
+
</button>
|
|
620
|
+
<button
|
|
621
|
+
type="button"
|
|
622
|
+
className="dc-annot-ctx-ibtn"
|
|
623
|
+
aria-label="Pill corners"
|
|
624
|
+
aria-pressed={uniqRadius === 999}
|
|
625
|
+
title="Pill corners"
|
|
626
|
+
onClick={() => setCornerRadius(999)}
|
|
627
|
+
>
|
|
628
|
+
<IconCornerPill />
|
|
629
|
+
</button>
|
|
630
|
+
</>
|
|
631
|
+
) : null}
|
|
632
|
+
{caps.arrowDir ? (
|
|
633
|
+
<>
|
|
634
|
+
<div className="dc-annot-ctx-sep" />
|
|
635
|
+
<button
|
|
636
|
+
type="button"
|
|
637
|
+
className="dc-annot-ctx-ibtn"
|
|
638
|
+
aria-label="Line, no arrowheads"
|
|
639
|
+
aria-pressed={uniqStartHead === 'none' && uniqEndHead === 'none'}
|
|
640
|
+
title="Line (no heads)"
|
|
641
|
+
onClick={() => setArrowDir('none', 'none')}
|
|
642
|
+
>
|
|
643
|
+
<IconArrowNone />
|
|
644
|
+
</button>
|
|
645
|
+
<button
|
|
646
|
+
type="button"
|
|
647
|
+
className="dc-annot-ctx-ibtn"
|
|
648
|
+
aria-label="Arrowhead at start"
|
|
649
|
+
aria-pressed={uniqStartHead === 'triangle' && uniqEndHead === 'none'}
|
|
650
|
+
title="Head at start"
|
|
651
|
+
onClick={() => setArrowDir('triangle', 'none')}
|
|
652
|
+
>
|
|
653
|
+
<IconArrowStartHead />
|
|
654
|
+
</button>
|
|
655
|
+
<button
|
|
656
|
+
type="button"
|
|
657
|
+
className="dc-annot-ctx-ibtn"
|
|
658
|
+
aria-label="Arrowhead at end"
|
|
659
|
+
aria-pressed={uniqStartHead === 'none' && uniqEndHead === 'triangle'}
|
|
660
|
+
title="Head at end"
|
|
661
|
+
onClick={() => setArrowDir('none', 'triangle')}
|
|
662
|
+
>
|
|
663
|
+
<IconArrowEndHead />
|
|
664
|
+
</button>
|
|
665
|
+
<button
|
|
666
|
+
type="button"
|
|
667
|
+
className="dc-annot-ctx-ibtn"
|
|
668
|
+
aria-label="Arrowheads at both ends"
|
|
669
|
+
aria-pressed={uniqStartHead === 'triangle' && uniqEndHead === 'triangle'}
|
|
670
|
+
title="Heads at both ends"
|
|
671
|
+
onClick={() => setArrowDir('triangle', 'triangle')}
|
|
672
|
+
>
|
|
673
|
+
<IconArrowBothHeads />
|
|
674
|
+
</button>
|
|
675
|
+
</>
|
|
676
|
+
) : null}
|
|
677
|
+
{caps.dash ? (
|
|
678
|
+
<>
|
|
679
|
+
<div className="dc-annot-ctx-sep" />
|
|
680
|
+
<button
|
|
681
|
+
type="button"
|
|
682
|
+
className="dc-annot-ctx-ibtn"
|
|
683
|
+
aria-label="Dashed line"
|
|
684
|
+
aria-pressed={uniqDashed === true}
|
|
685
|
+
title="Dashed line"
|
|
686
|
+
onClick={() => setDashed(!(uniqDashed === true))}
|
|
687
|
+
>
|
|
688
|
+
<IconDash />
|
|
453
689
|
</button>
|
|
454
690
|
</>
|
|
455
691
|
) : null}
|
|
692
|
+
<div className="dc-annot-ctx-sep" />
|
|
456
693
|
<button
|
|
457
694
|
type="button"
|
|
458
|
-
className="dc-annot-ctx-
|
|
695
|
+
className="dc-annot-ctx-ibtn dc-annot-ctx-ibtn--danger"
|
|
459
696
|
title="Delete (Backspace)"
|
|
460
697
|
aria-label="Delete selected annotations"
|
|
461
698
|
onClick={remove}
|
|
462
699
|
>
|
|
463
|
-
|
|
700
|
+
<IconTrash />
|
|
464
701
|
</button>
|
|
465
702
|
</div>
|
|
466
703
|
);
|