Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions apps/desktop/src/main/__tests__/spacing-4pt-ratchet-contract.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
/**
* P-4PT spacing ratchet (design-refinement-roadmap-2026-07 §1.4, owner
* decision D1: converge on a 4pt spacing grid, migrating incrementally).
*
* Every padding/gap/margin px value in renderer CSS should be a multiple
* of 4 (0 allowed; 1px and 2px exempt as hairline/optical nudges). The
* legacy drift (442 values at baseline) is FROZEN per file below and may
* only go DOWN:
*
* - touching a file and reducing its count → update the baseline DOWN
* - adding a new off-grid value anywhere → this test fails
* - new CSS files must be born clean (no entry = zero tolerance)
*
* This is a ratchet, not an allowlist of specific lines, so refactors
* inside a file stay cheap while the global trend is monotonic.
*/

import { strict as assert } from 'node:assert';
import { readdir, readFile } from 'node:fs/promises';
import { join, relative, resolve } from 'node:path';
import { describe, it } from 'node:test';

const DESKTOP_ROOT = process.cwd().endsWith(join('apps', 'desktop'))
? process.cwd()
: resolve(process.cwd(), 'apps', 'desktop');
const RENDERER_ROOT = resolve(DESKTOP_ROOT, 'src', 'renderer');

/** Frozen per-file baseline (2026-07-03). Only decrease these numbers. */
const BASELINE: ReadonlyMap<string, number> = new Map([
['src/renderer/maka-tokens.css', 23],
['src/renderer/styles/chat-header.css', 26],
['src/renderer/styles/chat-message.css', 4],
['src/renderer/styles/composer.css', 12],
['src/renderer/styles/daily-review.css', 17],
['src/renderer/styles/health-center.css', 11],
['src/renderer/styles/module-pages.css', 76],
['src/renderer/styles/onboarding.css', 29],
['src/renderer/styles/permission-center.css', 28],
['src/renderer/styles/reasoning-panel.css', 3],
['src/renderer/styles/settings/bot.css', 23],
['src/renderer/styles/settings/connection.css', 9],
['src/renderer/styles/settings/form.css', 7],
['src/renderer/styles/settings/models.css', 31],
['src/renderer/styles/settings/nav-sidebar.css', 21],
['src/renderer/styles/settings/provider-editor.css', 20],
['src/renderer/styles/settings/theme-preview.css', 23],
['src/renderer/styles/sidebar.css', 36],
['src/renderer/styles/tool-output.css', 8],
['src/renderer/styles/tool-stream.css', 35],
]);

const DECL_RE =
/(?:^|;|\{)\s*(padding|gap|margin|row-gap|column-gap|padding-(?:top|right|bottom|left|inline|block)|margin-(?:top|right|bottom|left|inline|block))\s*:\s*([^;}]+)/gm;
const PX_RE = /(?<![\w.-])(\d+(?:\.\d+)?)px/g;

