@1agh/maude 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/cli/bin/maude.mjs +5 -1
- package/cli/commands/design-link.test.mjs +207 -0
- package/cli/commands/design.mjs +42 -12
- package/cli/commands/doctor.mjs +361 -0
- package/cli/commands/doctor.test.mjs +185 -0
- package/cli/commands/help.mjs +24 -0
- package/cli/commands/hub.mjs +245 -0
- package/cli/commands/hub.test.mjs +87 -0
- package/cli/lib/config-lint.mjs +141 -0
- package/cli/lib/config-lint.test.mjs +117 -0
- package/cli/lib/design-link.mjs +216 -0
- package/cli/lib/hubs-config.mjs +123 -0
- package/cli/lib/hubs-config.test.mjs +100 -0
- package/cli/lib/preflight.mjs +232 -0
- package/cli/lib/stack-detect.mjs +344 -0
- package/cli/lib/stack-detect.test.mjs +121 -0
- package/package.json +16 -8
- package/plugins/design/dependencies.json +147 -0
- package/plugins/design/dependencies.schema.json +107 -0
- package/plugins/design/dev-server/ai-banner.tsx +188 -0
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +5 -5
- package/plugins/design/dev-server/annotations-layer.tsx +52 -12
- package/plugins/design/dev-server/api.ts +17 -1
- package/plugins/design/dev-server/artboard-marquee.tsx +2 -2
- package/plugins/design/dev-server/bin/preflight.sh +32 -0
- package/plugins/design/dev-server/bin/runtime-health.sh +169 -0
- package/plugins/design/dev-server/canvas-lib.tsx +33 -7
- package/plugins/design/dev-server/canvas-shell.tsx +127 -9
- package/plugins/design/dev-server/client/app.jsx +72 -0
- package/plugins/design/dev-server/collab/ai-activity.ts +127 -0
- package/plugins/design/dev-server/collab/git-lifecycle.ts +124 -0
- package/plugins/design/dev-server/collab/index.ts +47 -0
- package/plugins/design/dev-server/collab/persistence.ts +123 -0
- package/plugins/design/dev-server/collab/protocol.ts +108 -0
- package/plugins/design/dev-server/collab/registry.ts +110 -0
- package/plugins/design/dev-server/collab/room.ts +215 -0
- package/plugins/design/dev-server/comments-overlay.tsx +29 -0
- package/plugins/design/dev-server/contextual-toolbar.tsx +1 -1
- package/plugins/design/dev-server/cursors-overlay.tsx +296 -0
- package/plugins/design/dev-server/dist/client.bundle.js +75 -3
- package/plugins/design/dev-server/dist/runtime/lib0_decoding.js +624 -0
- package/plugins/design/dev-server/dist/runtime/lib0_encoding.js +608 -0
- package/plugins/design/dev-server/dist/runtime/y-protocols_awareness.js +380 -0
- package/plugins/design/dev-server/dist/runtime/y-protocols_sync.js +104 -0
- package/plugins/design/dev-server/dist/runtime/yjs.js +6691 -0
- package/plugins/design/dev-server/export-dialog.tsx +1 -1
- package/plugins/design/dev-server/hmr-broadcast.ts +15 -2
- package/plugins/design/dev-server/http.ts +64 -1
- package/plugins/design/dev-server/marquee-overlay.tsx +2 -2
- package/plugins/design/dev-server/participants-chrome.tsx +261 -0
- package/plugins/design/dev-server/runtime-bundle.ts +19 -0
- package/plugins/design/dev-server/server.ts +78 -11
- package/plugins/design/dev-server/test/ai-activity.test.ts +113 -0
- package/plugins/design/dev-server/test/collab-annotations-bridge.test.ts +55 -0
- package/plugins/design/dev-server/test/collab-bridge.test.ts +81 -0
- package/plugins/design/dev-server/test/collab-loopback.test.ts +63 -0
- package/plugins/design/dev-server/test/collab-protocol.test.ts +146 -0
- package/plugins/design/dev-server/test/collab-room.test.ts +182 -0
- package/plugins/design/dev-server/test/collab-stress.test.ts +121 -0
- package/plugins/design/dev-server/test/git-lifecycle.test.ts +101 -0
- package/plugins/design/dev-server/test/participants-chrome.test.ts +30 -0
- package/plugins/design/dev-server/test/use-collab.test.ts +71 -0
- package/plugins/design/dev-server/tool-palette.tsx +7 -7
- package/plugins/design/dev-server/use-annotation-resize.tsx +1 -1
- package/plugins/design/dev-server/use-collab.tsx +478 -0
- package/plugins/design/dev-server/ws.ts +123 -7
- package/plugins/design/templates/_shell.html +37 -1
- package/plugins/flow/.claude-plugin/config.schema.json +12 -0
- package/plugins/flow/dependencies.json +143 -0
- package/plugins/flow/dependencies.schema.json +107 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Phase 8 Task 8 — multi-tab stress harness.
|
|
2
|
+
//
|
|
3
|
+
// Two in-memory peers attached to one Room, broadcasting cursor-shaped
|
|
4
|
+
// Awareness updates at ~30 Hz for `STRESS_MS` (default 10 s in CI; the plan
|
|
5
|
+
// spec calls for 2 min but a tighter CI default keeps the run under 15 s).
|
|
6
|
+
// Measures:
|
|
7
|
+
//
|
|
8
|
+
// - RSS growth via process.memoryUsage().rss (before vs after)
|
|
9
|
+
// - Y.Doc state size growth via Y.encodeStateAsUpdate(doc).byteLength
|
|
10
|
+
// - Awareness state cleanup — when both peers disconnect, no entries leak
|
|
11
|
+
//
|
|
12
|
+
// Pass thresholds (CI-safe):
|
|
13
|
+
// - RSS delta < 20 MB (plan spec; conservative because GC is non-deterministic)
|
|
14
|
+
// - Y.Doc growth < 100 KB (the spec ceiling is 500 KB for a 2-min run; with
|
|
15
|
+
// awareness ephemeral + no doc.update traffic, growth should be ~0).
|
|
16
|
+
|
|
17
|
+
import { afterEach, describe, expect, test } from 'bun:test';
|
|
18
|
+
|
|
19
|
+
import { Awareness } from 'y-protocols/awareness';
|
|
20
|
+
import * as Y from 'yjs';
|
|
21
|
+
|
|
22
|
+
import { type RoomConn, createRoom } from '../collab/room.ts';
|
|
23
|
+
|
|
24
|
+
const STRESS_MS = Number(process.env.MAUDE_STRESS_MS ?? 10_000);
|
|
25
|
+
const RSS_GROWTH_LIMIT_MB = 20;
|
|
26
|
+
const YDOC_GROWTH_LIMIT_BYTES = 100_000;
|
|
27
|
+
|
|
28
|
+
function makeConn(id: string): RoomConn {
|
|
29
|
+
return {
|
|
30
|
+
id,
|
|
31
|
+
send() {
|
|
32
|
+
/* drain — we don't care about the network bytes; the memory pressure
|
|
33
|
+
is on the Awareness map + Y.Doc internals, not the WS buffer */
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('multi-tab stress (Phase 8 Task 8)', () => {
|
|
39
|
+
let cleanupRoom: (() => Promise<void>) | null = null;
|
|
40
|
+
|
|
41
|
+
afterEach(async () => {
|
|
42
|
+
if (cleanupRoom) {
|
|
43
|
+
await cleanupRoom();
|
|
44
|
+
cleanupRoom = null;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test(
|
|
49
|
+
'30 Hz × STRESS_MS × 2 peers — bounded RSS + Y.Doc growth',
|
|
50
|
+
async () => {
|
|
51
|
+
const room = createRoom('stress-slug', {
|
|
52
|
+
async seed() {},
|
|
53
|
+
async persistJson() {},
|
|
54
|
+
async persistBinary() {},
|
|
55
|
+
});
|
|
56
|
+
cleanupRoom = () => room.destroy();
|
|
57
|
+
|
|
58
|
+
const A = makeConn('peer-a');
|
|
59
|
+
const B = makeConn('peer-b');
|
|
60
|
+
await room.connect(A);
|
|
61
|
+
await room.connect(B);
|
|
62
|
+
|
|
63
|
+
// Each peer has its own local Awareness instance to model what real
|
|
64
|
+
// clients do — they don't share a memory address space with the server's
|
|
65
|
+
// awareness. We pump updates by setting state on the server-side
|
|
66
|
+
// awareness directly (the server's room.awareness is what the real WS
|
|
67
|
+
// peers' setLocalState() would land in via applyAwarenessUpdate).
|
|
68
|
+
const awA = new Awareness(new Y.Doc());
|
|
69
|
+
const awB = new Awareness(new Y.Doc());
|
|
70
|
+
awA.setLocalState({ name: 'Alice', color: '#f00', cursor: { x: 0, y: 0 }, __connId: A.id });
|
|
71
|
+
awB.setLocalState({ name: 'Bob', color: '#0f0', cursor: { x: 0, y: 0 }, __connId: B.id });
|
|
72
|
+
|
|
73
|
+
const rssBefore = process.memoryUsage().rss;
|
|
74
|
+
const docSizeBefore = Y.encodeStateAsUpdate(room.doc).byteLength;
|
|
75
|
+
|
|
76
|
+
const start = Date.now();
|
|
77
|
+
const tickInterval = 33; // ~30 Hz
|
|
78
|
+
let aX = 0;
|
|
79
|
+
let bX = 0;
|
|
80
|
+
let updates = 0;
|
|
81
|
+
while (Date.now() - start < STRESS_MS) {
|
|
82
|
+
aX = (aX + 1) % 1000;
|
|
83
|
+
bX = (bX + 7) % 1000;
|
|
84
|
+
awA.setLocalState({
|
|
85
|
+
name: 'Alice',
|
|
86
|
+
color: '#f00',
|
|
87
|
+
cursor: { x: aX, y: aX },
|
|
88
|
+
__connId: A.id,
|
|
89
|
+
});
|
|
90
|
+
awB.setLocalState({
|
|
91
|
+
name: 'Bob',
|
|
92
|
+
color: '#0f0',
|
|
93
|
+
cursor: { x: bX, y: bX },
|
|
94
|
+
__connId: B.id,
|
|
95
|
+
});
|
|
96
|
+
updates += 2;
|
|
97
|
+
await Bun.sleep(tickInterval);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const rssAfter = process.memoryUsage().rss;
|
|
101
|
+
const docSizeAfter = Y.encodeStateAsUpdate(room.doc).byteLength;
|
|
102
|
+
|
|
103
|
+
const rssGrowthMb = (rssAfter - rssBefore) / (1024 * 1024);
|
|
104
|
+
const docGrowth = docSizeAfter - docSizeBefore;
|
|
105
|
+
|
|
106
|
+
console.log(
|
|
107
|
+
`[stress] ${updates} awareness updates over ${STRESS_MS} ms → ` +
|
|
108
|
+
`RSS Δ=${rssGrowthMb.toFixed(1)} MB, Y.Doc Δ=${docGrowth} bytes`
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(rssGrowthMb).toBeLessThan(RSS_GROWTH_LIMIT_MB);
|
|
112
|
+
expect(docGrowth).toBeLessThan(YDOC_GROWTH_LIMIT_BYTES);
|
|
113
|
+
|
|
114
|
+
// Disconnect both peers; size should go to 0.
|
|
115
|
+
room.disconnect(A);
|
|
116
|
+
room.disconnect(B);
|
|
117
|
+
expect(room.size()).toBe(0);
|
|
118
|
+
},
|
|
119
|
+
STRESS_MS + 5_000
|
|
120
|
+
);
|
|
121
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Unit: Task 7 — `.git/HEAD` watcher. We don't exercise fs.watch directly
|
|
2
|
+
// (flaky across platforms in CI); instead we drive the module's public
|
|
3
|
+
// surface: it gracefully no-ops when no .git/ exists, and the public stop()
|
|
4
|
+
// is idempotent.
|
|
5
|
+
//
|
|
6
|
+
// The flush-before-broadcast invariant is verified separately by the
|
|
7
|
+
// collab-bridge tests and the persistence module's contract — registry
|
|
8
|
+
// already exposes flushAll(). The integration with .git/HEAD writes is
|
|
9
|
+
// exercised manually in the Task 7 acceptance test (Phase 8 plan validate row).
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
16
|
+
|
|
17
|
+
import { createGitLifecycle } from '../collab/git-lifecycle.ts';
|
|
18
|
+
import { createRegistry } from '../collab/registry.ts';
|
|
19
|
+
import type { RoomCallbacks } from '../collab/room.ts';
|
|
20
|
+
import { type Context, createBus } from '../context.ts';
|
|
21
|
+
|
|
22
|
+
function ctxFor(repoRoot: string): Context {
|
|
23
|
+
return {
|
|
24
|
+
cfg: {} as Context['cfg'],
|
|
25
|
+
projectLabel: 'test',
|
|
26
|
+
paths: {
|
|
27
|
+
repoRoot,
|
|
28
|
+
designRel: '.design',
|
|
29
|
+
designRoot: path.join(repoRoot, '.design'),
|
|
30
|
+
serverInfoFile: '',
|
|
31
|
+
activeFile: '',
|
|
32
|
+
commentsDir: '',
|
|
33
|
+
canvasStateDir: '',
|
|
34
|
+
historyDir: '',
|
|
35
|
+
tokensUrlRel: '',
|
|
36
|
+
systemDirRel: '',
|
|
37
|
+
},
|
|
38
|
+
bus: createBus(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function noopCallbacks(): RoomCallbacks {
|
|
43
|
+
return { async seed() {}, async persistJson() {}, async persistBinary() {} };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('git-lifecycle', () => {
|
|
47
|
+
let tmp: string;
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
tmp = mkdtempSync(path.join(tmpdir(), 'maude-git-lifecycle-'));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
try {
|
|
55
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
56
|
+
} catch {
|
|
57
|
+
/* ignore */
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('no .git/ directory → silent no-op (still returns a stop function)', () => {
|
|
62
|
+
const ctx = ctxFor(tmp);
|
|
63
|
+
const registry = createRegistry(noopCallbacks());
|
|
64
|
+
expect(existsSync(path.join(tmp, '.git'))).toBe(false);
|
|
65
|
+
const gl = createGitLifecycle(ctx, registry);
|
|
66
|
+
expect(typeof gl.stop).toBe('function');
|
|
67
|
+
gl.stop(); // idempotent
|
|
68
|
+
gl.stop();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('with .git/HEAD present → watcher attaches without throwing', () => {
|
|
72
|
+
mkdirSync(path.join(tmp, '.git'), { recursive: true });
|
|
73
|
+
writeFileSync(path.join(tmp, '.git', 'HEAD'), 'ref: refs/heads/main\n');
|
|
74
|
+
const ctx = ctxFor(tmp);
|
|
75
|
+
const registry = createRegistry(noopCallbacks());
|
|
76
|
+
const gl = createGitLifecycle(ctx, registry);
|
|
77
|
+
expect(typeof gl.stop).toBe('function');
|
|
78
|
+
gl.stop();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('emits git-lifecycle bus event on HEAD change (manual rewrite)', async () => {
|
|
82
|
+
mkdirSync(path.join(tmp, '.git'), { recursive: true });
|
|
83
|
+
writeFileSync(path.join(tmp, '.git', 'HEAD'), 'ref: refs/heads/main\n');
|
|
84
|
+
const ctx = ctxFor(tmp);
|
|
85
|
+
const registry = createRegistry(noopCallbacks());
|
|
86
|
+
const events: unknown[] = [];
|
|
87
|
+
ctx.bus.on('git-lifecycle', (e) => events.push(e));
|
|
88
|
+
const gl = createGitLifecycle(ctx, registry);
|
|
89
|
+
|
|
90
|
+
// Rewrite HEAD; the watcher fires + 250 ms debounce + flush. Wait > 500 ms
|
|
91
|
+
// to be safe with fs.watch + flushAll latency on macOS + Linux.
|
|
92
|
+
writeFileSync(path.join(tmp, '.git', 'HEAD'), 'ref: refs/heads/other\n');
|
|
93
|
+
await Bun.sleep(700);
|
|
94
|
+
|
|
95
|
+
expect(events.length).toBeGreaterThanOrEqual(1);
|
|
96
|
+
const evt = events[0] as { reason?: string; head?: string };
|
|
97
|
+
expect(evt.reason).toBe('head-changed');
|
|
98
|
+
expect(evt.head).toContain('refs/heads/other');
|
|
99
|
+
gl.stop();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Unit: pure helpers from participants-chrome.tsx.
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from 'bun:test';
|
|
4
|
+
|
|
5
|
+
import { initialsFor } from '../participants-chrome.tsx';
|
|
6
|
+
|
|
7
|
+
describe('initialsFor', () => {
|
|
8
|
+
test('two-word name → uppercase first letters', () => {
|
|
9
|
+
expect(initialsFor('Alice Smith')).toBe('AS');
|
|
10
|
+
expect(initialsFor('michał dovrtěl')).toBe('MD');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('single-word name → first two letters uppercased', () => {
|
|
14
|
+
expect(initialsFor('alice')).toBe('AL');
|
|
15
|
+
expect(initialsFor('A')).toBe('A');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('many-word name → first letter of first two words', () => {
|
|
19
|
+
expect(initialsFor('John Ronald Reuel Tolkien')).toBe('JR');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('empty / whitespace → ?', () => {
|
|
23
|
+
expect(initialsFor('')).toBe('?');
|
|
24
|
+
expect(initialsFor(' ')).toBe('?');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('handles extra whitespace', () => {
|
|
28
|
+
expect(initialsFor(' Alice Smith ')).toBe('AS');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Unit: pure helpers from use-collab.tsx (color hash + slug derivation).
|
|
2
|
+
// The provider itself + the WS round-trip live in browser-shaped harnesses.
|
|
3
|
+
|
|
4
|
+
import { describe, expect, test } from 'bun:test';
|
|
5
|
+
|
|
6
|
+
import { canvasSlugFromPath, colorForName } from '../use-collab.tsx';
|
|
7
|
+
|
|
8
|
+
describe('colorForName', () => {
|
|
9
|
+
test('returns a color from the curated palette', () => {
|
|
10
|
+
const color = colorForName('Alice');
|
|
11
|
+
expect(color).toMatch(/^#[0-9a-f]{6}$/i);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('deterministic per input', () => {
|
|
15
|
+
const a = colorForName('Alice');
|
|
16
|
+
const b = colorForName('Alice');
|
|
17
|
+
expect(a).toBe(b);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('different names land on different colors (probabilistically)', () => {
|
|
21
|
+
// Sample many distinct names; expect at least 8 distinct colors among 30
|
|
22
|
+
// names. With 12-color palette + uniform-ish djb2, this is comfortable.
|
|
23
|
+
const names = Array.from({ length: 30 }, (_, i) => `peer-${i}`);
|
|
24
|
+
const colors = new Set(names.map(colorForName));
|
|
25
|
+
expect(colors.size).toBeGreaterThanOrEqual(8);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('empty string returns a palette color (not crash)', () => {
|
|
29
|
+
expect(colorForName('')).toMatch(/^#[0-9a-f]{6}$/i);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('unicode names work', () => {
|
|
33
|
+
expect(colorForName('Michał Dovrtěl')).toMatch(/^#[0-9a-f]{6}$/i);
|
|
34
|
+
expect(colorForName('佐藤')).toMatch(/^#[0-9a-f]{6}$/i);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('canvasSlugFromPath', () => {
|
|
39
|
+
test('normalizes a typical canvas path', () => {
|
|
40
|
+
expect(canvasSlugFromPath('ui/Foo.tsx')).toBe('ui-foo');
|
|
41
|
+
expect(canvasSlugFromPath('ui/Canvas Viewport.tsx')).toBe('ui-canvas_viewport');
|
|
42
|
+
expect(canvasSlugFromPath('system/project/preview/colors-accent.tsx')).toBe(
|
|
43
|
+
'system-project-preview-colors-accent'
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('strips .tsx + .html extensions case-insensitively', () => {
|
|
48
|
+
expect(canvasSlugFromPath('ui/Foo.TSX')).toBe('ui-foo');
|
|
49
|
+
expect(canvasSlugFromPath('ui/Foo.html')).toBe('ui-foo');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('returns null for paths that would yield invalid slug chars', () => {
|
|
53
|
+
// Dots in name (not extension) survive normalization and fail the gate.
|
|
54
|
+
expect(canvasSlugFromPath('ui/Foo.Bar.tsx')).toBeNull();
|
|
55
|
+
// Empty input.
|
|
56
|
+
expect(canvasSlugFromPath('')).toBeNull();
|
|
57
|
+
expect(canvasSlugFromPath(null)).toBeNull();
|
|
58
|
+
expect(canvasSlugFromPath(undefined)).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('round-trips against the slug grammar parseCollabSlug accepts', () => {
|
|
62
|
+
// parseCollabSlug regex = ^[a-z0-9_-]+$
|
|
63
|
+
const re = /^[a-z0-9_-]+$/;
|
|
64
|
+
const samples = ['ui/Foo.tsx', 'system/project/preview/x.tsx', 'a/b c.tsx'];
|
|
65
|
+
for (const s of samples) {
|
|
66
|
+
const slug = canvasSlugFromPath(s);
|
|
67
|
+
expect(slug).not.toBeNull();
|
|
68
|
+
if (slug !== null) expect(re.test(slug)).toBe(true);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -76,19 +76,19 @@ const PALETTE_CSS = `
|
|
|
76
76
|
transition: background-color 80ms linear, color 80ms linear;
|
|
77
77
|
}
|
|
78
78
|
.dc-tool-palette button:hover {
|
|
79
|
-
background: color-mix(in oklab, var(--accent, #d63b1f) 8%, transparent);
|
|
79
|
+
background: color-mix(in oklab, var(--maude-hud-accent, #d63b1f) 8%, transparent);
|
|
80
80
|
color: var(--fg-0, #1a1a1a);
|
|
81
81
|
}
|
|
82
82
|
.dc-tool-palette button:focus-visible {
|
|
83
|
-
outline: 2px solid var(--accent, #d63b1f);
|
|
83
|
+
outline: 2px solid var(--maude-hud-accent, #d63b1f);
|
|
84
84
|
outline-offset: -2px;
|
|
85
85
|
}
|
|
86
86
|
/* DDR-046 — Active tool: tinted background + accent underbar + accent text.
|
|
87
87
|
The underbar is rendered via ::after so the visual stays inside the 6 px
|
|
88
88
|
radius without leaking past the button edge. */
|
|
89
89
|
.dc-tool-palette button[aria-pressed="true"] {
|
|
90
|
-
background: color-mix(in oklab, var(--accent, #d63b1f) 14%, transparent);
|
|
91
|
-
color: var(--accent, #d63b1f);
|
|
90
|
+
background: color-mix(in oklab, var(--maude-hud-accent, #d63b1f) 14%, transparent);
|
|
91
|
+
color: var(--maude-hud-accent, #d63b1f);
|
|
92
92
|
}
|
|
93
93
|
.dc-tool-palette button[aria-pressed="true"]::after {
|
|
94
94
|
content: "";
|
|
@@ -97,7 +97,7 @@ const PALETTE_CSS = `
|
|
|
97
97
|
right: 6px;
|
|
98
98
|
bottom: 2px;
|
|
99
99
|
height: 2px;
|
|
100
|
-
background: var(--accent, #d63b1f);
|
|
100
|
+
background: var(--maude-hud-accent, #d63b1f);
|
|
101
101
|
border-radius: 1px;
|
|
102
102
|
}
|
|
103
103
|
/* T19 — sticky-tool lock badge. Tiny accent square in the top-right corner
|
|
@@ -107,7 +107,7 @@ const PALETTE_CSS = `
|
|
|
107
107
|
.dc-tool-palette button[data-sticky="true"] {
|
|
108
108
|
/* Keep the tinted active background; add a hairline ring so the lock state
|
|
109
109
|
is readable even when the button is also aria-pressed. */
|
|
110
|
-
box-shadow: inset 0 0 0 1px var(--accent, #d63b1f);
|
|
110
|
+
box-shadow: inset 0 0 0 1px var(--maude-hud-accent, #d63b1f);
|
|
111
111
|
}
|
|
112
112
|
.dc-tool-palette button .dc-tp-sticky-badge {
|
|
113
113
|
position: absolute;
|
|
@@ -115,7 +115,7 @@ const PALETTE_CSS = `
|
|
|
115
115
|
right: 3px;
|
|
116
116
|
width: 6px;
|
|
117
117
|
height: 6px;
|
|
118
|
-
background: var(--accent, #d63b1f);
|
|
118
|
+
background: var(--maude-hud-accent, #d63b1f);
|
|
119
119
|
border-radius: 1px;
|
|
120
120
|
box-shadow: 0 0 0 1px var(--bg-0, #ffffff);
|
|
121
121
|
opacity: 0;
|
|
@@ -34,7 +34,7 @@ const RESIZE_CSS = `
|
|
|
34
34
|
position: fixed;
|
|
35
35
|
width: 8px;
|
|
36
36
|
height: 8px;
|
|
37
|
-
background: var(--accent, #d63b1f);
|
|
37
|
+
background: var(--maude-hud-accent, #d63b1f);
|
|
38
38
|
border: 1px solid var(--bg-0, #ffffff);
|
|
39
39
|
border-radius: 1px;
|
|
40
40
|
box-shadow: 0 0 0 0.5px color-mix(in oklab, var(--fg-0, #1c1917) 30%, transparent);
|