@1agh/maude 0.23.0 → 0.25.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 +32 -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 +126 -2
- 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 +74 -10
- 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/plugin-cli-reachability.test.mjs +56 -0
- package/cli/lib/preflight.mjs +41 -10
- package/package.json +8 -8
- package/plugins/design/dependencies.json +30 -2
- package/plugins/design/dev-server/ai-banner.tsx +2 -2
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +763 -123
- package/plugins/design/dev-server/annotations-layer.tsx +1624 -208
- package/plugins/design/dev-server/api.ts +123 -4
- package/plugins/design/dev-server/bin/_enumerate-artboards-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_html-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pdf-playwright.mjs +25 -8
- package/plugins/design/dev-server/bin/_png-playwright.mjs +20 -20
- package/plugins/design/dev-server/bin/_pptx-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pw-launch.mjs +61 -0
- package/plugins/design/dev-server/bin/_screenshot-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_svg-playwright.mjs +125 -19
- 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 +208 -23
- package/plugins/design/dev-server/canvas-arrowheads.ts +220 -0
- package/plugins/design/dev-server/canvas-comment-mount.tsx +8 -24
- package/plugins/design/dev-server/canvas-cursors.ts +173 -0
- package/plugins/design/dev-server/canvas-icons.tsx +299 -0
- package/plugins/design/dev-server/canvas-lib.tsx +25 -21
- package/plugins/design/dev-server/canvas-meta.schema.json +20 -0
- package/plugins/design/dev-server/canvas-shell.tsx +404 -66
- package/plugins/design/dev-server/client/app.jsx +1231 -416
- package/plugins/design/dev-server/client/styles/3-shell.css +10 -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 +80 -2
- package/plugins/design/dev-server/collab/room.ts +21 -8
- package/plugins/design/dev-server/context-menu.tsx +167 -23
- package/plugins/design/dev-server/context.ts +24 -0
- package/plugins/design/dev-server/contextual-toolbar.tsx +7 -7
- package/plugins/design/dev-server/dist/client.bundle.js +364 -29
- package/plugins/design/dev-server/dist/comment-mount.js +78 -33
- package/plugins/design/dev-server/dist/styles.css +16 -0
- package/plugins/design/dev-server/equal-spacing-handles.tsx +1 -1
- package/plugins/design/dev-server/export-dialog.tsx +208 -18
- package/plugins/design/dev-server/exporters/html.ts +4 -1
- package/plugins/design/dev-server/exporters/index.ts +4 -1
- package/plugins/design/dev-server/exporters/pdf.ts +7 -1
- package/plugins/design/dev-server/exporters/png.ts +21 -2
- package/plugins/design/dev-server/exporters/pptx.ts +330 -201
- package/plugins/design/dev-server/exporters/scope.ts +107 -11
- package/plugins/design/dev-server/exporters/svg.ts +4 -1
- package/plugins/design/dev-server/fs-watch.ts +1 -0
- package/plugins/design/dev-server/http.ts +299 -28
- package/plugins/design/dev-server/input-router.tsx +35 -5
- package/plugins/design/dev-server/participants-chrome.tsx +10 -10
- package/plugins/design/dev-server/server.ts +123 -1
- package/plugins/design/dev-server/sync/agent.ts +95 -0
- package/plugins/design/dev-server/sync/codec.ts +155 -0
- package/plugins/design/dev-server/sync/connection-state.ts +203 -0
- package/plugins/design/dev-server/sync/index.ts +479 -35
- 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-draw-modifiers.test.ts +206 -0
- package/plugins/design/dev-server/test/annotations-layer.test.ts +311 -0
- package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +519 -0
- package/plugins/design/dev-server/test/canvas-cursors.test.ts +162 -0
- package/plugins/design/dev-server/test/canvas-origin-gate.test.ts +76 -0
- package/plugins/design/dev-server/test/canvas-route.test.ts +4 -1
- 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/csp-canvas-shell.test.ts +46 -0
- package/plugins/design/dev-server/test/exporters/png.test.ts +24 -1
- package/plugins/design/dev-server/test/exporters/pptx-deck.test.ts +112 -0
- package/plugins/design/dev-server/test/exporters/pw-launch.test.ts +49 -0
- package/plugins/design/dev-server/test/exporters/scope.test.ts +0 -0
- package/plugins/design/dev-server/test/fixtures/phase-20-annotations.svg +1 -0
- package/plugins/design/dev-server/test/fixtures/phase-21-annotations.svg +1 -0
- package/plugins/design/dev-server/test/input-router.test.ts +32 -4
- package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +132 -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-connection-state.test.ts +146 -0
- package/plugins/design/dev-server/test/sync-meta-codec.test.ts +123 -0
- package/plugins/design/dev-server/test/sync-runtime.test.ts +531 -4
- 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-annotation-resize.test.ts +200 -0
- package/plugins/design/dev-server/test/use-tool-mode.test.tsx +39 -13
- package/plugins/design/dev-server/tool-palette.tsx +155 -24
- 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 +219 -61
- package/plugins/design/dev-server/use-tool-mode.tsx +68 -19
- package/plugins/design/dev-server/ws.ts +40 -1
- package/plugins/design/templates/_shell.html +85 -10
- package/plugins/design/templates/canvas.tsx.template +13 -0
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +14 -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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// T4.5 (DDR-054 §3 F3) — untrusted-marker for sync-written files. Verifies the
|
|
2
|
+
// `_untrusted/INDEX.json` marker + the managed `.claudeignore` block: written
|
|
3
|
+
// for the syncable set, cleared when empty, stale entries dropped on re-write,
|
|
4
|
+
// and user-authored `.claudeignore` content outside the block preserved.
|
|
5
|
+
|
|
6
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
11
|
+
|
|
12
|
+
import type { Context } from '../context.ts';
|
|
13
|
+
import type { CanvasDescriptor } from '../sync/index.ts';
|
|
14
|
+
import { clearUntrustedMarkers, writeUntrustedMarkers } from '../sync/untrusted.ts';
|
|
15
|
+
|
|
16
|
+
let repoRoot: string;
|
|
17
|
+
let designRoot: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
repoRoot = mkdtempSync(join(tmpdir(), 'untrusted-'));
|
|
21
|
+
designRoot = join(repoRoot, '.design');
|
|
22
|
+
mkdirSync(designRoot, { recursive: true });
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
/* tmpdir — left for the OS to reap */
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function ctxOf(): Context {
|
|
29
|
+
return { paths: { repoRoot, designRoot } } as unknown as Context;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function desc(slug: string, body: string): CanvasDescriptor {
|
|
33
|
+
return {
|
|
34
|
+
slug,
|
|
35
|
+
html: join(designRoot, body),
|
|
36
|
+
comments: join(designRoot, '_comments', `${slug}.json`),
|
|
37
|
+
annotations: join(designRoot, `${slug}.annotations.svg`),
|
|
38
|
+
meta: join(designRoot, body.replace(/\.(tsx|html)$/i, '.meta.json')),
|
|
39
|
+
css: join(designRoot, body.replace(/\.(tsx|html)$/i, '.css')),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const HUB = 'https://hub.example.com';
|
|
44
|
+
|
|
45
|
+
describe('writeUntrustedMarkers — T4.5', () => {
|
|
46
|
+
test('writes INDEX.json + a managed .claudeignore block for the synced set', () => {
|
|
47
|
+
const ctx = ctxOf();
|
|
48
|
+
writeUntrustedMarkers(ctx, [desc('ui-a', 'ui/a.tsx')], HUB);
|
|
49
|
+
|
|
50
|
+
const indexAbs = join(designRoot, '_untrusted', 'INDEX.json');
|
|
51
|
+
expect(existsSync(indexAbs)).toBe(true);
|
|
52
|
+
const index = JSON.parse(readFileSync(indexAbs, 'utf8'));
|
|
53
|
+
expect(index.hubUrl).toBe(HUB);
|
|
54
|
+
expect(index.canvases[0].slug).toBe('ui-a');
|
|
55
|
+
expect(index.canvases[0].body).toBe('.design/ui/a.tsx');
|
|
56
|
+
expect(index.canvases[0].meta).toBe('.design/ui/a.meta.json'); // Gap 2: meta is untrusted too
|
|
57
|
+
expect(index.canvases[0].css).toBe('.design/ui/a.css'); // Gap 3: css too
|
|
58
|
+
expect(index.note.toLowerCase()).toContain('untrusted');
|
|
59
|
+
|
|
60
|
+
const ci = readFileSync(join(repoRoot, '.claudeignore'), 'utf8');
|
|
61
|
+
expect(ci).toContain('maude:sync-untrusted begin');
|
|
62
|
+
expect(ci).toContain('.design/ui/a.tsx');
|
|
63
|
+
expect(ci).toContain('.design/ui-a.annotations.svg');
|
|
64
|
+
expect(ci).toContain('.design/ui/a.meta.json');
|
|
65
|
+
expect(ci).toContain('.design/ui/a.css');
|
|
66
|
+
expect(ci).toContain('maude:sync-untrusted end');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('re-write with a smaller set drops the stale entry', () => {
|
|
70
|
+
const ctx = ctxOf();
|
|
71
|
+
writeUntrustedMarkers(ctx, [desc('ui-a', 'ui/a.tsx'), desc('ui-b', 'ui/b.tsx')], HUB);
|
|
72
|
+
writeUntrustedMarkers(ctx, [desc('ui-a', 'ui/a.tsx')], HUB);
|
|
73
|
+
const ci = readFileSync(join(repoRoot, '.claudeignore'), 'utf8');
|
|
74
|
+
expect(ci).toContain('.design/ui/a.tsx');
|
|
75
|
+
expect(ci).not.toContain('.design/ui/b.tsx');
|
|
76
|
+
// Only one managed block (no accumulation).
|
|
77
|
+
expect(ci.match(/maude:sync-untrusted begin/g)?.length).toBe(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('empty set clears INDEX.json + the .claudeignore block', () => {
|
|
81
|
+
const ctx = ctxOf();
|
|
82
|
+
writeUntrustedMarkers(ctx, [desc('ui-a', 'ui/a.tsx')], HUB);
|
|
83
|
+
writeUntrustedMarkers(ctx, [], HUB);
|
|
84
|
+
expect(existsSync(join(designRoot, '_untrusted', 'INDEX.json'))).toBe(false);
|
|
85
|
+
// .claudeignore had only our block → removed entirely.
|
|
86
|
+
expect(existsSync(join(repoRoot, '.claudeignore'))).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('preserves user-authored .claudeignore content outside the block', () => {
|
|
90
|
+
writeFileSync(join(repoRoot, '.claudeignore'), 'secrets/\n*.key\n');
|
|
91
|
+
const ctx = ctxOf();
|
|
92
|
+
writeUntrustedMarkers(ctx, [desc('ui-a', 'ui/a.tsx')], HUB);
|
|
93
|
+
let ci = readFileSync(join(repoRoot, '.claudeignore'), 'utf8');
|
|
94
|
+
expect(ci).toContain('secrets/');
|
|
95
|
+
expect(ci).toContain('*.key');
|
|
96
|
+
expect(ci).toContain('.design/ui/a.tsx');
|
|
97
|
+
// Clearing leaves the user content intact.
|
|
98
|
+
clearUntrustedMarkers(ctx);
|
|
99
|
+
ci = readFileSync(join(repoRoot, '.claudeignore'), 'utf8');
|
|
100
|
+
expect(ci).toContain('secrets/');
|
|
101
|
+
expect(ci).not.toContain('.design/ui/a.tsx');
|
|
102
|
+
expect(ci).not.toContain('maude:sync-untrusted');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// use-annotation-resize — FigJam resize-modifier math (pure `resizeStroke`).
|
|
2
|
+
//
|
|
3
|
+
// Covers the no-modifier baseline (must stay algebraically identical to the
|
|
4
|
+
// pre-modifier corner math) plus Shift (aspect-lock / 45° snap), Alt
|
|
5
|
+
// (scale-from-center / mirror-from-midpoint), and Shift+Alt combined, across
|
|
6
|
+
// every resizable stroke kind. Pure function — no DOM render needed.
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from 'bun:test';
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ArrowStroke,
|
|
12
|
+
EllipseStroke,
|
|
13
|
+
PenStroke,
|
|
14
|
+
PolygonStroke,
|
|
15
|
+
RectStroke,
|
|
16
|
+
StickyStroke,
|
|
17
|
+
} from '../annotations-layer.tsx';
|
|
18
|
+
import { resizeStroke } from '../use-annotation-resize.tsx';
|
|
19
|
+
|
|
20
|
+
const rect: RectStroke = {
|
|
21
|
+
id: 'r1',
|
|
22
|
+
tool: 'rect',
|
|
23
|
+
color: '#000',
|
|
24
|
+
width: 2,
|
|
25
|
+
x: 0,
|
|
26
|
+
y: 0,
|
|
27
|
+
w: 100,
|
|
28
|
+
h: 50,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe('resizeStroke / rect — no modifiers (baseline)', () => {
|
|
32
|
+
test('se corner anchors the nw corner (back-compat)', () => {
|
|
33
|
+
expect(resizeStroke(rect, 'se', 120, 80)).toEqual({ x: 0, y: 0, w: 120, h: 80 });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('nw corner anchors the se corner', () => {
|
|
37
|
+
expect(resizeStroke(rect, 'nw', -20, -10)).toEqual({ x: -20, y: -10, w: 120, h: 60 });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('dragging past the anchor flips the box (min/max parity)', () => {
|
|
41
|
+
// se dragged to the left of x=0 → box lives to the left of the anchor.
|
|
42
|
+
expect(resizeStroke(rect, 'se', -40, 80)).toEqual({ x: -40, y: 0, w: 40, h: 80 });
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('resizeStroke / rect — Shift (aspect lock)', () => {
|
|
47
|
+
test('keeps the 2:1 start ratio, dominant axis drives scale', () => {
|
|
48
|
+
// raw 120×200 → scale = max(1.2, 4) = 4 → 400×200 (ratio preserved).
|
|
49
|
+
expect(resizeStroke(rect, 'se', 120, 200, { shift: true, alt: false })).toEqual({
|
|
50
|
+
x: 0,
|
|
51
|
+
y: 0,
|
|
52
|
+
w: 400,
|
|
53
|
+
h: 200,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('resizeStroke / rect — Alt (scale from center)', () => {
|
|
59
|
+
test('center stays fixed; both sides grow symmetrically', () => {
|
|
60
|
+
const out = resizeStroke(rect, 'se', 120, 80, { shift: false, alt: true });
|
|
61
|
+
expect(out).toEqual({ x: -20, y: -30, w: 140, h: 110 });
|
|
62
|
+
// center invariant: (-20 + 140/2, -30 + 110/2) === (50, 25) === start center.
|
|
63
|
+
expect((out as RectStroke).x + (out as RectStroke).w / 2).toBe(50);
|
|
64
|
+
expect((out as RectStroke).y + (out as RectStroke).h / 2).toBe(25);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('resizeStroke / rect — Shift+Alt (ratio + center)', () => {
|
|
69
|
+
test('center-anchored and ratio-locked at once', () => {
|
|
70
|
+
expect(resizeStroke(rect, 'se', 120, 200, { shift: true, alt: true })).toEqual({
|
|
71
|
+
x: -300,
|
|
72
|
+
y: -150,
|
|
73
|
+
w: 700,
|
|
74
|
+
h: 350,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('resizeStroke / polygon — routes through the shared bbox math', () => {
|
|
80
|
+
const poly: PolygonStroke = {
|
|
81
|
+
id: 'p1',
|
|
82
|
+
tool: 'polygon',
|
|
83
|
+
shape: 'diamond',
|
|
84
|
+
color: '#000',
|
|
85
|
+
width: 2,
|
|
86
|
+
x: 0,
|
|
87
|
+
y: 0,
|
|
88
|
+
w: 100,
|
|
89
|
+
h: 50,
|
|
90
|
+
};
|
|
91
|
+
test('Shift aspect-locks the polygon like a rect', () => {
|
|
92
|
+
expect(resizeStroke(poly, 'se', 120, 200, { shift: true, alt: false })).toEqual({
|
|
93
|
+
x: 0,
|
|
94
|
+
y: 0,
|
|
95
|
+
w: 400,
|
|
96
|
+
h: 200,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('resizeStroke / sticky — always 1:1 square', () => {
|
|
102
|
+
const sticky: StickyStroke = {
|
|
103
|
+
id: 's1',
|
|
104
|
+
tool: 'sticky',
|
|
105
|
+
color: '#fce8a6',
|
|
106
|
+
x: 0,
|
|
107
|
+
y: 0,
|
|
108
|
+
w: 100,
|
|
109
|
+
h: 100,
|
|
110
|
+
text: '',
|
|
111
|
+
fontSize: 16,
|
|
112
|
+
};
|
|
113
|
+
test('no modifiers — larger axis becomes the side', () => {
|
|
114
|
+
expect(resizeStroke(sticky, 'se', 150, 120)).toEqual({ x: 0, y: 0, w: 150, h: 150 });
|
|
115
|
+
});
|
|
116
|
+
test('Alt — square grows from the center', () => {
|
|
117
|
+
expect(resizeStroke(sticky, 'se', 150, 120, { shift: false, alt: true })).toEqual({
|
|
118
|
+
x: -50,
|
|
119
|
+
y: -50,
|
|
120
|
+
w: 200,
|
|
121
|
+
h: 200,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('resizeStroke / ellipse — Alt keeps the center fixed', () => {
|
|
127
|
+
const ell: EllipseStroke = {
|
|
128
|
+
id: 'e1',
|
|
129
|
+
tool: 'ellipse',
|
|
130
|
+
color: '#000',
|
|
131
|
+
width: 2,
|
|
132
|
+
cx: 50,
|
|
133
|
+
cy: 50,
|
|
134
|
+
rx: 40,
|
|
135
|
+
ry: 20,
|
|
136
|
+
};
|
|
137
|
+
test('center invariant under symmetric scale', () => {
|
|
138
|
+
expect(resizeStroke(ell, 'se', 120, 90, { shift: false, alt: true })).toEqual({
|
|
139
|
+
cx: 50,
|
|
140
|
+
cy: 50,
|
|
141
|
+
rx: 70,
|
|
142
|
+
ry: 40,
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('resizeStroke / arrow', () => {
|
|
148
|
+
const arrow: ArrowStroke = {
|
|
149
|
+
id: 'a1',
|
|
150
|
+
tool: 'arrow',
|
|
151
|
+
color: '#000',
|
|
152
|
+
width: 2,
|
|
153
|
+
x1: 0,
|
|
154
|
+
y1: 0,
|
|
155
|
+
x2: 100,
|
|
156
|
+
y2: 0,
|
|
157
|
+
};
|
|
158
|
+
test('no modifiers — only the dragged endpoint moves', () => {
|
|
159
|
+
expect(resizeStroke(arrow, 'ep2', 100, 40)).toEqual({ x2: 100, y2: 40 });
|
|
160
|
+
});
|
|
161
|
+
test('Alt — the midpoint is pinned; the far end mirrors', () => {
|
|
162
|
+
expect(resizeStroke(arrow, 'ep2', 100, 40, { shift: false, alt: true })).toEqual({
|
|
163
|
+
x2: 100,
|
|
164
|
+
y2: 40,
|
|
165
|
+
x1: 0,
|
|
166
|
+
y1: -40,
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
test('Shift — shaft angle snaps to the nearest 45°', () => {
|
|
170
|
+
const out = resizeStroke(arrow, 'ep2', 50, 60, { shift: true, alt: false }) as ArrowStroke;
|
|
171
|
+
// Pointer at atan2(60,50)≈49° snaps to 45° → dx === dy along the shaft.
|
|
172
|
+
expect(out.x2).toBeCloseTo(out.y2 ?? Number.NaN, 6);
|
|
173
|
+
expect(out.x2).toBeGreaterThan(0);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('resizeStroke / pen — point scaling', () => {
|
|
178
|
+
const pen: PenStroke = {
|
|
179
|
+
id: 'pen1',
|
|
180
|
+
tool: 'pen',
|
|
181
|
+
color: '#000',
|
|
182
|
+
width: 2,
|
|
183
|
+
points: [
|
|
184
|
+
[0, 0],
|
|
185
|
+
[100, 0],
|
|
186
|
+
[100, 100],
|
|
187
|
+
[0, 100],
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
test('no modifiers — scales around the opposite corner', () => {
|
|
191
|
+
const out = resizeStroke(pen, 'se', 200, 200) as PenStroke;
|
|
192
|
+
expect(out.points[0]).toEqual([0, 0]);
|
|
193
|
+
expect(out.points[2]).toEqual([200, 200]);
|
|
194
|
+
});
|
|
195
|
+
test('Alt — scales around the bbox center', () => {
|
|
196
|
+
const out = resizeStroke(pen, 'se', 150, 150, { shift: false, alt: true }) as PenStroke;
|
|
197
|
+
expect(out.points[0]).toEqual([-50, -50]);
|
|
198
|
+
expect(out.points[2]).toEqual([150, 150]);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -12,34 +12,60 @@ import {
|
|
|
12
12
|
} from '../use-tool-mode.tsx';
|
|
13
13
|
|
|
14
14
|
describe('use-tool-mode / static', () => {
|
|
15
|
-
test('DEFAULT_TOOLS exposes V/H/C +
|
|
15
|
+
test('DEFAULT_TOOLS exposes V/H/C + draw set B/R/N/A/T/E (Phase 24 single Shape tool)', () => {
|
|
16
16
|
expect(DEFAULT_TOOLS.map((t) => t.id)).toEqual([
|
|
17
17
|
'move',
|
|
18
18
|
'hand',
|
|
19
19
|
'comment',
|
|
20
20
|
'pen',
|
|
21
|
-
'
|
|
22
|
-
'
|
|
21
|
+
'shape',
|
|
22
|
+
'sticky',
|
|
23
23
|
'arrow',
|
|
24
|
+
'text',
|
|
24
25
|
'eraser',
|
|
25
26
|
]);
|
|
26
|
-
expect(DEFAULT_TOOLS.map((t) => t.shortcut)).toEqual([
|
|
27
|
+
expect(DEFAULT_TOOLS.map((t) => t.shortcut)).toEqual([
|
|
28
|
+
'V',
|
|
29
|
+
'H',
|
|
30
|
+
'C',
|
|
31
|
+
'B',
|
|
32
|
+
'R',
|
|
33
|
+
'N',
|
|
34
|
+
'A',
|
|
35
|
+
'T',
|
|
36
|
+
'E',
|
|
37
|
+
]);
|
|
27
38
|
});
|
|
28
39
|
|
|
29
40
|
test('DEFAULT_TOOLS is immutable (Object.freeze applied)', () => {
|
|
30
41
|
expect(Object.isFrozen(DEFAULT_TOOLS)).toBe(true);
|
|
31
42
|
});
|
|
32
43
|
|
|
33
|
-
test('
|
|
44
|
+
test('cursors per tool (Phase 24 — ONE Kenney library for every tool + native fallback)', () => {
|
|
34
45
|
const byId = Object.fromEntries(DEFAULT_TOOLS.map((t) => [t.id, t.cursor]));
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
// Phase 24 — EVERY tool (incl. move) ships a Kenney data-URI SVG cursor that
|
|
47
|
+
// falls back to the right native cursor if the image is rejected.
|
|
48
|
+
for (const id of [
|
|
49
|
+
'move',
|
|
50
|
+
'hand',
|
|
51
|
+
'comment',
|
|
52
|
+
'pen',
|
|
53
|
+
'shape',
|
|
54
|
+
'sticky',
|
|
55
|
+
'arrow',
|
|
56
|
+
'text',
|
|
57
|
+
'eraser',
|
|
58
|
+
]) {
|
|
59
|
+
expect(byId[id]).toContain('url("data:image/svg+xml,');
|
|
60
|
+
expect(byId[id]).toContain('32'); // 32×32 cursor
|
|
61
|
+
}
|
|
62
|
+
// Native fallbacks are preserved after the custom URL.
|
|
63
|
+
expect(byId.move).toMatch(/, default$/);
|
|
64
|
+
expect(byId.hand).toMatch(/, grab$/);
|
|
65
|
+
expect(byId.text).toMatch(/, text$/);
|
|
66
|
+
expect(byId.eraser).toMatch(/, cell$/);
|
|
67
|
+
expect(byId.pen).toMatch(/, crosshair$/);
|
|
68
|
+
expect(byId.shape).toMatch(/, crosshair$/);
|
|
43
69
|
});
|
|
44
70
|
});
|
|
45
71
|
|
|
@@ -14,10 +14,15 @@
|
|
|
14
14
|
|
|
15
15
|
import { useEffect, useRef, useState } from 'react';
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
IconChevronDown,
|
|
19
|
+
IconPresentation,
|
|
20
|
+
SHAPE_KIND_ICONS,
|
|
21
|
+
TOOL_ICONS,
|
|
22
|
+
} from './canvas-icons.tsx';
|
|
18
23
|
import { useViewportControllerContext } from './canvas-lib.tsx';
|
|
19
24
|
import { useAnnotationsVisibility } from './use-annotations-visibility.tsx';
|
|
20
|
-
import { useToolMode } from './use-tool-mode.tsx';
|
|
25
|
+
import { type ShapeKind, useToolMode } from './use-tool-mode.tsx';
|
|
21
26
|
|
|
22
27
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
28
|
// Styles — pulled from menubar tokens (`.mb` family). Centered bottom toolbar,
|
|
@@ -35,13 +40,13 @@ const PALETTE_CSS = `
|
|
|
35
40
|
transform: translateX(-50%);
|
|
36
41
|
display: flex;
|
|
37
42
|
align-items: stretch;
|
|
38
|
-
background: var(--
|
|
39
|
-
border: 1px solid var(--
|
|
43
|
+
background: var(--maude-chrome-bg-0, #ffffff);
|
|
44
|
+
border: 1px solid var(--maude-chrome-fg-0, #1c1917);
|
|
40
45
|
border-radius: 8px;
|
|
41
|
-
box-shadow: 0 6px 24px color-mix(in oklab,
|
|
42
|
-
font-family: var(--
|
|
46
|
+
box-shadow: 0 6px 24px var(--maude-chrome-shadow, color-mix(in oklab, #1c1917 10%, transparent));
|
|
47
|
+
font-family: var(--maude-chrome-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
43
48
|
font-size: 12px;
|
|
44
|
-
color: var(--
|
|
49
|
+
color: var(--maude-chrome-fg-0, #1a1a1a);
|
|
45
50
|
z-index: 6;
|
|
46
51
|
user-select: none;
|
|
47
52
|
/* Intentionally NO overflow:hidden — the zoom popover (.dc-tp-popover) is
|
|
@@ -56,7 +61,7 @@ const PALETTE_CSS = `
|
|
|
56
61
|
}
|
|
57
62
|
.dc-tool-palette .dc-tp-sep {
|
|
58
63
|
width: 1px;
|
|
59
|
-
background: var(--
|
|
64
|
+
background: var(--maude-chrome-border, rgba(0,0,0,0.08));
|
|
60
65
|
margin: 6px 0;
|
|
61
66
|
}
|
|
62
67
|
.dc-tool-palette button {
|
|
@@ -70,14 +75,14 @@ const PALETTE_CSS = `
|
|
|
70
75
|
display: inline-flex;
|
|
71
76
|
align-items: center;
|
|
72
77
|
justify-content: center;
|
|
73
|
-
color: var(--fg-1, rgba(40,30,20,0.75));
|
|
78
|
+
color: var(--maude-chrome-fg-1, rgba(40,30,20,0.75));
|
|
74
79
|
cursor: pointer;
|
|
75
80
|
position: relative;
|
|
76
81
|
transition: background-color 80ms linear, color 80ms linear;
|
|
77
82
|
}
|
|
78
83
|
.dc-tool-palette button:hover {
|
|
79
84
|
background: color-mix(in oklab, var(--maude-hud-accent, #d63b1f) 8%, transparent);
|
|
80
|
-
color: var(--fg-0, #1a1a1a);
|
|
85
|
+
color: var(--maude-chrome-fg-0, #1a1a1a);
|
|
81
86
|
}
|
|
82
87
|
.dc-tool-palette button:focus-visible {
|
|
83
88
|
outline: 2px solid var(--maude-hud-accent, #d63b1f);
|
|
@@ -117,7 +122,7 @@ const PALETTE_CSS = `
|
|
|
117
122
|
height: 6px;
|
|
118
123
|
background: var(--maude-hud-accent, #d63b1f);
|
|
119
124
|
border-radius: 1px;
|
|
120
|
-
box-shadow: 0 0 0 1px var(--bg-0, #ffffff);
|
|
125
|
+
box-shadow: 0 0 0 1px var(--maude-chrome-bg-0, #ffffff);
|
|
121
126
|
opacity: 0;
|
|
122
127
|
transition: opacity 50ms linear 50ms;
|
|
123
128
|
pointer-events: none;
|
|
@@ -132,7 +137,7 @@ const PALETTE_CSS = `
|
|
|
132
137
|
min-width: 56px;
|
|
133
138
|
padding: 0 8px;
|
|
134
139
|
font-variant-numeric: tabular-nums;
|
|
135
|
-
font-family: var(--
|
|
140
|
+
font-family: var(--maude-chrome-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
136
141
|
font-size: 11px;
|
|
137
142
|
letter-spacing: 0.04em;
|
|
138
143
|
border-radius: 6px;
|
|
@@ -145,16 +150,16 @@ const PALETTE_CSS = `
|
|
|
145
150
|
position: absolute;
|
|
146
151
|
right: 4px;
|
|
147
152
|
bottom: 44px;
|
|
148
|
-
background: var(--
|
|
149
|
-
border: 1px solid var(--
|
|
153
|
+
background: var(--maude-chrome-bg-0, #ffffff);
|
|
154
|
+
border: 1px solid var(--maude-chrome-fg-0, #1c1917);
|
|
150
155
|
border-radius: 8px;
|
|
151
|
-
box-shadow: 0 6px 24px color-mix(in oklab,
|
|
156
|
+
box-shadow: 0 6px 24px var(--maude-chrome-shadow, color-mix(in oklab, #1c1917 10%, transparent));
|
|
152
157
|
display: flex;
|
|
153
158
|
flex-direction: column;
|
|
154
159
|
padding: 4px;
|
|
155
160
|
min-width: 160px;
|
|
156
161
|
z-index: 7;
|
|
157
|
-
font-family: var(--
|
|
162
|
+
font-family: var(--maude-chrome-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
158
163
|
}
|
|
159
164
|
.dc-tp-popover button {
|
|
160
165
|
appearance: none;
|
|
@@ -171,12 +176,45 @@ const PALETTE_CSS = `
|
|
|
171
176
|
gap: 16px;
|
|
172
177
|
width: 100%;
|
|
173
178
|
}
|
|
174
|
-
.dc-tp-popover button:hover { background:
|
|
179
|
+
.dc-tp-popover button:hover { background: color-mix(in oklab, var(--maude-chrome-fg-0, #1c1917) 5%, transparent); }
|
|
175
180
|
.dc-tp-popover .dc-tp-kbd {
|
|
176
181
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
177
182
|
font-size: 11px;
|
|
178
183
|
opacity: 0.55;
|
|
179
184
|
}
|
|
185
|
+
/* Phase 24 — Shape tool button + its kind popover. The wrapper is the popover's
|
|
186
|
+
positioning context so it anchors directly above the button. */
|
|
187
|
+
.dc-tool-palette .dc-tp-shape {
|
|
188
|
+
position: relative;
|
|
189
|
+
display: inline-flex;
|
|
190
|
+
}
|
|
191
|
+
.dc-tool-palette .dc-tp-shape > button { position: relative; }
|
|
192
|
+
.dc-tool-palette .dc-tp-shape-caret {
|
|
193
|
+
position: absolute;
|
|
194
|
+
right: 2px;
|
|
195
|
+
bottom: 1px;
|
|
196
|
+
display: inline-flex;
|
|
197
|
+
opacity: 0.5;
|
|
198
|
+
pointer-events: none;
|
|
199
|
+
}
|
|
200
|
+
.dc-tp-shape-popover {
|
|
201
|
+
position: absolute;
|
|
202
|
+
bottom: calc(100% + 8px);
|
|
203
|
+
left: 50%;
|
|
204
|
+
transform: translateX(-50%);
|
|
205
|
+
display: flex;
|
|
206
|
+
gap: 2px;
|
|
207
|
+
padding: 4px;
|
|
208
|
+
background: var(--maude-chrome-bg-0, #ffffff);
|
|
209
|
+
border: 1px solid var(--maude-chrome-fg-0, #1c1917);
|
|
210
|
+
border-radius: 8px;
|
|
211
|
+
box-shadow: 0 6px 24px var(--maude-chrome-shadow, color-mix(in oklab, #1c1917 10%, transparent));
|
|
212
|
+
z-index: 8;
|
|
213
|
+
}
|
|
214
|
+
.dc-tp-shape-popover button[aria-checked="true"] {
|
|
215
|
+
background: color-mix(in oklab, var(--maude-hud-accent, #d63b1f) 14%, transparent);
|
|
216
|
+
color: var(--maude-hud-accent, #d63b1f);
|
|
217
|
+
}
|
|
180
218
|
`.trim();
|
|
181
219
|
|
|
182
220
|
function ensurePaletteStyles(): void {
|
|
@@ -189,29 +227,59 @@ function ensurePaletteStyles(): void {
|
|
|
189
227
|
}
|
|
190
228
|
|
|
191
229
|
const NAV_TOOLS = ['move', 'hand', 'comment'] as const;
|
|
192
|
-
|
|
230
|
+
// Phase 24 — the two rect/ellipse buttons collapse into one Shape tool (with a
|
|
231
|
+
// kind popover); sticky/arrow/text/eraser keep their order.
|
|
232
|
+
const DRAW_TOOLS = ['pen', 'shape', 'sticky', 'arrow', 'text', 'eraser'] as const;
|
|
233
|
+
|
|
234
|
+
// Phase 24 — the Shape tool's primitive picker (popover order matches FigJam).
|
|
235
|
+
const SHAPE_KINDS: ReadonlyArray<{ kind: ShapeKind; label: string }> = [
|
|
236
|
+
{ kind: 'square', label: 'Square' },
|
|
237
|
+
{ kind: 'rounded', label: 'Rounded square' },
|
|
238
|
+
{ kind: 'circle', label: 'Circle' },
|
|
239
|
+
{ kind: 'diamond', label: 'Diamond' },
|
|
240
|
+
{ kind: 'triangle', label: 'Triangle' },
|
|
241
|
+
{ kind: 'triangle-down', label: 'Triangle down' },
|
|
242
|
+
];
|
|
193
243
|
|
|
194
244
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
195
245
|
// Component
|
|
196
246
|
|
|
197
247
|
export function ToolPalette() {
|
|
198
248
|
ensurePaletteStyles();
|
|
199
|
-
const { tool, setTool, tools, sticky, toggleSticky } = useToolMode();
|
|
249
|
+
const { tool, setTool, tools, sticky, toggleSticky, shapeKind, setShapeKind } = useToolMode();
|
|
200
250
|
const controller = useViewportControllerContext();
|
|
201
251
|
const visibilityCtx = useAnnotationsVisibility();
|
|
202
252
|
const [mounted, setMounted] = useState(false);
|
|
203
253
|
const [zoomOpen, setZoomOpen] = useState(false);
|
|
254
|
+
const [shapeOpen, setShapeOpen] = useState(false);
|
|
204
255
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
205
256
|
|
|
206
257
|
useEffect(() => setMounted(true), []);
|
|
207
258
|
useEffect(() => {
|
|
208
|
-
if (!zoomOpen) return;
|
|
259
|
+
if (!zoomOpen && !shapeOpen) return;
|
|
209
260
|
const onDown = (e: PointerEvent) => {
|
|
210
|
-
if (!containerRef.current?.contains(e.target as Node))
|
|
261
|
+
if (!containerRef.current?.contains(e.target as Node)) {
|
|
262
|
+
setZoomOpen(false);
|
|
263
|
+
setShapeOpen(false);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
const onKey = (e: KeyboardEvent) => {
|
|
267
|
+
if (e.key === 'Escape') {
|
|
268
|
+
setZoomOpen(false);
|
|
269
|
+
setShapeOpen(false);
|
|
270
|
+
}
|
|
211
271
|
};
|
|
212
272
|
document.addEventListener('pointerdown', onDown, true);
|
|
213
|
-
|
|
214
|
-
|
|
273
|
+
document.addEventListener('keydown', onKey, true);
|
|
274
|
+
return () => {
|
|
275
|
+
document.removeEventListener('pointerdown', onDown, true);
|
|
276
|
+
document.removeEventListener('keydown', onKey, true);
|
|
277
|
+
};
|
|
278
|
+
}, [zoomOpen, shapeOpen]);
|
|
279
|
+
// Close the shape popover whenever the tool changes away from Shape.
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
if (tool !== 'shape') setShapeOpen(false);
|
|
282
|
+
}, [tool]);
|
|
215
283
|
|
|
216
284
|
if (!mounted) return null;
|
|
217
285
|
|
|
@@ -246,6 +314,63 @@ export function ToolPalette() {
|
|
|
246
314
|
);
|
|
247
315
|
};
|
|
248
316
|
|
|
317
|
+
// Phase 24 — the single Shape tool. The button shows the CURRENT shape-kind
|
|
318
|
+
// glyph (so the toolbar reflects what you'll draw); first click arms the tool,
|
|
319
|
+
// clicking it again (or the caret) opens the 6-kind popover.
|
|
320
|
+
const renderShapeButton = () => {
|
|
321
|
+
const isShape = tool === 'shape';
|
|
322
|
+
const isSticky = sticky.locked && sticky.tool === 'shape';
|
|
323
|
+
const KindIcon = SHAPE_KIND_ICONS[shapeKind] ?? TOOL_ICONS.shape;
|
|
324
|
+
return (
|
|
325
|
+
<span key="shape" className="dc-tp-shape">
|
|
326
|
+
<button
|
|
327
|
+
type="button"
|
|
328
|
+
aria-label={`Shape (R) — ${shapeKind}, click again for shape types`}
|
|
329
|
+
aria-pressed={isShape}
|
|
330
|
+
aria-haspopup="menu"
|
|
331
|
+
aria-expanded={shapeOpen}
|
|
332
|
+
data-sticky={isSticky ? 'true' : undefined}
|
|
333
|
+
title="Shape (R) · double-click to lock"
|
|
334
|
+
onClick={() => {
|
|
335
|
+
if (!isShape) setTool('shape');
|
|
336
|
+
else setShapeOpen((o) => !o);
|
|
337
|
+
}}
|
|
338
|
+
onDoubleClick={() => toggleSticky('shape')}
|
|
339
|
+
>
|
|
340
|
+
{KindIcon ? <KindIcon /> : null}
|
|
341
|
+
<span className="dc-tp-shape-caret" aria-hidden="true">
|
|
342
|
+
<IconChevronDown size={9} />
|
|
343
|
+
</span>
|
|
344
|
+
{isSticky ? <span className="dc-tp-sticky-badge" aria-hidden="true" /> : null}
|
|
345
|
+
</button>
|
|
346
|
+
{shapeOpen ? (
|
|
347
|
+
<div className="dc-tp-shape-popover" role="menu" aria-label="Shape type">
|
|
348
|
+
{SHAPE_KINDS.map(({ kind, label }) => {
|
|
349
|
+
const Icon = SHAPE_KIND_ICONS[kind];
|
|
350
|
+
return (
|
|
351
|
+
<button
|
|
352
|
+
key={kind}
|
|
353
|
+
type="button"
|
|
354
|
+
role="menuitemradio"
|
|
355
|
+
aria-checked={shapeKind === kind}
|
|
356
|
+
aria-label={label}
|
|
357
|
+
title={label}
|
|
358
|
+
onClick={() => {
|
|
359
|
+
setShapeKind(kind);
|
|
360
|
+
setTool('shape');
|
|
361
|
+
setShapeOpen(false);
|
|
362
|
+
}}
|
|
363
|
+
>
|
|
364
|
+
{Icon ? <Icon /> : null}
|
|
365
|
+
</button>
|
|
366
|
+
);
|
|
367
|
+
})}
|
|
368
|
+
</div>
|
|
369
|
+
) : null}
|
|
370
|
+
</span>
|
|
371
|
+
);
|
|
372
|
+
};
|
|
373
|
+
|
|
249
374
|
return (
|
|
250
375
|
<div ref={containerRef} className="dc-tool-palette" role="toolbar" aria-label="Canvas tools">
|
|
251
376
|
<div className="dc-tp-group">
|
|
@@ -253,7 +378,13 @@ export function ToolPalette() {
|
|
|
253
378
|
</div>
|
|
254
379
|
<div className="dc-tp-sep" />
|
|
255
380
|
<div className="dc-tp-group">
|
|
256
|
-
{drawList.map((t) =>
|
|
381
|
+
{drawList.map((t) =>
|
|
382
|
+
t
|
|
383
|
+
? t.id === 'shape'
|
|
384
|
+
? renderShapeButton()
|
|
385
|
+
: renderToolButton(t.id, t.label, t.shortcut)
|
|
386
|
+
: null
|
|
387
|
+
)}
|
|
257
388
|
</div>
|
|
258
389
|
<div className="dc-tp-sep" />
|
|
259
390
|
<div className="dc-tp-group">
|
|
@@ -23,11 +23,11 @@ const HUD_CSS = `
|
|
|
23
23
|
z-index: 7;
|
|
24
24
|
pointer-events: none;
|
|
25
25
|
padding: 6px 10px;
|
|
26
|
-
background: var(--bg-2, rgba(20, 20, 20, 0.85));
|
|
27
|
-
color: var(--fg-1, rgba(255, 255, 255, 0.85));
|
|
28
|
-
border: 1px solid var(--border
|
|
26
|
+
background: var(--maude-chrome-bg-2, rgba(20, 20, 20, 0.85));
|
|
27
|
+
color: var(--maude-chrome-fg-1, rgba(255, 255, 255, 0.85));
|
|
28
|
+
border: 1px solid var(--maude-chrome-border, rgba(255, 255, 255, 0.08));
|
|
29
29
|
border-radius: 6px;
|
|
30
|
-
font: 11px var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
30
|
+
font: 11px var(--maude-chrome-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
31
31
|
letter-spacing: 0.02em;
|
|
32
32
|
user-select: none;
|
|
33
33
|
opacity: 0;
|