function stripComments(src: string): string {
return src.replace(/\/\*[\s\S]*?\*\//g, '');
}

function countOffGrid(css: string): number {
const stripped = stripComments(css);
let count = 0;
for (const decl of stripped.matchAll(DECL_RE)) {
for (const px of (decl[2] ?? '').matchAll(PX_RE)) {
const value = Number(px[1]);
if (value !== 0 && value % 4 !== 0 && value !== 1 && value !== 2) count += 1;
}
}
return count;
}

async function collectCssFiles(dir: string): Promise<string[]> {
const out: string[] = [];
for (const entry of await readdir(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) out.push(...(await collectCssFiles(full)));
else if (entry.name.endsWith('.css')) out.push(full);
}
return out;
}

describe('P-4PT spacing ratchet', () => {
it('padding/gap/margin px values stay on the 4pt grid (frozen legacy may only shrink)', async () => {
const files = await collectCssFiles(RENDERER_ROOT);
const failures: string[] = [];
for (const file of files.sort()) {
const rel = relative(DESKTOP_ROOT, file).split('\\').join('/');
const count = countOffGrid(await readFile(file, 'utf8'));
const allowed = BASELINE.get(rel) ?? 0;
if (count > allowed) {
failures.push(`${rel}: ${count} off-grid spacing values (baseline ${allowed})`);
}
}
assert.deepEqual(
failures,
[],
`off-grid spacing crept in — use 4/8/12/16/24/32 (1px/2px hairlines exempt), or shrink the file below its frozen baseline:\n${failures.join('\n')}`,
);
});

it('baseline entries stay honest (no stale higher-than-actual counts)', async () => {
// Guard against the ratchet rusting: if a file improves but the
// baseline is not updated, the slack could hide future regressions.
// Tolerate up to 3 slack per file before requiring a baseline update.
const stale: string[] = [];
for (const [rel, allowed] of BASELINE) {
const count = countOffGrid(await readFile(resolve(DESKTOP_ROOT, rel), 'utf8'));
if (allowed - count > 3) {
stale.push(`${rel}: baseline ${allowed} but actual ${count} — lower the baseline`);
}
}
assert.deepEqual(stale, [], stale.join('\n'));
});
});
9 changes: 5 additions & 4 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,8 +126,11 @@
--foreground-5: color-mix(in oklch, var(--foreground) 5%, var(--background));
--foreground-8: color-mix(in oklch, var(--foreground) 8%, var(--background));
--foreground-10: color-mix(in oklch, var(--foreground) 10%, var(--background));
--foreground-20: color-mix(in oklch, var(--foreground) 20%, var(--background));
--foreground-30: color-mix(in oklch, var(--foreground) 30%, var(--background));
/* P-TEXT (roadmap §1.6): the -20/-30 text tiers were orphans (8 call
sites total) sitting between the surface washes (2..10) and the
real text ladder (40/50/60/70/80). Text uses moved to -40 (also a
contrast fix: 30% ink fails 4.5:1); decorative uses inlined their
color-mix. Fewer tiers = crisper hierarchy. */
--foreground-40: color-mix(in oklch, var(--foreground) 40%, var(--background));
--foreground-50: color-mix(in oklch, var(--foreground) 50%, var(--background));
--foreground-60: color-mix(in oklch, var(--foreground) 60%, var(--background));
Expand DownExpand Up@@ -681,8 +684,6 @@
--color-foreground-5: var(--foreground-5);
--color-foreground-8: var(--foreground-8);
--color-foreground-10: var(--foreground-10);
--color-foreground-20: var(--foreground-20);
--color-foreground-30: var(--foreground-30);
--color-foreground-40: var(--foreground-40);
--color-foreground-50: var(--foreground-50);
--color-foreground-60: var(--foreground-60);
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/onboarding.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -863,7 +863,7 @@
}
.maka-list-group-count {
font-variant-numeric: tabular-nums;
color: var(--foreground-30);
color: var(--foreground-40);
font-size: var(--font-size-caption);
font-weight: 500;
}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/settings/bot.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -114,7 +114,7 @@
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--foreground-30);
background: color-mix(in oklch, var(--foreground) 30%, var(--background));
box-shadow: 0 0 0 2px var(--background);
}
.settingsBotLogo[data-large="true"] .settingsBotLogoStatusDot {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/sidebar.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -165,7 +165,7 @@

.maka-resize-handle:hover::after,
.isResizingColumns .maka-resize-handle::after {
background: var(--foreground-20);
background: color-mix(in oklch, var(--foreground) 20%, var(--background));
}

/* Keyboard-focused separator: replace the suppressed native outline with a
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -376,7 +376,7 @@
.maka-composer-context-plus {
width: 26px;
height: 26px;
border: 1px solid var(--foreground-20);
border: 1px solid color-mix(in oklch, var(--foreground) 20%, var(--background));
border-radius: var(--radius-pill);
background: transparent;
}
Expand Down
8 changes: 6 additions & 2 deletions docs/design-refinement-roadmap-2026-07.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -112,8 +112,12 @@
4. **P-MOTION** ✅(已达标,无需动作):duration token
120/150/180/280ms 完全符合 Emil 表且取下限;装饰性入场动画已被
#406 gap 3 清除;剩余 keyframes 全部是功能性流式动画(D3 豁免)
5. **P-TEXT**:四档文字语义别名 + 新代码治理契约
6. **P-4PT**:4pt 治理契约(新增声明检查 + 存量 allowlist)
5. **P-TEXT** ✅(首轮):孤儿档 -20/-30 清除(8 处调用点:文字并入
-40 兼修对比度、装饰内联 color-mix);文字主力收敛为
40/50/60/70/80。全量四档语义别名迁移留待逐面触碰。
6. **P-4PT** ✅:ratchet 契约上线
(spacing-4pt-ratchet-contract.test.ts):442 个存量违例按文件冻结
只减不增,新文件零容忍,防 baseline 生锈的 slack 检查
7. **P-DARK**:dark elevation 独立化(依赖 P-SHADOW)
8. **P-STATE**:状态全周期补齐审计(skeleton 对形/empty 构图/inline error)

Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/primitives/chat.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -449,13 +449,13 @@ const toolVariants = cva("", {
// `.maka-tool-status-dot` (+ the `[data-status]` color swaps; running adds
// the box-shadow ring + `maka-tool-pulse` breath — keyframe stays in CSS).
dot:
"w-[8px] h-[8px] rounded-[var(--radius-pill)] bg-[var(--foreground-30)] [flex:0_0_auto]"
"w-[8px] h-[8px] rounded-[var(--radius-pill)] bg-[var(--foreground-40)] [flex:0_0_auto]"
// `waiting_permission` dot tint — see `WP_DOT_BG` above (String.raw).
+ " " + WP_DOT_BG
+ " data-[status=running]:bg-[var(--status-running)] data-[status=running]:[box-shadow:0_0_0_3px_oklch(from_var(--status-running)_l_c_h_/_0.15)] data-[status=running]:[animation:maka-tool-pulse_1.5s_ease-in-out_infinite]"
+ " data-[status=completed]:bg-[var(--success)]"
+ " data-[status=errored]:bg-[var(--destructive)]"
+ " data-[status=interrupted]:bg-[var(--foreground-30)]",
+ " data-[status=interrupted]:bg-[var(--foreground-40)]",
// `.maka-tool-name` — the mono tool name, ellipsized.
name:
"min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[color:var(--foreground)] font-medium [font-family:var(--font-mono)]",
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/stories/design-tokens.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ const foregroundScale = [
['foreground-2', '--foreground-2'],
['foreground-5', '--foreground-5'],
['foreground-10', '--foreground-10'],
['foreground-20', '--foreground-20'],
['foreground-50', '--foreground-50'],
['foreground-40', '--foreground-40'],
['foreground-60', '--foreground-60'],
['foreground-80', '--foreground-80'],
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions apps/desktop/src/main/__tests__/spacing-4pt-ratchet-contract.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
/**
* P-4PT spacing ratchet (design-refinement-roadmap-2026-07 §1.4, owner
* decision D1: converge on a 4pt spacing grid, migrating incrementally).
*
* Every padding/gap/margin px value in renderer CSS should be a multiple
* of 4 (0 allowed; 1px and 2px exempt as hairline/optical nudges). The
* legacy drift (442 values at baseline) is FROZEN per file below and may
* only go DOWN:
*
* - touching a file and reducing its count → update the baseline DOWN
* - adding a new off-grid value anywhere → this test fails
* - new CSS files must be born clean (no entry = zero tolerance)
*
* This is a ratchet, not an allowlist of specific lines, so refactors
* inside a file stay cheap while the global trend is monotonic.
*/

import { strict as assert } from 'node:assert';
import { readdir, readFile } from 'node:fs/promises';
import { join, relative, resolve } from 'node:path';
import { describe, it } from 'node:test';

const DESKTOP_ROOT = process.cwd().endsWith(join('apps', 'desktop'))
? process.cwd()
: resolve(process.cwd(), 'apps', 'desktop');
const RENDERER_ROOT = resolve(DESKTOP_ROOT, 'src', 'renderer');

/** Frozen per-file baseline (2026-07-03). Only decrease these numbers. */
const BASELINE: ReadonlyMap<string, number> = new Map([
['src/renderer/maka-tokens.css', 23],
['src/renderer/styles/chat-header.css', 26],
['src/renderer/styles/chat-message.css', 4],
['src/renderer/styles/composer.css', 12],
['src/renderer/styles/daily-review.css', 17],
['src/renderer/styles/health-center.css', 11],
['src/renderer/styles/module-pages.css', 76],
['src/renderer/styles/onboarding.css', 29],
['src/renderer/styles/permission-center.css', 28],
['src/renderer/styles/reasoning-panel.css', 3],
['src/renderer/styles/settings/bot.css', 23],
['src/renderer/styles/settings/connection.css', 9],
['src/renderer/styles/settings/form.css', 7],
['src/renderer/styles/settings/models.css', 31],
['src/renderer/styles/settings/nav-sidebar.css', 21],
['src/renderer/styles/settings/provider-editor.css', 20],
['src/renderer/styles/settings/theme-preview.css', 23],
['src/renderer/styles/sidebar.css', 36],
['src/renderer/styles/tool-output.css', 8],
['src/renderer/styles/tool-stream.css', 35],
]);

const DECL_RE =
/(?:^|;|\{)\s*(padding|gap|margin|row-gap|column-gap|padding-(?:top|right|bottom|left|inline|block)|margin-(?:top|right|bottom|left|inline|block))\s*:\s*([^;}]+)/gm;
const PX_RE = /(?<![\w.-])(\d+(?:\.\d+)?)px/g;

function stripComments(src: string): string {
return src.replace(/\/\*[\s\S]*?\*\//g, '');
}

function countOffGrid(css: string): number {
const stripped = stripComments(css);
let count = 0;
for (const decl of stripped.matchAll(DECL_RE)) {
for (const px of (decl[2] ?? '').matchAll(PX_RE)) {
const value = Number(px[1]);
if (value !== 0 && value % 4 !== 0 && value !== 1 && value !== 2) count += 1;
}
}
return count;
}

async function collectCssFiles(dir: string): Promise<string[]> {
const out: string[] = [];
for (const entry of await readdir(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) out.push(...(await collectCssFiles(full)));
else if (entry.name.endsWith('.css')) out.push(full);
}
return out;
}

describe('P-4PT spacing ratchet', () => {
it('padding/gap/margin px values stay on the 4pt grid (frozen legacy may only shrink)', async () => {
const files = await collectCssFiles(RENDERER_ROOT);
const failures: string[] = [];
for (const file of files.sort()) {
const rel = relative(DESKTOP_ROOT, file).split('\\').join('/');
const count = countOffGrid(await readFile(file, 'utf8'));
const allowed = BASELINE.get(rel) ?? 0;
if (count > allowed) {
failures.push(`${rel}: ${count} off-grid spacing values (baseline ${allowed})`);
}
}
assert.deepEqual(
failures,
[],
`off-grid spacing crept in — use 4/8/12/16/24/32 (1px/2px hairlines exempt), or shrink the file below its frozen baseline:\n${failures.join('\n')}`,
);
});

it('baseline entries stay honest (no stale higher-than-actual counts)', async () => {
// Guard against the ratchet rusting: if a file improves but the
// baseline is not updated, the slack could hide future regressions.
// Tolerate up to 3 slack per file before requiring a baseline update.
const stale: string[] = [];
for (const [rel, allowed] of BASELINE) {
const count = countOffGrid(await readFile(resolve(DESKTOP_ROOT, rel), 'utf8'));
if (allowed - count > 3) {
stale.push(`${rel}: baseline ${allowed} but actual ${count} — lower the baseline`);
}
}
assert.deepEqual(stale, [], stale.join('\n'));
});
});
9 changes: 5 additions & 4 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,8 +126,11 @@
--foreground-5: color-mix(in oklch, var(--foreground) 5%, var(--background));
--foreground-8: color-mix(in oklch, var(--foreground) 8%, var(--background));
--foreground-10: color-mix(in oklch, var(--foreground) 10%, var(--background));
--foreground-20: color-mix(in oklch, var(--foreground) 20%, var(--background));
--foreground-30: color-mix(in oklch, var(--foreground) 30%, var(--background));
/* P-TEXT (roadmap §1.6): the -20/-30 text tiers were orphans (8 call
sites total) sitting between the surface washes (2..10) and the
real text ladder (40/50/60/70/80). Text uses moved to -40 (also a
contrast fix: 30% ink fails 4.5:1); decorative uses inlined their
color-mix. Fewer tiers = crisper hierarchy. */
--foreground-40: color-mix(in oklch, var(--foreground) 40%, var(--background));
--foreground-50: color-mix(in oklch, var(--foreground) 50%, var(--background));
--foreground-60: color-mix(in oklch, var(--foreground) 60%, var(--background));
Expand DownExpand Up@@ -681,8 +684,6 @@
--color-foreground-5: var(--foreground-5);
--color-foreground-8: var(--foreground-8);
--color-foreground-10: var(--foreground-10);
--color-foreground-20: var(--foreground-20);
--color-foreground-30: var(--foreground-30);
--color-foreground-40: var(--foreground-40);
--color-foreground-50: var(--foreground-50);
--color-foreground-60: var(--foreground-60);
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/onboarding.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -863,7 +863,7 @@
}
.maka-list-group-count {
font-variant-numeric: tabular-nums;
color: var(--foreground-30);
color: var(--foreground-40);
font-size: var(--font-size-caption);
font-weight: 500;
}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/settings/bot.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -114,7 +114,7 @@
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--foreground-30);
background: color-mix(in oklch, var(--foreground) 30%, var(--background));
box-shadow: 0 0 0 2px var(--background);
}
.settingsBotLogo[data-large="true"] .settingsBotLogoStatusDot {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/sidebar.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -165,7 +165,7 @@

.maka-resize-handle:hover::after,
.isResizingColumns .maka-resize-handle::after {
background: var(--foreground-20);
background: color-mix(in oklch, var(--foreground) 20%, var(--background));
}

/* Keyboard-focused separator: replace the suppressed native outline with a
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -376,7 +376,7 @@
.maka-composer-context-plus {
width: 26px;
height: 26px;
border: 1px solid var(--foreground-20);
border: 1px solid color-mix(in oklch, var(--foreground) 20%, var(--background));
border-radius: var(--radius-pill);
background: transparent;
}
Expand Down
8 changes: 6 additions & 2 deletions docs/design-refinement-roadmap-2026-07.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -112,8 +112,12 @@
4. **P-MOTION** ✅(已达标,无需动作):duration token
120/150/180/280ms 完全符合 Emil 表且取下限;装饰性入场动画已被
#406 gap 3 清除;剩余 keyframes 全部是功能性流式动画(D3 豁免)
5. **P-TEXT**:四档文字语义别名 + 新代码治理契约
6. **P-4PT**:4pt 治理契约(新增声明检查 + 存量 allowlist)
5. **P-TEXT** ✅(首轮):孤儿档 -20/-30 清除(8 处调用点:文字并入
-40 兼修对比度、装饰内联 color-mix);文字主力收敛为
40/50/60/70/80。全量四档语义别名迁移留待逐面触碰。
6. **P-4PT** ✅:ratchet 契约上线
(spacing-4pt-ratchet-contract.test.ts):442 个存量违例按文件冻结
只减不增,新文件零容忍,防 baseline 生锈的 slack 检查
7. **P-DARK**:dark elevation 独立化(依赖 P-SHADOW)
8. **P-STATE**:状态全周期补齐审计(skeleton 对形/empty 构图/inline error)

Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/primitives/chat.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -449,13 +449,13 @@ const toolVariants = cva("", {
// `.maka-tool-status-dot` (+ the `[data-status]` color swaps; running adds
// the box-shadow ring + `maka-tool-pulse` breath — keyframe stays in CSS).
dot:
"w-[8px] h-[8px] rounded-[var(--radius-pill)] bg-[var(--foreground-30)] [flex:0_0_auto]"
"w-[8px] h-[8px] rounded-[var(--radius-pill)] bg-[var(--foreground-40)] [flex:0_0_auto]"
// `waiting_permission` dot tint — see `WP_DOT_BG` above (String.raw).
+ " " + WP_DOT_BG
+ " data-[status=running]:bg-[var(--status-running)] data-[status=running]:[box-shadow:0_0_0_3px_oklch(from_var(--status-running)_l_c_h_/_0.15)] data-[status=running]:[animation:maka-tool-pulse_1.5s_ease-in-out_infinite]"
+ " data-[status=completed]:bg-[var(--success)]"
+ " data-[status=errored]:bg-[var(--destructive)]"
+ " data-[status=interrupted]:bg-[var(--foreground-30)]",
+ " data-[status=interrupted]:bg-[var(--foreground-40)]",
// `.maka-tool-name` — the mono tool name, ellipsized.
name:
"min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[color:var(--foreground)] font-medium [font-family:var(--font-mono)]",
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/stories/design-tokens.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ const foregroundScale = [
['foreground-2', '--foreground-2'],
['foreground-5', '--foreground-5'],
['foreground-10', '--foreground-10'],
['foreground-20', '--foreground-20'],
['foreground-50', '--foreground-50'],
['foreground-40', '--foreground-40'],
['foreground-60', '--foreground-60'],
['foreground-80', '--foreground-80'],
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions apps/desktop/src/main/__tests__/spacing-4pt-ratchet-contract.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
/**
* P-4PT spacing ratchet (design-refinement-roadmap-2026-07 §1.4, owner
* decision D1: converge on a 4pt spacing grid, migrating incrementally).
*
* Every padding/gap/margin px value in renderer CSS should be a multiple
* of 4 (0 allowed; 1px and 2px exempt as hairline/optical nudges). The
* legacy drift (442 values at baseline) is FROZEN per file below and may
* only go DOWN:
*
* - touching a file and reducing its count → update the baseline DOWN
* - adding a new off-grid value anywhere → this test fails
* - new CSS files must be born clean (no entry = zero tolerance)
*
* This is a ratchet, not an allowlist of specific lines, so refactors
* inside a file stay cheap while the global trend is monotonic.
*/

import { strict as assert } from 'node:assert';
import { readdir, readFile } from 'node:fs/promises';
import { join, relative, resolve } from 'node:path';
import { describe, it } from 'node:test';

const DESKTOP_ROOT = process.cwd().endsWith(join('apps', 'desktop'))
? process.cwd()
: resolve(process.cwd(), 'apps', 'desktop');
const RENDERER_ROOT = resolve(DESKTOP_ROOT, 'src', 'renderer');

/** Frozen per-file baseline (2026-07-03). Only decrease these numbers. */
const BASELINE: ReadonlyMap<string, number> = new Map([
['src/renderer/maka-tokens.css', 23],
['src/renderer/styles/chat-header.css', 26],
['src/renderer/styles/chat-message.css', 4],
['src/renderer/styles/composer.css', 12],
['src/renderer/styles/daily-review.css', 17],
['src/renderer/styles/health-center.css', 11],
['src/renderer/styles/module-pages.css', 76],
['src/renderer/styles/onboarding.css', 29],
['src/renderer/styles/permission-center.css', 28],
['src/renderer/styles/reasoning-panel.css', 3],
['src/renderer/styles/settings/bot.css', 23],
['src/renderer/styles/settings/connection.css', 9],
['src/renderer/styles/settings/form.css', 7],
['src/renderer/styles/settings/models.css', 31],
['src/renderer/styles/settings/nav-sidebar.css', 21],
['src/renderer/styles/settings/provider-editor.css', 20],
['src/renderer/styles/settings/theme-preview.css', 23],
['src/renderer/styles/sidebar.css', 36],
['src/renderer/styles/tool-output.css', 8],
['src/renderer/styles/tool-stream.css', 35],
]);

const DECL_RE =
/(?:^|;|\{)\s*(padding|gap|margin|row-gap|column-gap|padding-(?:top|right|bottom|left|inline|block)|margin-(?:top|right|bottom|left|inline|block))\s*:\s*([^;}]+)/gm;
const PX_RE = /(?<![\w.-])(\d+(?:\.\d+)?)px/g;

function stripComments(src: string): string {
return src.replace(/\/\*[\s\S]*?\*\//g, '');
}

function countOffGrid(css: string): number {
const stripped = stripComments(css);
let count = 0;
for (const decl of stripped.matchAll(DECL_RE)) {
for (const px of (decl[2] ?? '').matchAll(PX_RE)) {
const value = Number(px[1]);
if (value !== 0 && value % 4 !== 0 && value !== 1 && value !== 2) count += 1;
}
}
return count;
}

async function collectCssFiles(dir: string): Promise<string[]> {
const out: string[] = [];
for (const entry of await readdir(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) out.push(...(await collectCssFiles(full)));
else if (entry.name.endsWith('.css')) out.push(full);
}
return out;
}

describe('P-4PT spacing ratchet', () => {
it('padding/gap/margin px values stay on the 4pt grid (frozen legacy may only shrink)', async () => {
const files = await collectCssFiles(RENDERER_ROOT);
const failures: string[] = [];
for (const file of files.sort()) {
const rel = relative(DESKTOP_ROOT, file).split('\\').join('/');
const count = countOffGrid(await readFile(file, 'utf8'));
const allowed = BASELINE.get(rel) ?? 0;
if (count > allowed) {
failures.push(`${rel}: ${count} off-grid spacing values (baseline ${allowed})`);
}
}
assert.deepEqual(
failures,
[],
`off-grid spacing crept in — use 4/8/12/16/24/32 (1px/2px hairlines exempt), or shrink the file below its frozen baseline:\n${failures.join('\n')}`,
);
});

it('baseline entries stay honest (no stale higher-than-actual counts)', async () => {
// Guard against the ratchet rusting: if a file improves but the
// baseline is not updated, the slack could hide future regressions.
// Tolerate up to 3 slack per file before requiring a baseline update.
const stale: string[] = [];
for (const [rel, allowed] of BASELINE) {
const count = countOffGrid(await readFile(resolve(DESKTOP_ROOT, rel), 'utf8'));
if (allowed - count > 3) {
stale.push(`${rel}: baseline ${allowed} but actual ${count} — lower the baseline`);
}
}
assert.deepEqual(stale, [], stale.join('\n'));
});
});
9 changes: 5 additions & 4 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,8 +126,11 @@
--foreground-5: color-mix(in oklch, var(--foreground) 5%, var(--background));
--foreground-8: color-mix(in oklch, var(--foreground) 8%, var(--background));
--foreground-10: color-mix(in oklch, var(--foreground) 10%, var(--background));
--foreground-20: color-mix(in oklch, var(--foreground) 20%, var(--background));
--foreground-30: color-mix(in oklch, var(--foreground) 30%, var(--background));
/* P-TEXT (roadmap §1.6): the -20/-30 text tiers were orphans (8 call
sites total) sitting between the surface washes (2..10) and the
real text ladder (40/50/60/70/80). Text uses moved to -40 (also a
contrast fix: 30% ink fails 4.5:1); decorative uses inlined their
color-mix. Fewer tiers = crisper hierarchy. */
--foreground-40: color-mix(in oklch, var(--foreground) 40%, var(--background));
--foreground-50: color-mix(in oklch, var(--foreground) 50%, var(--background));
--foreground-60: color-mix(in oklch, var(--foreground) 60%, var(--background));
Expand DownExpand Up@@ -681,8 +684,6 @@
--color-foreground-5: var(--foreground-5);
--color-foreground-8: var(--foreground-8);
--color-foreground-10: var(--foreground-10);
--color-foreground-20: var(--foreground-20);
--color-foreground-30: var(--foreground-30);
--color-foreground-40: var(--foreground-40);
--color-foreground-50: var(--foreground-50);
--color-foreground-60: var(--foreground-60);
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/onboarding.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -863,7 +863,7 @@
}
.maka-list-group-count {
font-variant-numeric: tabular-nums;
color: var(--foreground-30);
color: var(--foreground-40);
font-size: var(--font-size-caption);
font-weight: 500;
}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/settings/bot.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -114,7 +114,7 @@
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--foreground-30);
background: color-mix(in oklch, var(--foreground) 30%, var(--background));
box-shadow: 0 0 0 2px var(--background);
}
.settingsBotLogo[data-large="true"] .settingsBotLogoStatusDot {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/sidebar.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -165,7 +165,7 @@

.maka-resize-handle:hover::after,
.isResizingColumns .maka-resize-handle::after {
background: var(--foreground-20);
background: color-mix(in oklch, var(--foreground) 20%, var(--background));
}

/* Keyboard-focused separator: replace the suppressed native outline with a
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -376,7 +376,7 @@
.maka-composer-context-plus {
width: 26px;
height: 26px;
border: 1px solid var(--foreground-20);
border: 1px solid color-mix(in oklch, var(--foreground) 20%, var(--background));
border-radius: var(--radius-pill);
background: transparent;
}
Expand Down
8 changes: 6 additions & 2 deletions docs/design-refinement-roadmap-2026-07.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -112,8 +112,12 @@
4. **P-MOTION** ✅(已达标,无需动作):duration token
120/150/180/280ms 完全符合 Emil 表且取下限;装饰性入场动画已被
#406 gap 3 清除;剩余 keyframes 全部是功能性流式动画(D3 豁免)
5. **P-TEXT**:四档文字语义别名 + 新代码治理契约
6. **P-4PT**:4pt 治理契约(新增声明检查 + 存量 allowlist)
5. **P-TEXT** ✅(首轮):孤儿档 -20/-30 清除(8 处调用点:文字并入
-40 兼修对比度、装饰内联 color-mix);文字主力收敛为
40/50/60/70/80。全量四档语义别名迁移留待逐面触碰。
6. **P-4PT** ✅:ratchet 契约上线
(spacing-4pt-ratchet-contract.test.ts):442 个存量违例按文件冻结
只减不增,新文件零容忍,防 baseline 生锈的 slack 检查
7. **P-DARK**:dark elevation 独立化(依赖 P-SHADOW)
8. **P-STATE**:状态全周期补齐审计(skeleton 对形/empty 构图/inline error)

Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/primitives/chat.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -449,13 +449,13 @@ const toolVariants = cva("", {
// `.maka-tool-status-dot` (+ the `[data-status]` color swaps; running adds
// the box-shadow ring + `maka-tool-pulse` breath — keyframe stays in CSS).
dot:
"w-[8px] h-[8px] rounded-[var(--radius-pill)] bg-[var(--foreground-30)] [flex:0_0_auto]"
"w-[8px] h-[8px] rounded-[var(--radius-pill)] bg-[var(--foreground-40)] [flex:0_0_auto]"
// `waiting_permission` dot tint — see `WP_DOT_BG` above (String.raw).
+ " " + WP_DOT_BG
+ " data-[status=running]:bg-[var(--status-running)] data-[status=running]:[box-shadow:0_0_0_3px_oklch(from_var(--status-running)_l_c_h_/_0.15)] data-[status=running]:[animation:maka-tool-pulse_1.5s_ease-in-out_infinite]"
+ " data-[status=completed]:bg-[var(--success)]"
+ " data-[status=errored]:bg-[var(--destructive)]"
+ " data-[status=interrupted]:bg-[var(--foreground-30)]",
+ " data-[status=interrupted]:bg-[var(--foreground-40)]",
// `.maka-tool-name` — the mono tool name, ellipsized.
name:
"min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[color:var(--foreground)] font-medium [font-family:var(--font-mono)]",
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/stories/design-tokens.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ const foregroundScale = [
['foreground-2', '--foreground-2'],
['foreground-5', '--foreground-5'],
['foreground-10', '--foreground-10'],
['foreground-20', '--foreground-20'],
['foreground-50', '--foreground-50'],
['foreground-40', '--foreground-40'],
['foreground-60', '--foreground-60'],
['foreground-80', '--foreground-80'],
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions apps/desktop/src/main/__tests__/spacing-4pt-ratchet-contract.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
/**
* P-4PT spacing ratchet (design-refinement-roadmap-2026-07 §1.4, owner
* decision D1: converge on a 4pt spacing grid, migrating incrementally).
*
* Every padding/gap/margin px value in renderer CSS should be a multiple
* of 4 (0 allowed; 1px and 2px exempt as hairline/optical nudges). The
* legacy drift (442 values at baseline) is FROZEN per file below and may
* only go DOWN:
*
* - touching a file and reducing its count → update the baseline DOWN
* - adding a new off-grid value anywhere → this test fails
* - new CSS files must be born clean (no entry = zero tolerance)
*
* This is a ratchet, not an allowlist of specific lines, so refactors
* inside a file stay cheap while the global trend is monotonic.
*/

import { strict as assert } from 'node:assert';
import { readdir, readFile } from 'node:fs/promises';
import { join, relative, resolve } from 'node:path';
import { describe, it } from 'node:test';

const DESKTOP_ROOT = process.cwd().endsWith(join('apps', 'desktop'))
? process.cwd()
: resolve(process.cwd(), 'apps', 'desktop');
const RENDERER_ROOT = resolve(DESKTOP_ROOT, 'src', 'renderer');

/** Frozen per-file baseline (2026-07-03). Only decrease these numbers. */
const BASELINE: ReadonlyMap<string, number> = new Map([
['src/renderer/maka-tokens.css', 23],
['src/renderer/styles/chat-header.css', 26],
['src/renderer/styles/chat-message.css', 4],
['src/renderer/styles/composer.css', 12],
['src/renderer/styles/daily-review.css', 17],
['src/renderer/styles/health-center.css', 11],
['src/renderer/styles/module-pages.css', 76],
['src/renderer/styles/onboarding.css', 29],
['src/renderer/styles/permission-center.css', 28],
['src/renderer/styles/reasoning-panel.css', 3],
['src/renderer/styles/settings/bot.css', 23],
['src/renderer/styles/settings/connection.css', 9],
['src/renderer/styles/settings/form.css', 7],
['src/renderer/styles/settings/models.css', 31],
['src/renderer/styles/settings/nav-sidebar.css', 21],
['src/renderer/styles/settings/provider-editor.css', 20],
['src/renderer/styles/settings/theme-preview.css', 23],
['src/renderer/styles/sidebar.css', 36],
['src/renderer/styles/tool-output.css', 8],
['src/renderer/styles/tool-stream.css', 35],
]);

const DECL_RE =
/(?:^|;|\{)\s*(padding|gap|margin|row-gap|column-gap|padding-(?:top|right|bottom|left|inline|block)|margin-(?:top|right|bottom|left|inline|block))\s*:\s*([^;}]+)/gm;
const PX_RE = /(?<![\w.-])(\d+(?:\.\d+)?)px/g;

function stripComments(src: string): string {
return src.replace(/\/\*[\s\S]*?\*\//g, '');
}

function countOffGrid(css: string): number {
const stripped = stripComments(css);
let count = 0;
for (const decl of stripped.matchAll(DECL_RE)) {
for (const px of (decl[2] ?? '').matchAll(PX_RE)) {
const value = Number(px[1]);
if (value !== 0 && value % 4 !== 0 && value !== 1 && value !== 2) count += 1;
}
}
return count;
}

async function collectCssFiles(dir: string): Promise<string[]> {
const out: string[] = [];
for (const entry of await readdir(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) out.push(...(await collectCssFiles(full)));
else if (entry.name.endsWith('.css')) out.push(full);
}
return out;
}

describe('P-4PT spacing ratchet', () => {
it('padding/gap/margin px values stay on the 4pt grid (frozen legacy may only shrink)', async () => {
const files = await collectCssFiles(RENDERER_ROOT);
const failures: string[] = [];
for (const file of files.sort()) {
const rel = relative(DESKTOP_ROOT, file).split('\\').join('/');
const count = countOffGrid(await readFile(file, 'utf8'));
const allowed = BASELINE.get(rel) ?? 0;
if (count > allowed) {
failures.push(`${rel}: ${count} off-grid spacing values (baseline ${allowed})`);
}
}
assert.deepEqual(
failures,
[],
`off-grid spacing crept in — use 4/8/12/16/24/32 (1px/2px hairlines exempt), or shrink the file below its frozen baseline:\n${failures.join('\n')}`,
);
});

it('baseline entries stay honest (no stale higher-than-actual counts)', async () => {
// Guard against the ratchet rusting: if a file improves but the
// baseline is not updated, the slack could hide future regressions.
// Tolerate up to 3 slack per file before requiring a baseline update.
const stale: string[] = [];
for (const [rel, allowed] of BASELINE) {
const count = countOffGrid(await readFile(resolve(DESKTOP_ROOT, rel), 'utf8'));
if (allowed - count > 3) {
stale.push(`${rel}: baseline ${allowed} but actual ${count} — lower the baseline`);
}
}
assert.deepEqual(stale, [], stale.join('\n'));
});
});
9 changes: 5 additions & 4 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,8 +126,11 @@
--foreground-5: color-mix(in oklch, var(--foreground) 5%, var(--background));
--foreground-8: color-mix(in oklch, var(--foreground) 8%, var(--background));
--foreground-10: color-mix(in oklch, var(--foreground) 10%, var(--background));
--foreground-20: color-mix(in oklch, var(--foreground) 20%, var(--background));
--foreground-30: color-mix(in oklch, var(--foreground) 30%, var(--background));
/* P-TEXT (roadmap §1.6): the -20/-30 text tiers were orphans (8 call
sites total) sitting between the surface washes (2..10) and the
real text ladder (40/50/60/70/80). Text uses moved to -40 (also a
contrast fix: 30% ink fails 4.5:1); decorative uses inlined their
color-mix. Fewer tiers = crisper hierarchy. */
--foreground-40: color-mix(in oklch, var(--foreground) 40%, var(--background));
--foreground-50: color-mix(in oklch, var(--foreground) 50%, var(--background));
--foreground-60: color-mix(in oklch, var(--foreground) 60%, var(--background));
Expand DownExpand Up@@ -681,8 +684,6 @@
--color-foreground-5: var(--foreground-5);
--color-foreground-8: var(--foreground-8);
--color-foreground-10: var(--foreground-10);
--color-foreground-20: var(--foreground-20);
--color-foreground-30: var(--foreground-30);
--color-foreground-40: var(--foreground-40);
--color-foreground-50: var(--foreground-50);
--color-foreground-60: var(--foreground-60);
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/onboarding.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -863,7 +863,7 @@
}
.maka-list-group-count {
font-variant-numeric: tabular-nums;
color: var(--foreground-30);
color: var(--foreground-40);
font-size: var(--font-size-caption);
font-weight: 500;
}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/settings/bot.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -114,7 +114,7 @@
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--foreground-30);
background: color-mix(in oklch, var(--foreground) 30%, var(--background));
box-shadow: 0 0 0 2px var(--background);
}
.settingsBotLogo[data-large="true"] .settingsBotLogoStatusDot {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/sidebar.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -165,7 +165,7 @@

.maka-resize-handle:hover::after,
.isResizingColumns .maka-resize-handle::after {
background: var(--foreground-20);
background: color-mix(in oklch, var(--foreground) 20%, var(--background));
}

/* Keyboard-focused separator: replace the suppressed native outline with a
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -376,7 +376,7 @@
.maka-composer-context-plus {
width: 26px;
height: 26px;
border: 1px solid var(--foreground-20);
border: 1px solid color-mix(in oklch, var(--foreground) 20%, var(--background));
border-radius: var(--radius-pill);
background: transparent;
}
Expand Down
8 changes: 6 additions & 2 deletions docs/design-refinement-roadmap-2026-07.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -112,8 +112,12 @@
4. **P-MOTION** ✅(已达标,无需动作):duration token
120/150/180/280ms 完全符合 Emil 表且取下限;装饰性入场动画已被
#406 gap 3 清除;剩余 keyframes 全部是功能性流式动画(D3 豁免)
5. **P-TEXT**:四档文字语义别名 + 新代码治理契约
6. **P-4PT**:4pt 治理契约(新增声明检查 + 存量 allowlist)
5. **P-TEXT** ✅(首轮):孤儿档 -20/-30 清除(8 处调用点:文字并入
-40 兼修对比度、装饰内联 color-mix);文字主力收敛为
40/50/60/70/80。全量四档语义别名迁移留待逐面触碰。
6. **P-4PT** ✅:ratchet 契约上线
(spacing-4pt-ratchet-contract.test.ts):442 个存量违例按文件冻结
只减不增,新文件零容忍,防 baseline 生锈的 slack 检查
7. **P-DARK**:dark elevation 独立化(依赖 P-SHADOW)
8. **P-STATE**:状态全周期补齐审计(skeleton 对形/empty 构图/inline error)

Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/primitives/chat.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -449,13 +449,13 @@ const toolVariants = cva("", {
// `.maka-tool-status-dot` (+ the `[data-status]` color swaps; running adds
// the box-shadow ring + `maka-tool-pulse` breath — keyframe stays in CSS).
dot:
"w-[8px] h-[8px] rounded-[var(--radius-pill)] bg-[var(--foreground-30)] [flex:0_0_auto]"
"w-[8px] h-[8px] rounded-[var(--radius-pill)] bg-[var(--foreground-40)] [flex:0_0_auto]"
// `waiting_permission` dot tint — see `WP_DOT_BG` above (String.raw).
+ " " + WP_DOT_BG
+ " data-[status=running]:bg-[var(--status-running)] data-[status=running]:[box-shadow:0_0_0_3px_oklch(from_var(--status-running)_l_c_h_/_0.15)] data-[status=running]:[animation:maka-tool-pulse_1.5s_ease-in-out_infinite]"
+ " data-[status=completed]:bg-[var(--success)]"
+ " data-[status=errored]:bg-[var(--destructive)]"
+ " data-[status=interrupted]:bg-[var(--foreground-30)]",
+ " data-[status=interrupted]:bg-[var(--foreground-40)]",
// `.maka-tool-name` — the mono tool name, ellipsized.
name:
"min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[color:var(--foreground)] font-medium [font-family:var(--font-mono)]",
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/stories/design-tokens.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ const foregroundScale = [
['foreground-2', '--foreground-2'],
['foreground-5', '--foreground-5'],
['foreground-10', '--foreground-10'],
['foreground-20', '--foreground-20'],
['foreground-50', '--foreground-50'],
['foreground-40', '--foreground-40'],
['foreground-60', '--foreground-60'],
['foreground-80', '--foreground-80'],
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions apps/desktop/src/main/__tests__/spacing-4pt-ratchet-contract.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
/**
* P-4PT spacing ratchet (design-refinement-roadmap-2026-07 §1.4, owner
* decision D1: converge on a 4pt spacing grid, migrating incrementally).
*
* Every padding/gap/margin px value in renderer CSS should be a multiple
* of 4 (0 allowed; 1px and 2px exempt as hairline/optical nudges). The
* legacy drift (442 values at baseline) is FROZEN per file below and may
* only go DOWN:
*
* - touching a file and reducing its count → update the baseline DOWN
* - adding a new off-grid value anywhere → this test fails
* - new CSS files must be born clean (no entry = zero tolerance)
*
* This is a ratchet, not an allowlist of specific lines, so refactors
* inside a file stay cheap while the global trend is monotonic.
*/

import { strict as assert } from 'node:assert';
import { readdir, readFile } from 'node:fs/promises';
import { join, relative, resolve } from 'node:path';
import { describe, it } from 'node:test';

const DESKTOP_ROOT = process.cwd().endsWith(join('apps', 'desktop'))
? process.cwd()
: resolve(process.cwd(), 'apps', 'desktop');
const RENDERER_ROOT = resolve(DESKTOP_ROOT, 'src', 'renderer');

/** Frozen per-file baseline (2026-07-03). Only decrease these numbers. */
const BASELINE: ReadonlyMap<string, number> = new Map([
['src/renderer/maka-tokens.css', 23],
['src/renderer/styles/chat-header.css', 26],
['src/renderer/styles/chat-message.css', 4],
['src/renderer/styles/composer.css', 12],
['src/renderer/styles/daily-review.css', 17],
['src/renderer/styles/health-center.css', 11],
['src/renderer/styles/module-pages.css', 76],
['src/renderer/styles/onboarding.css', 29],
['src/renderer/styles/permission-center.css', 28],
['src/renderer/styles/reasoning-panel.css', 3],
['src/renderer/styles/settings/bot.css', 23],
['src/renderer/styles/settings/connection.css', 9],
['src/renderer/styles/settings/form.css', 7],
['src/renderer/styles/settings/models.css', 31],
['src/renderer/styles/settings/nav-sidebar.css', 21],
['src/renderer/styles/settings/provider-editor.css', 20],
['src/renderer/styles/settings/theme-preview.css', 23],
['src/renderer/styles/sidebar.css', 36],
['src/renderer/styles/tool-output.css', 8],
['src/renderer/styles/tool-stream.css', 35],
]);

const DECL_RE =
/(?:^|;|\{)\s*(padding|gap|margin|row-gap|column-gap|padding-(?:top|right|bottom|left|inline|block)|margin-(?:top|right|bottom|left|inline|block))\s*:\s*([^;}]+)/gm;
const PX_RE = /(?<![\w.-])(\d+(?:\.\d+)?)px/g;

function stripComments(src: string): string {
return src.replace(/\/\*[\s\S]*?\*\//g, '');
}

function countOffGrid(css: string): number {
const stripped = stripComments(css);
let count = 0;
for (const decl of stripped.matchAll(DECL_RE)) {
for (const px of (decl[2] ?? '').matchAll(PX_RE)) {
const value = Number(px[1]);
if (value !== 0 && value % 4 !== 0 && value !== 1 && value !== 2) count += 1;
}
}
return count;
}

async function collectCssFiles(dir: string): Promise<string[]> {
const out: string[] = [];
for (const entry of await readdir(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) out.push(...(await collectCssFiles(full)));
else if (entry.name.endsWith('.css')) out.push(full);
}
return out;
}

describe('P-4PT spacing ratchet', () => {
it('padding/gap/margin px values stay on the 4pt grid (frozen legacy may only shrink)', async () => {
const files = await collectCssFiles(RENDERER_ROOT);
const failures: string[] = [];
for (const file of files.sort()) {
const rel = relative(DESKTOP_ROOT, file).split('\\').join('/');
const count = countOffGrid(await readFile(file, 'utf8'));
const allowed = BASELINE.get(rel) ?? 0;
if (count > allowed) {
failures.push(`${rel}: ${count} off-grid spacing values (baseline ${allowed})`);
}
}
assert.deepEqual(
failures,
[],
`off-grid spacing crept in — use 4/8/12/16/24/32 (1px/2px hairlines exempt), or shrink the file below its frozen baseline:\n${failures.join('\n')}`,
);
});

it('baseline entries stay honest (no stale higher-than-actual counts)', async () => {
// Guard against the ratchet rusting: if a file improves but the
// baseline is not updated, the slack could hide future regressions.
// Tolerate up to 3 slack per file before requiring a baseline update.
const stale: string[] = [];
for (const [rel, allowed] of BASELINE) {
const count = countOffGrid(await readFile(resolve(DESKTOP_ROOT, rel), 'utf8'));
if (allowed - count > 3) {
stale.push(`${rel}: baseline ${allowed} but actual ${count} — lower the baseline`);
}
}
assert.deepEqual(stale, [], stale.join('\n'));
});
});
9 changes: 5 additions & 4 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,8 +126,11 @@
--foreground-5: color-mix(in oklch, var(--foreground) 5%, var(--background));
--foreground-8: color-mix(in oklch, var(--foreground) 8%, var(--background));
--foreground-10: color-mix(in oklch, var(--foreground) 10%, var(--background));
--foreground-20: color-mix(in oklch, var(--foreground) 20%, var(--background));
--foreground-30: color-mix(in oklch, var(--foreground) 30%, var(--background));
/* P-TEXT (roadmap §1.6): the -20/-30 text tiers were orphans (8 call
sites total) sitting between the surface washes (2..10) and the
real text ladder (40/50/60/70/80). Text uses moved to -40 (also a
contrast fix: 30% ink fails 4.5:1); decorative uses inlined their
color-mix. Fewer tiers = crisper hierarchy. */
--foreground-40: color-mix(in oklch, var(--foreground) 40%, var(--background));
--foreground-50: color-mix(in oklch, var(--foreground) 50%, var(--background));
--foreground-60: color-mix(in oklch, var(--foreground) 60%, var(--background));
Expand DownExpand Up@@ -681,8 +684,6 @@
--color-foreground-5: var(--foreground-5);
--color-foreground-8: var(--foreground-8);
--color-foreground-10: var(--foreground-10);
--color-foreground-20: var(--foreground-20);
--color-foreground-30: var(--foreground-30);
--color-foreground-40: var(--foreground-40);
--color-foreground-50: var(--foreground-50);
--color-foreground-60: var(--foreground-60);
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/onboarding.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -863,7 +863,7 @@
}
.maka-list-group-count {
font-variant-numeric: tabular-nums;
color: var(--foreground-30);
color: var(--foreground-40);
font-size: var(--font-size-caption);
font-weight: 500;
}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/settings/bot.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -114,7 +114,7 @@
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--foreground-30);
background: color-mix(in oklch, var(--foreground) 30%, var(--background));
box-shadow: 0 0 0 2px var(--background);
}
.settingsBotLogo[data-large="true"] .settingsBotLogoStatusDot {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/sidebar.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -165,7 +165,7 @@

.maka-resize-handle:hover::after,
.isResizingColumns .maka-resize-handle::after {
background: var(--foreground-20);
background: color-mix(in oklch, var(--foreground) 20%, var(--background));
}

/* Keyboard-focused separator: replace the suppressed native outline with a
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -376,7 +376,7 @@
.maka-composer-context-plus {
width: 26px;
height: 26px;
border: 1px solid var(--foreground-20);
border: 1px solid color-mix(in oklch, var(--foreground) 20%, var(--background));
border-radius: var(--radius-pill);
background: transparent;
}
Expand Down
8 changes: 6 additions & 2 deletions docs/design-refinement-roadmap-2026-07.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -112,8 +112,12 @@
4. **P-MOTION** ✅(已达标,无需动作):duration token
120/150/180/280ms 完全符合 Emil 表且取下限;装饰性入场动画已被
#406 gap 3 清除;剩余 keyframes 全部是功能性流式动画(D3 豁免)
5. **P-TEXT**:四档文字语义别名 + 新代码治理契约
6. **P-4PT**:4pt 治理契约(新增声明检查 + 存量 allowlist)
5. **P-TEXT** ✅(首轮):孤儿档 -20/-30 清除(8 处调用点:文字并入
-40 兼修对比度、装饰内联 color-mix);文字主力收敛为
40/50/60/70/80。全量四档语义别名迁移留待逐面触碰。
6. **P-4PT** ✅:ratchet 契约上线
(spacing-4pt-ratchet-contract.test.ts):442 个存量违例按文件冻结
只减不增,新文件零容忍,防 baseline 生锈的 slack 检查
7. **P-DARK**:dark elevation 独立化(依赖 P-SHADOW)
8. **P-STATE**:状态全周期补齐审计(skeleton 对形/empty 构图/inline error)

Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/primitives/chat.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -449,13 +449,13 @@ const toolVariants = cva("", {
// `.maka-tool-status-dot` (+ the `[data-status]` color swaps; running adds
// the box-shadow ring + `maka-tool-pulse` breath — keyframe stays in CSS).
dot:
"w-[8px] h-[8px] rounded-[var(--radius-pill)] bg-[var(--foreground-30)] [flex:0_0_auto]"
"w-[8px] h-[8px] rounded-[var(--radius-pill)] bg-[var(--foreground-40)] [flex:0_0_auto]"
// `waiting_permission` dot tint — see `WP_DOT_BG` above (String.raw).
+ " " + WP_DOT_BG
+ " data-[status=running]:bg-[var(--status-running)] data-[status=running]:[box-shadow:0_0_0_3px_oklch(from_var(--status-running)_l_c_h_/_0.15)] data-[status=running]:[animation:maka-tool-pulse_1.5s_ease-in-out_infinite]"
+ " data-[status=completed]:bg-[var(--success)]"
+ " data-[status=errored]:bg-[var(--destructive)]"
+ " data-[status=interrupted]:bg-[var(--foreground-30)]",
+ " data-[status=interrupted]:bg-[var(--foreground-40)]",
// `.maka-tool-name` — the mono tool name, ellipsized.
name:
"min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[color:var(--foreground)] font-medium [font-family:var(--font-mono)]",
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/stories/design-tokens.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ const foregroundScale = [
['foreground-2', '--foreground-2'],
['foreground-5', '--foreground-5'],
['foreground-10', '--foreground-10'],
['foreground-20', '--foreground-20'],
['foreground-50', '--foreground-50'],
['foreground-40', '--foreground-40'],
['foreground-60', '--foreground-60'],
['foreground-80', '--foreground-80'],
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions apps/desktop/src/main/__tests__/spacing-4pt-ratchet-contract.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
/**
* P-4PT spacing ratchet (design-refinement-roadmap-2026-07 §1.4, owner
* decision D1: converge on a 4pt spacing grid, migrating incrementally).
*
* Every padding/gap/margin px value in renderer CSS should be a multiple
* of 4 (0 allowed; 1px and 2px exempt as hairline/optical nudges). The
* legacy drift (442 values at baseline) is FROZEN per file below and may
* only go DOWN:
*
* - touching a file and reducing its count → update the baseline DOWN
* - adding a new off-grid value anywhere → this test fails
* - new CSS files must be born clean (no entry = zero tolerance)
*
* This is a ratchet, not an allowlist of specific lines, so refactors
* inside a file stay cheap while the global trend is monotonic.
*/

import { strict as assert } from 'node:assert';
import { readdir, readFile } from 'node:fs/promises';
import { join, relative, resolve } from 'node:path';
import { describe, it } from 'node:test';

const DESKTOP_ROOT = process.cwd().endsWith(join('apps', 'desktop'))
? process.cwd()
: resolve(process.cwd(), 'apps', 'desktop');
const RENDERER_ROOT = resolve(DESKTOP_ROOT, 'src', 'renderer');

/** Frozen per-file baseline (2026-07-03). Only decrease these numbers. */
const BASELINE: ReadonlyMap<string, number> = new Map([
['src/renderer/maka-tokens.css', 23],
['src/renderer/styles/chat-header.css', 26],
['src/renderer/styles/chat-message.css', 4],
['src/renderer/styles/composer.css', 12],
['src/renderer/styles/daily-review.css', 17],
['src/renderer/styles/health-center.css', 11],
['src/renderer/styles/module-pages.css', 76],
['src/renderer/styles/onboarding.css', 29],
['src/renderer/styles/permission-center.css', 28],
['src/renderer/styles/reasoning-panel.css', 3],
['src/renderer/styles/settings/bot.css', 23],
['src/renderer/styles/settings/connection.css', 9],
['src/renderer/styles/settings/form.css', 7],
['src/renderer/styles/settings/models.css', 31],
['src/renderer/styles/settings/nav-sidebar.css', 21],
['src/renderer/styles/settings/provider-editor.css', 20],
['src/renderer/styles/settings/theme-preview.css', 23],
['src/renderer/styles/sidebar.css', 36],
['src/renderer/styles/tool-output.css', 8],
['src/renderer/styles/tool-stream.css', 35],
]);

const DECL_RE =
/(?:^|;|\{)\s*(padding|gap|margin|row-gap|column-gap|padding-(?:top|right|bottom|left|inline|block)|margin-(?:top|right|bottom|left|inline|block))\s*:\s*([^;}]+)/gm;
const PX_RE = /(?<![\w.-])(\d+(?:\.\d+)?)px/g;

function stripComments(src: string): string {
return src.replace(/\/\*[\s\S]*?\*\//g, '');
}

function countOffGrid(css: string): number {
const stripped = stripComments(css);
let count = 0;
for (const decl of stripped.matchAll(DECL_RE)) {
for (const px of (decl[2] ?? '').matchAll(PX_RE)) {
const value = Number(px[1]);
if (value !== 0 && value % 4 !== 0 && value !== 1 && value !== 2) count += 1;
}
}
return count;
}

async function collectCssFiles(dir: string): Promise<string[]> {
const out: string[] = [];
for (const entry of await readdir(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) out.push(...(await collectCssFiles(full)));
else if (entry.name.endsWith('.css')) out.push(full);
}
return out;
}

describe('P-4PT spacing ratchet', () => {
it('padding/gap/margin px values stay on the 4pt grid (frozen legacy may only shrink)', async () => {
const files = await collectCssFiles(RENDERER_ROOT);
const failures: string[] = [];
for (const file of files.sort()) {
const rel = relative(DESKTOP_ROOT, file).split('\\').join('/');
const count = countOffGrid(await readFile(file, 'utf8'));
const allowed = BASELINE.get(rel) ?? 0;
if (count > allowed) {
failures.push(`${rel}: ${count} off-grid spacing values (baseline ${allowed})`);
}
}
assert.deepEqual(
failures,
[],
`off-grid spacing crept in — use 4/8/12/16/24/32 (1px/2px hairlines exempt), or shrink the file below its frozen baseline:\n${failures.join('\n')}`,
);
});

it('baseline entries stay honest (no stale higher-than-actual counts)', async () => {
// Guard against the ratchet rusting: if a file improves but the
// baseline is not updated, the slack could hide future regressions.
// Tolerate up to 3 slack per file before requiring a baseline update.
const stale: string[] = [];
for (const [rel, allowed] of BASELINE) {
const count = countOffGrid(await readFile(resolve(DESKTOP_ROOT, rel), 'utf8'));
if (allowed - count > 3) {
stale.push(`${rel}: baseline ${allowed} but actual ${count} — lower the baseline`);
}
}
assert.deepEqual(stale, [], stale.join('\n'));
});
});
9 changes: 5 additions & 4 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,8 +126,11 @@
--foreground-5: color-mix(in oklch, var(--foreground) 5%, var(--background));
--foreground-8: color-mix(in oklch, var(--foreground) 8%, var(--background));
--foreground-10: color-mix(in oklch, var(--foreground) 10%, var(--background));
--foreground-20: color-mix(in oklch, var(--foreground) 20%, var(--background));
--foreground-30: color-mix(in oklch, var(--foreground) 30%, var(--background));
/* P-TEXT (roadmap §1.6): the -20/-30 text tiers were orphans (8 call
sites total) sitting between the surface washes (2..10) and the
real text ladder (40/50/60/70/80). Text uses moved to -40 (also a
contrast fix: 30% ink fails 4.5:1); decorative uses inlined their
color-mix. Fewer tiers = crisper hierarchy. */
--foreground-40: color-mix(in oklch, var(--foreground) 40%, var(--background));
--foreground-50: color-mix(in oklch, var(--foreground) 50%, var(--background));
--foreground-60: color-mix(in oklch, var(--foreground) 60%, var(--background));
Expand DownExpand Up@@ -681,8 +684,6 @@
--color-foreground-5: var(--foreground-5);
--color-foreground-8: var(--foreground-8);
--color-foreground-10: var(--foreground-10);
--color-foreground-20: var(--foreground-20);
--color-foreground-30: var(--foreground-30);
--color-foreground-40: var(--foreground-40);
--color-foreground-50: var(--foreground-50);
--color-foreground-60: var(--foreground-60);
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/onboarding.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -863,7 +863,7 @@
}
.maka-list-group-count {
font-variant-numeric: tabular-nums;
color: var(--foreground-30);
color: var(--foreground-40);
font-size: var(--font-size-caption);
font-weight: 500;
}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/settings/bot.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -114,7 +114,7 @@
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--foreground-30);
background: color-mix(in oklch, var(--foreground) 30%, var(--background));
box-shadow: 0 0 0 2px var(--background);
}
.settingsBotLogo[data-large="true"] .settingsBotLogoStatusDot {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/sidebar.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -165,7 +165,7 @@

.maka-resize-handle:hover::after,
.isResizingColumns .maka-resize-handle::after {
background: var(--foreground-20);
background: color-mix(in oklch, var(--foreground) 20%, var(--background));
}

/* Keyboard-focused separator: replace the suppressed native outline with a
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -376,7 +376,7 @@
.maka-composer-context-plus {
width: 26px;
height: 26px;
border: 1px solid var(--foreground-20);
border: 1px solid color-mix(in oklch, var(--foreground) 20%, var(--background));
border-radius: var(--radius-pill);
background: transparent;
}
Expand Down
8 changes: 6 additions & 2 deletions docs/design-refinement-roadmap-2026-07.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -112,8 +112,12 @@
4. **P-MOTION** ✅(已达标,无需动作):duration token
120/150/180/280ms 完全符合 Emil 表且取下限;装饰性入场动画已被
#406 gap 3 清除;剩余 keyframes 全部是功能性流式动画(D3 豁免)
5. **P-TEXT**:四档文字语义别名 + 新代码治理契约
6. **P-4PT**:4pt 治理契约(新增声明检查 + 存量 allowlist)
5. **P-TEXT** ✅(首轮):孤儿档 -20/-30 清除(8 处调用点:文字并入
-40 兼修对比度、装饰内联 color-mix);文字主力收敛为
40/50/60/70/80。全量四档语义别名迁移留待逐面触碰。
6. **P-4PT** ✅:ratchet 契约上线
(spacing-4pt-ratchet-contract.test.ts):442 个存量违例按文件冻结
只减不增,新文件零容忍,防 baseline 生锈的 slack 检查
7. **P-DARK**:dark elevation 独立化(依赖 P-SHADOW)
8. **P-STATE**:状态全周期补齐审计(skeleton 对形/empty 构图/inline error)

Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/primitives/chat.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -449,13 +449,13 @@ const toolVariants = cva("", {
// `.maka-tool-status-dot` (+ the `[data-status]` color swaps; running adds
// the box-shadow ring + `maka-tool-pulse` breath — keyframe stays in CSS).
dot:
"w-[8px] h-[8px] rounded-[var(--radius-pill)] bg-[var(--foreground-30)] [flex:0_0_auto]"
"w-[8px] h-[8px] rounded-[var(--radius-pill)] bg-[var(--foreground-40)] [flex:0_0_auto]"
// `waiting_permission` dot tint — see `WP_DOT_BG` above (String.raw).
+ " " + WP_DOT_BG
+ " data-[status=running]:bg-[var(--status-running)] data-[status=running]:[box-shadow:0_0_0_3px_oklch(from_var(--status-running)_l_c_h_/_0.15)] data-[status=running]:[animation:maka-tool-pulse_1.5s_ease-in-out_infinite]"
+ " data-[status=completed]:bg-[var(--success)]"
+ " data-[status=errored]:bg-[var(--destructive)]"
+ " data-[status=interrupted]:bg-[var(--foreground-30)]",
+ " data-[status=interrupted]:bg-[var(--foreground-40)]",
// `.maka-tool-name` — the mono tool name, ellipsized.
name:
"min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[color:var(--foreground)] font-medium [font-family:var(--font-mono)]",
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/stories/design-tokens.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ const foregroundScale = [
['foreground-2', '--foreground-2'],
['foreground-5', '--foreground-5'],
['foreground-10', '--foreground-10'],
['foreground-20', '--foreground-20'],
['foreground-50', '--foreground-50'],
['foreground-40', '--foreground-40'],
['foreground-60', '--foreground-60'],
['foreground-80', '--foreground-80'],
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions apps/desktop/src/main/__tests__/spacing-4pt-ratchet-contract.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
/**
* P-4PT spacing ratchet (design-refinement-roadmap-2026-07 §1.4, owner
* decision D1: converge on a 4pt spacing grid, migrating incrementally).
*
* Every padding/gap/margin px value in renderer CSS should be a multiple
* of 4 (0 allowed; 1px and 2px exempt as hairline/optical nudges). The
* legacy drift (442 values at baseline) is FROZEN per file below and may
* only go DOWN:
*
* - touching a file and reducing its count → update the baseline DOWN
* - adding a new off-grid value anywhere → this test fails
* - new CSS files must be born clean (no entry = zero tolerance)
*
* This is a ratchet, not an allowlist of specific lines, so refactors
* inside a file stay cheap while the global trend is monotonic.
*/

import { strict as assert } from 'node:assert';
import { readdir, readFile } from 'node:fs/promises';
import { join, relative, resolve } from 'node:path';
import { describe, it } from 'node:test';

const DESKTOP_ROOT = process.cwd().endsWith(join('apps', 'desktop'))
? process.cwd()
: resolve(process.cwd(), 'apps', 'desktop');
const RENDERER_ROOT = resolve(DESKTOP_ROOT, 'src', 'renderer');

/** Frozen per-file baseline (2026-07-03). Only decrease these numbers. */
const BASELINE: ReadonlyMap<string, number> = new Map([
['src/renderer/maka-tokens.css', 23],
['src/renderer/styles/chat-header.css', 26],
['src/renderer/styles/chat-message.css', 4],
['src/renderer/styles/composer.css', 12],
['src/renderer/styles/daily-review.css', 17],
['src/renderer/styles/health-center.css', 11],
['src/renderer/styles/module-pages.css', 76],
['src/renderer/styles/onboarding.css', 29],
['src/renderer/styles/permission-center.css', 28],
['src/renderer/styles/reasoning-panel.css', 3],
['src/renderer/styles/settings/bot.css', 23],
['src/renderer/styles/settings/connection.css', 9],
['src/renderer/styles/settings/form.css', 7],
['src/renderer/styles/settings/models.css', 31],
['src/renderer/styles/settings/nav-sidebar.css', 21],
['src/renderer/styles/settings/provider-editor.css', 20],
['src/renderer/styles/settings/theme-preview.css', 23],
['src/renderer/styles/sidebar.css', 36],
['src/renderer/styles/tool-output.css', 8],
['src/renderer/styles/tool-stream.css', 35],
]);

const DECL_RE =
/(?:^|;|\{)\s*(padding|gap|margin|row-gap|column-gap|padding-(?:top|right|bottom|left|inline|block)|margin-(?:top|right|bottom|left|inline|block))\s*:\s*([^;}]+)/gm;
const PX_RE = /(?<![\w.-])(\d+(?:\.\d+)?)px/g;

function stripComments(src: string): string {
return src.replace(/\/\*[\s\S]*?\*\//g, '');
}

function countOffGrid(css: string): number {
const stripped = stripComments(css);
let count = 0;
for (const decl of stripped.matchAll(DECL_RE)) {
for (const px of (decl[2] ?? '').matchAll(PX_RE)) {
const value = Number(px[1]);
if (value !== 0 && value % 4 !== 0 && value !== 1 && value !== 2) count += 1;
}
}
return count;
}

async function collectCssFiles(dir: string): Promise<string[]> {
const out: string[] = [];
for (const entry of await readdir(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) out.push(...(await collectCssFiles(full)));
else if (entry.name.endsWith('.css')) out.push(full);
}
return out;
}

describe('P-4PT spacing ratchet', () => {
it('padding/gap/margin px values stay on the 4pt grid (frozen legacy may only shrink)', async () => {
const files = await collectCssFiles(RENDERER_ROOT);
const failures: string[] = [];
for (const file of files.sort()) {
const rel = relative(DESKTOP_ROOT, file).split('\\').join('/');
const count = countOffGrid(await readFile(file, 'utf8'));
const allowed = BASELINE.get(rel) ?? 0;
if (count > allowed) {
failures.push(`${rel}: ${count} off-grid spacing values (baseline ${allowed})`);
}
}
assert.deepEqual(
failures,
[],
`off-grid spacing crept in — use 4/8/12/16/24/32 (1px/2px hairlines exempt), or shrink the file below its frozen baseline:\n${failures.join('\n')}`,
);
});

it('baseline entries stay honest (no stale higher-than-actual counts)', async () => {
// Guard against the ratchet rusting: if a file improves but the
// baseline is not updated, the slack could hide future regressions.
// Tolerate up to 3 slack per file before requiring a baseline update.
const stale: string[] = [];
for (const [rel, allowed] of BASELINE) {
const count = countOffGrid(await readFile(resolve(DESKTOP_ROOT, rel), 'utf8'));
if (allowed - count > 3) {
stale.push(`${rel}: baseline ${allowed} but actual ${count} — lower the baseline`);
}
}
assert.deepEqual(stale, [], stale.join('\n'));
});
});
9 changes: 5 additions & 4 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,8 +126,11 @@
--foreground-5: color-mix(in oklch, var(--foreground) 5%, var(--background));
--foreground-8: color-mix(in oklch, var(--foreground) 8%, var(--background));
--foreground-10: color-mix(in oklch, var(--foreground) 10%, var(--background));
--foreground-20: color-mix(in oklch, var(--foreground) 20%, var(--background));
--foreground-30: color-mix(in oklch, var(--foreground) 30%, var(--background));
/* P-TEXT (roadmap §1.6): the -20/-30 text tiers were orphans (8 call
sites total) sitting between the surface washes (2..10) and the
real text ladder (40/50/60/70/80). Text uses moved to -40 (also a
contrast fix: 30% ink fails 4.5:1); decorative uses inlined their
color-mix. Fewer tiers = crisper hierarchy. */
--foreground-40: color-mix(in oklch, var(--foreground) 40%, var(--background));
--foreground-50: color-mix(in oklch, var(--foreground) 50%, var(--background));
--foreground-60: color-mix(in oklch, var(--foreground) 60%, var(--background));
Expand DownExpand Up@@ -681,8 +684,6 @@
--color-foreground-5: var(--foreground-5);
--color-foreground-8: var(--foreground-8);
--color-foreground-10: var(--foreground-10);
--color-foreground-20: var(--foreground-20);
--color-foreground-30: var(--foreground-30);
--color-foreground-40: var(--foreground-40);
--color-foreground-50: var(--foreground-50);
--color-foreground-60: var(--foreground-60);
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/onboarding.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -863,7 +863,7 @@
}
.maka-list-group-count {
font-variant-numeric: tabular-nums;
color: var(--foreground-30);
color: var(--foreground-40);
font-size: var(--font-size-caption);
font-weight: 500;
}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/settings/bot.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -114,7 +114,7 @@
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--foreground-30);
background: color-mix(in oklch, var(--foreground) 30%, var(--background));
box-shadow: 0 0 0 2px var(--background);
}
.settingsBotLogo[data-large="true"] .settingsBotLogoStatusDot {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/sidebar.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -165,7 +165,7 @@

.maka-resize-handle:hover::after,
.isResizingColumns .maka-resize-handle::after {
background: var(--foreground-20);
background: color-mix(in oklch, var(--foreground) 20%, var(--background));
}

/* Keyboard-focused separator: replace the suppressed native outline with a
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -376,7 +376,7 @@
.maka-composer-context-plus {
width: 26px;
height: 26px;
border: 1px solid var(--foreground-20);
border: 1px solid color-mix(in oklch, var(--foreground) 20%, var(--background));
border-radius: var(--radius-pill);
background: transparent;
}
Expand Down
8 changes: 6 additions & 2 deletions docs/design-refinement-roadmap-2026-07.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -112,8 +112,12 @@
4. **P-MOTION** ✅(已达标,无需动作):duration token
120/150/180/280ms 完全符合 Emil 表且取下限;装饰性入场动画已被
#406 gap 3 清除;剩余 keyframes 全部是功能性流式动画(D3 豁免)
5. **P-TEXT**:四档文字语义别名 + 新代码治理契约
6. **P-4PT**:4pt 治理契约(新增声明检查 + 存量 allowlist)
5. **P-TEXT** ✅(首轮):孤儿档 -20/-30 清除(8 处调用点:文字并入
-40 兼修对比度、装饰内联 color-mix);文字主力收敛为
40/50/60/70/80。全量四档语义别名迁移留待逐面触碰。
6. **P-4PT** ✅:ratchet 契约上线
(spacing-4pt-ratchet-contract.test.ts):442 个存量违例按文件冻结
只减不增,新文件零容忍,防 baseline 生锈的 slack 检查
7. **P-DARK**:dark elevation 独立化(依赖 P-SHADOW)
8. **P-STATE**:状态全周期补齐审计(skeleton 对形/empty 构图/inline error)

Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/primitives/chat.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -449,13 +449,13 @@ const toolVariants = cva("", {
// `.maka-tool-status-dot` (+ the `[data-status]` color swaps; running adds
// the box-shadow ring + `maka-tool-pulse` breath — keyframe stays in CSS).
dot:
"w-[8px] h-[8px] rounded-[var(--radius-pill)] bg-[var(--foreground-30)] [flex:0_0_auto]"
"w-[8px] h-[8px] rounded-[var(--radius-pill)] bg-[var(--foreground-40)] [flex:0_0_auto]"
// `waiting_permission` dot tint — see `WP_DOT_BG` above (String.raw).
+ " " + WP_DOT_BG
+ " data-[status=running]:bg-[var(--status-running)] data-[status=running]:[box-shadow:0_0_0_3px_oklch(from_var(--status-running)_l_c_h_/_0.15)] data-[status=running]:[animation:maka-tool-pulse_1.5s_ease-in-out_infinite]"
+ " data-[status=completed]:bg-[var(--success)]"
+ " data-[status=errored]:bg-[var(--destructive)]"
+ " data-[status=interrupted]:bg-[var(--foreground-30)]",
+ " data-[status=interrupted]:bg-[var(--foreground-40)]",
// `.maka-tool-name` — the mono tool name, ellipsized.
name:
"min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[color:var(--foreground)] font-medium [font-family:var(--font-mono)]",
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/stories/design-tokens.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ const foregroundScale = [
['foreground-2', '--foreground-2'],
['foreground-5', '--foreground-5'],
['foreground-10', '--foreground-10'],
['foreground-20', '--foreground-20'],
['foreground-50', '--foreground-50'],
['foreground-40', '--foreground-40'],
['foreground-60', '--foreground-60'],
['foreground-80', '--foreground-80'],
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions apps/desktop/src/main/__tests__/spacing-4pt-ratchet-contract.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
/**
* P-4PT spacing ratchet (design-refinement-roadmap-2026-07 §1.4, owner
* decision D1: converge on a 4pt spacing grid, migrating incrementally).
*
* Every padding/gap/margin px value in renderer CSS should be a multiple
* of 4 (0 allowed; 1px and 2px exempt as hairline/optical nudges). The
* legacy drift (442 values at baseline) is FROZEN per file below and may
* only go DOWN:
*
* - touching a file and reducing its count → update the baseline DOWN
* - adding a new off-grid value anywhere → this test fails
* - new CSS files must be born clean (no entry = zero tolerance)
*
* This is a ratchet, not an allowlist of specific lines, so refactors
* inside a file stay cheap while the global trend is monotonic.
*/

import { strict as assert } from 'node:assert';
import { readdir, readFile } from 'node:fs/promises';
import { join, relative, resolve } from 'node:path';
import { describe, it } from 'node:test';

const DESKTOP_ROOT = process.cwd().endsWith(join('apps', 'desktop'))
? process.cwd()
: resolve(process.cwd(), 'apps', 'desktop');
const RENDERER_ROOT = resolve(DESKTOP_ROOT, 'src', 'renderer');

/** Frozen per-file baseline (2026-07-03). Only decrease these numbers. */
const BASELINE: ReadonlyMap<string, number> = new Map([
['src/renderer/maka-tokens.css', 23],
['src/renderer/styles/chat-header.css', 26],
['src/renderer/styles/chat-message.css', 4],
['src/renderer/styles/composer.css', 12],
['src/renderer/styles/daily-review.css', 17],
['src/renderer/styles/health-center.css', 11],
['src/renderer/styles/module-pages.css', 76],
['src/renderer/styles/onboarding.css', 29],
['src/renderer/styles/permission-center.css', 28],
['src/renderer/styles/reasoning-panel.css', 3],
['src/renderer/styles/settings/bot.css', 23],
['src/renderer/styles/settings/connection.css', 9],
['src/renderer/styles/settings/form.css', 7],
['src/renderer/styles/settings/models.css', 31],
['src/renderer/styles/settings/nav-sidebar.css', 21],
['src/renderer/styles/settings/provider-editor.css', 20],
['src/renderer/styles/settings/theme-preview.css', 23],
['src/renderer/styles/sidebar.css', 36],
['src/renderer/styles/tool-output.css', 8],
['src/renderer/styles/tool-stream.css', 35],
]);

const DECL_RE =
/(?:^|;|\{)\s*(padding|gap|margin|row-gap|column-gap|padding-(?:top|right|bottom|left|inline|block)|margin-(?:top|right|bottom|left|inline|block))\s*:\s*([^;}]+)/gm;
const PX_RE = /(?<![\w.-])(\d+(?:\.\d+)?)px/g;

function stripComments(src: string): string {
return src.replace(/\/\*[\s\S]*?\*\//g, '');
}

function countOffGrid(css: string): number {
const stripped = stripComments(css);
let count = 0;
for (const decl of stripped.matchAll(DECL_RE)) {
for (const px of (decl[2] ?? '').matchAll(PX_RE)) {
const value = Number(px[1]);
if (value !== 0 && value % 4 !== 0 && value !== 1 && value !== 2) count += 1;
}
}
return count;
}

async function collectCssFiles(dir: string): Promise<string[]> {
const out: string[] = [];
for (const entry of await readdir(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) out.push(...(await collectCssFiles(full)));
else if (entry.name.endsWith('.css')) out.push(full);
}
return out;
}

describe('P-4PT spacing ratchet', () => {
it('padding/gap/margin px values stay on the 4pt grid (frozen legacy may only shrink)', async () => {
const files = await collectCssFiles(RENDERER_ROOT);
const failures: string[] = [];
for (const file of files.sort()) {
const rel = relative(DESKTOP_ROOT, file).split('\\').join('/');
const count = countOffGrid(await readFile(file, 'utf8'));
const allowed = BASELINE.get(rel) ?? 0;
if (count > allowed) {
failures.push(`${rel}: ${count} off-grid spacing values (baseline ${allowed})`);
}
}
assert.deepEqual(
failures,
[],
`off-grid spacing crept in — use 4/8/12/16/24/32 (1px/2px hairlines exempt), or shrink the file below its frozen baseline:\n${failures.join('\n')}`,
);
});

it('baseline entries stay honest (no stale higher-than-actual counts)', async () => {
// Guard against the ratchet rusting: if a file improves but the
// baseline is not updated, the slack could hide future regressions.
// Tolerate up to 3 slack per file before requiring a baseline update.
const stale: string[] = [];
for (const [rel, allowed] of BASELINE) {
const count = countOffGrid(await readFile(resolve(DESKTOP_ROOT, rel), 'utf8'));
if (allowed - count > 3) {
stale.push(`${rel}: baseline ${allowed} but actual ${count} — lower the baseline`);
}
}
assert.deepEqual(stale, [], stale.join('\n'));
});
});
9 changes: 5 additions & 4 deletions apps/desktop/src/renderer/maka-tokens.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,8 +126,11 @@
--foreground-5: color-mix(in oklch, var(--foreground) 5%, var(--background));
--foreground-8: color-mix(in oklch, var(--foreground) 8%, var(--background));
--foreground-10: color-mix(in oklch, var(--foreground) 10%, var(--background));
--foreground-20: color-mix(in oklch, var(--foreground) 20%, var(--background));
--foreground-30: color-mix(in oklch, var(--foreground) 30%, var(--background));
/* P-TEXT (roadmap §1.6): the -20/-30 text tiers were orphans (8 call
sites total) sitting between the surface washes (2..10) and the
real text ladder (40/50/60/70/80). Text uses moved to -40 (also a
contrast fix: 30% ink fails 4.5:1); decorative uses inlined their
color-mix. Fewer tiers = crisper hierarchy. */
--foreground-40: color-mix(in oklch, var(--foreground) 40%, var(--background));
--foreground-50: color-mix(in oklch, var(--foreground) 50%, var(--background));
--foreground-60: color-mix(in oklch, var(--foreground) 60%, var(--background));
Expand DownExpand Up@@ -681,8 +684,6 @@
--color-foreground-5: var(--foreground-5);
--color-foreground-8: var(--foreground-8);
--color-foreground-10: var(--foreground-10);
--color-foreground-20: var(--foreground-20);
--color-foreground-30: var(--foreground-30);
--color-foreground-40: var(--foreground-40);
--color-foreground-50: var(--foreground-50);
--color-foreground-60: var(--foreground-60);
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/onboarding.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -863,7 +863,7 @@
}
.maka-list-group-count {
font-variant-numeric: tabular-nums;
color: var(--foreground-30);
color: var(--foreground-40);
font-size: var(--font-size-caption);
font-weight: 500;
}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/settings/bot.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -114,7 +114,7 @@
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--foreground-30);
background: color-mix(in oklch, var(--foreground) 30%, var(--background));
box-shadow: 0 0 0 2px var(--background);
}
.settingsBotLogo[data-large="true"] .settingsBotLogoStatusDot {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/sidebar.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -165,7 +165,7 @@

.maka-resize-handle:hover::after,
.isResizingColumns .maka-resize-handle::after {
background: var(--foreground-20);
background: color-mix(in oklch, var(--foreground) 20%, var(--background));
}

/* Keyboard-focused separator: replace the suppressed native outline with a
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/styles/tool-output.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -376,7 +376,7 @@
.maka-composer-context-plus {
width: 26px;
height: 26px;
border: 1px solid var(--foreground-20);
border: 1px solid color-mix(in oklch, var(--foreground) 20%, var(--background));
border-radius: var(--radius-pill);
background: transparent;
}
Expand Down
8 changes: 6 additions & 2 deletions docs/design-refinement-roadmap-2026-07.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -112,8 +112,12 @@
4. **P-MOTION** ✅(已达标,无需动作):duration token
120/150/180/280ms 完全符合 Emil 表且取下限;装饰性入场动画已被
#406 gap 3 清除;剩余 keyframes 全部是功能性流式动画(D3 豁免)
5. **P-TEXT**:四档文字语义别名 + 新代码治理契约
6. **P-4PT**:4pt 治理契约(新增声明检查 + 存量 allowlist)
5. **P-TEXT** ✅(首轮):孤儿档 -20/-30 清除(8 处调用点:文字并入
-40 兼修对比度、装饰内联 color-mix);文字主力收敛为
40/50/60/70/80。全量四档语义别名迁移留待逐面触碰。
6. **P-4PT** ✅:ratchet 契约上线
(spacing-4pt-ratchet-contract.test.ts):442 个存量违例按文件冻结
只减不增,新文件零容忍,防 baseline 生锈的 slack 检查
7. **P-DARK**:dark elevation 独立化(依赖 P-SHADOW)
8. **P-STATE**:状态全周期补齐审计(skeleton 对形/empty 构图/inline error)

Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/primitives/chat.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -449,13 +449,13 @@ const toolVariants = cva("", {
// `.maka-tool-status-dot` (+ the `[data-status]` color swaps; running adds
// the box-shadow ring + `maka-tool-pulse` breath — keyframe stays in CSS).
dot:
"w-[8px] h-[8px] rounded-[var(--radius-pill)] bg-[var(--foreground-30)] [flex:0_0_auto]"
"w-[8px] h-[8px] rounded-[var(--radius-pill)] bg-[var(--foreground-40)] [flex:0_0_auto]"
// `waiting_permission` dot tint — see `WP_DOT_BG` above (String.raw).
+ " " + WP_DOT_BG
+ " data-[status=running]:bg-[var(--status-running)] data-[status=running]:[box-shadow:0_0_0_3px_oklch(from_var(--status-running)_l_c_h_/_0.15)] data-[status=running]:[animation:maka-tool-pulse_1.5s_ease-in-out_infinite]"
+ " data-[status=completed]:bg-[var(--success)]"
+ " data-[status=errored]:bg-[var(--destructive)]"
+ " data-[status=interrupted]:bg-[var(--foreground-30)]",
+ " data-[status=interrupted]:bg-[var(--foreground-40)]",
// `.maka-tool-name` — the mono tool name, ellipsized.
name:
"min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[color:var(--foreground)] font-medium [font-family:var(--font-mono)]",
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/stories/design-tokens.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ const foregroundScale = [
['foreground-2', '--foreground-2'],
['foreground-5', '--foreground-5'],
['foreground-10', '--foreground-10'],
['foreground-20', '--foreground-20'],
['foreground-50', '--foreground-50'],
['foreground-40', '--foreground-40'],
['foreground-60', '--foreground-60'],
['foreground-80', '--foreground-80'],
Expand Down