From 3776ad1ae475bc821caa49ba080a6264c821bb99 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sun, 21 Jun 2026 19:04:41 +0800 Subject: [PATCH 1/5] fix(desktop): align titlebar icon clusters to one vertical baseline The macOS window has a single titlebar row (traffic-light center), but the three top icon clusters each hardcoded their own vertical geometry in different containers, so the row jumped ~8px when the sidebar collapsed/ expanded and the left/right clusters never lined up. Add --maka-titlebar-control-safe-top (vertical companion to --maka-titlebar-control-safe-left, = traffic-light center) and --maka-detail-panel-inset-top, then anchor the sidebar header strip, the collapsed strip, and the workspace top-actions to that single baseline. The shared variable is the single source of truth, so the clusters can no longer drift apart. Also refresh the stale desktop-icon contract assertion to the actual PR #75 padded 1024x1024 icon (it still asserted the pre-#75 1254x1254 / >1.4MB edge-filled icon, failing on main). localized-main-shell-contract.test.ts now requires the shared-baseline vars instead of the old per-strip magic numbers. Claude-Session: https://claude.ai/code/session_01KtNufPddivogjCYaQijf5t --- .../localized-main-shell-contract.test.ts | 28 +++++++++++++--- apps/desktop/src/renderer/styles.css | 32 ++++++++++++++++--- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/main/__tests__/localized-main-shell-contract.test.ts b/apps/desktop/src/main/__tests__/localized-main-shell-contract.test.ts index 6df20f4f60..0fb469e4e7 100644 --- a/apps/desktop/src/main/__tests__/localized-main-shell-contract.test.ts +++ b/apps/desktop/src/main/__tests__/localized-main-shell-contract.test.ts @@ -478,7 +478,11 @@ describe('localized main shell contract', () => { assert.ok(workspaceTopActions, '.maka-workspace-top-actions rule must exist'); assert.ok(workspaceFeedbackAction, '.maka-workspace-feedback-action rule must exist'); assert.match(workspaceTopActions, /position:\s*absolute/); - assert.match(workspaceTopActions, /top:\s*11px/); + // PR-TITLEBAR-ROW-BASELINE: the right-side workspace top-actions + // share the single titlebar baseline with the two left strips, + // anchored on --maka-titlebar-control-safe-top minus the detail + // panel's own top inset — never a hardcoded vertical offset. + assert.match(workspaceTopActions, /top:\s*calc\(var\(--maka-titlebar-control-safe-top\) - var\(--maka-detail-panel-inset-top\) - 12px\)/); assert.match(workspaceTopActions, /right:\s*24px/); assert.match(workspaceTopActions, /gap:\s*6px/); assert.match(workspaceFeedbackAction, /font-size:\s*11px/); @@ -528,7 +532,17 @@ describe('localized main shell contract', () => { assert.ok(collapsedTopbar, '.maka-collapsed-drag-strip rule must exist'); assert.match(collapsedTopbar, /min-height:\s*38px/); assert.match(styles, /--maka-titlebar-control-safe-left:\s*94px/); - assert.match(collapsedTopbar, /padding:\s*8px 12px 0 var\(--maka-titlebar-control-safe-left\)/); + // PR-TITLEBAR-ROW-BASELINE: the macOS window has ONE physical + // titlebar row (traffic-light center). All three top icon clusters + // — sidebar header strip, collapsed strip, workspace top-actions — + // center their 24px icons on --maka-titlebar-control-safe-top so the + // row can't jump when the sidebar collapses/expands. Strips nested in + // the detail panel subtract --maka-detail-panel-inset-top (its 4px + // margin + 1px border). No cluster may reintroduce an independent + // vertical magic number. + assert.match(styles, /--maka-titlebar-control-safe-top:\s*20px/); + assert.match(styles, /--maka-detail-panel-inset-top:\s*5px/); + assert.match(collapsedTopbar, /padding:\s*calc\(var\(--maka-titlebar-control-safe-top\) - var\(--maka-detail-panel-inset-top\) - 12px\) 12px 0 var\(--maka-titlebar-control-safe-left\)/); assert.match(collapsedTopbar, /-webkit-app-region:\s*drag/); const collapsedTopbarButton = extractCssRule(styles, '.maka-collapsed-topbar-button'); assert.ok(collapsedTopbarButton, '.maka-collapsed-topbar-button rule must exist'); @@ -538,6 +552,10 @@ describe('localized main shell contract', () => { assert.match(sidebarTopBar, /justify-content:\s*space-between/); assert.match(sidebarTopBar, /box-sizing:\s*border-box/); assert.match(sidebarTopBar, /padding-left:\s*calc\(var\(--maka-titlebar-control-safe-left\) - 10px\)/); + // same shared vertical baseline as the collapsed strip + workspace + // top-actions; this strip sits in the sidebar header (6px pad), no + // detail-panel inset, so it subtracts the header pad inline. + assert.match(sidebarTopBar, /padding-top:\s*calc\(var\(--maka-titlebar-control-safe-top\) - 6px - 12px\)/); assert.match(styles, /(?:^|\n)\.maka-nav-icon\s*\{[\s\S]*?width:\s*18px[\s\S]*?height:\s*18px/); assert.match(styles, /\.maka-sidebar-modules\b/); assert.doesNotMatch(styles, /\.maka-sidebar-module-hint\b/); @@ -561,10 +579,10 @@ describe('localized main shell contract', () => { const icon = await stat(iconPath); const iconBuffer = await readFile(iconPath); - assert.ok(icon.size > 1_400_000, 'the edge-filled user-provided PNG icon must be present as the desktop app icon asset'); + assert.ok(icon.size > 700_000, 'the padded macOS-safe-area PNG icon (PR #75) must be present as the desktop app icon asset'); assert.equal(iconBuffer.toString('ascii', 1, 4), 'PNG', 'desktop app icon must remain a PNG asset'); - assert.equal(iconBuffer.readUInt32BE(16), 1254, 'desktop app icon width must match the supplied edge-filled icon'); - assert.equal(iconBuffer.readUInt32BE(20), 1254, 'desktop app icon height must match the supplied edge-filled icon'); + assert.equal(iconBuffer.readUInt32BE(16), 1024, 'desktop app icon width must match the supplied 1024² padded icon (PR #75)'); + assert.equal(iconBuffer.readUInt32BE(20), 1024, 'desktop app icon height must match the supplied 1024² padded icon (PR #75)'); assert.equal(iconBuffer[25], 6, 'desktop app icon must be RGBA so the rounded icon corners stay transparent'); assert.match(tokens, /--background:\s*oklch\(1\.000 0 0\);/); assert.match(tokens, /--surface-canvas:\s*oklch\(0\.935 0 0\);/); diff --git a/apps/desktop/src/renderer/styles.css b/apps/desktop/src/renderer/styles.css index 6dcc936d94..1cf486aa35 100644 --- a/apps/desktop/src/renderer/styles.css +++ b/apps/desktop/src/renderer/styles.css @@ -14,6 +14,20 @@ :root { --maka-titlebar-control-safe-left: 94px; + /* Vertical companion to --maka-titlebar-control-safe-left: the macOS + traffic-light vertical center in window space (main.ts + MAIN_WINDOW_TRAFFIC_LIGHT_POSITION.y = 14 + ~6px button radius). + The single baseline every titlebar-row icon cluster centers its + 24px icon on, so the sidebar header strip, the collapsed strip, and + the workspace top-actions share ONE row regardless of which panel + hosts them — collapsing/expanding the sidebar can't move the row. */ + --maka-titlebar-control-safe-top: 20px; + /* Top inset the detail panel adds over window space (its 4px margin + + 1px border). Titlebar strips nested inside .maka-panel-detail + subtract this so their window-space center still lands on + --maka-titlebar-control-safe-top. Keep in sync with the + .maka-panel-detail.maka-floating-panel margin/border. */ + --maka-detail-panel-inset-top: 5px; } @theme inline { @@ -253,9 +267,11 @@ button:active { .maka-collapsed-drag-strip { min-height: 38px; display: flex; - align-items: center; + align-items: flex-start; gap: 6px; - padding: 8px 12px 0 var(--maka-titlebar-control-safe-left); + /* Top padding lands the 24px icon's vertical center on the shared + titlebar baseline, minus this strip's own detail-panel inset. */ + padding: calc(var(--maka-titlebar-control-safe-top) - var(--maka-detail-panel-inset-top) - 12px) 12px 0 var(--maka-titlebar-control-safe-left); -webkit-app-region: drag; } @@ -364,7 +380,10 @@ button:active { -webkit-app-region: no-drag; position: absolute; z-index: 4; - top: 11px; + /* Shared titlebar baseline (see --maka-titlebar-control-safe-top). + Absolutely positioned within the detail panel, so it subtracts the + same detail-panel inset as the collapsed strip. */ + top: calc(var(--maka-titlebar-control-safe-top) - var(--maka-detail-panel-inset-top) - 12px); right: 24px; display: inline-flex; align-items: center; @@ -640,10 +659,15 @@ button:active { .maka-sidebar-drag-strip { min-height: 28px; display: flex; - align-items: center; + align-items: flex-start; justify-content: space-between; gap: 6px; box-sizing: border-box; + /* Same shared titlebar baseline as the collapsed strip + workspace + top-actions. This strip sits in the sidebar header (its 6px + padding-top), with no detail-panel inset, so it subtracts the + header pad inline instead of --maka-detail-panel-inset-top. */ + padding-top: calc(var(--maka-titlebar-control-safe-top) - 6px - 12px); padding-left: calc(var(--maka-titlebar-control-safe-left) - 10px); -webkit-app-region: drag; } From 7c2eb6338e5bef99f62fe40b6ae3486fb4347bc3 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sun, 21 Jun 2026 19:16:42 +0800 Subject: [PATCH 2/5] fix(desktop): adopt native cursor convention (arrow on controls, hand on links) Native macOS/Windows reserve the pointing-hand cursor for hyperlinks; buttons, rows, tabs, toggles, and other controls use the default arrow. The renderer had drifted: ~66 controls set `cursor: pointer` ad hoc while the titlebar icons kept the arrow, so hover was inconsistent (the sidebar showed a hand, the titlebar an arrow). Remove `cursor: pointer` from every control in styles.css (59) and maka-tokens.css (7); keep it only on .maka-markdown-link-internal (a link-styled in-app nav button). External links stay with the UA pointer; the broken/non-clickable variant has none. Matches Apple HIG for the macOS-native feel: hand = link, arrow = control. Claude-Session: https://claude.ai/code/session_01KtNufPddivogjCYaQijf5t --- apps/desktop/src/renderer/maka-tokens.css | 7 --- apps/desktop/src/renderer/styles.css | 59 ----------------------- 2 files changed, 66 deletions(-) diff --git a/apps/desktop/src/renderer/maka-tokens.css b/apps/desktop/src/renderer/maka-tokens.css index 72334f90e3..4f07e926e6 100644 --- a/apps/desktop/src/renderer/maka-tokens.css +++ b/apps/desktop/src/renderer/maka-tokens.css @@ -763,7 +763,6 @@ padding: 6px 10px; border-radius: var(--radius-button); color: var(--foreground-80); - cursor: pointer; font-size: 14px; line-height: 1.4; user-select: none; @@ -790,7 +789,6 @@ padding: 4px 8px; border-radius: var(--radius-button); color: var(--foreground-60); - cursor: pointer; font-size: 13px; } .maka-sidebar-button:hover { background: var(--hover); color: var(--foreground); } @@ -948,7 +946,6 @@ display: flex; align-items: center; gap: 8px; - cursor: pointer; font-weight: 600; color: var(--accent); letter-spacing: 0.02em; @@ -1158,7 +1155,6 @@ border-radius: 7px; background: transparent; color: var(--foreground-50); - cursor: pointer; transition: background 120ms ease, color 120ms ease, box-shadow 120ms ease; } .maka-code-block-copy:hover { @@ -1315,7 +1311,6 @@ border-radius: 7px; background: var(--background); color: var(--foreground-60); - cursor: pointer; opacity: 0; /* PR-CHAT-COPY-BUTTON-POLISH-0 (WAWQAQ 10min loop): the copy button used to fade in on hover but never moved — felt like @@ -1424,7 +1419,6 @@ .maka-tool { padding: 0; } .maka-tool > summary { list-style: none; - cursor: pointer; padding: 8px 12px; } .maka-tool > summary::-webkit-details-marker { display: none; } @@ -1670,7 +1664,6 @@ font-size: 13px; font-weight: 500; line-height: 1.2; - cursor: pointer; transition: background 120ms ease, border-color 120ms ease, color 120ms ease, transform 120ms ease, box-shadow 120ms ease; } .maka-button:hover { background: var(--hover); } diff --git a/apps/desktop/src/renderer/styles.css b/apps/desktop/src/renderer/styles.css index 1cf486aa35..b6cd991f1c 100644 --- a/apps/desktop/src/renderer/styles.css +++ b/apps/desktop/src/renderer/styles.css @@ -465,7 +465,6 @@ button:active { border: 0; background: transparent; color: var(--foreground-60); - cursor: pointer; font-size: 16px; line-height: 1; border-radius: 6px; @@ -589,7 +588,6 @@ button:active { background: transparent; color: var(--foreground); padding: 7px 10px; - cursor: pointer; transition: background 150ms ease-out; } .maka-search-modal-result:hover:not([disabled]) { @@ -683,7 +681,6 @@ button:active { border-radius: 6px; background: transparent; color: var(--foreground-55); - cursor: pointer; transition: background 150ms ease-out, color 150ms ease-out, box-shadow 150ms ease-out; } @@ -791,7 +788,6 @@ button:active { border-radius: 8px; background: transparent; color: var(--foreground); - cursor: pointer; padding: 3px 0; text-align: left; } @@ -848,7 +844,6 @@ button:active { border-radius: 6px; background: transparent; color: var(--foreground-55); - cursor: pointer; } /* PR-UX-POLISH-1 commit 4 (WAWQAQ `e0dbad11` + kenji `2844f64f`): @@ -899,7 +894,6 @@ button:active { background: transparent; box-shadow: none; color: var(--foreground-75); - cursor: pointer; font-size: 12px; font-weight: 500; text-align: left; @@ -980,7 +974,6 @@ button:active { border-radius: 6px; background: transparent; color: var(--foreground-80); - cursor: pointer; -webkit-app-region: no-drag; padding: 3px 6px; text-align: left; @@ -1255,7 +1248,6 @@ button:active { border: 0; background: transparent; color: inherit; - cursor: pointer; padding: 3px 8px 3px 10px; text-align: left; } @@ -1346,7 +1338,6 @@ button:active { border-radius: 8px; background: transparent; color: var(--foreground-60); - cursor: pointer; transition: background 120ms ease, color 120ms ease, box-shadow 120ms ease; } @@ -1456,7 +1447,6 @@ button:active { border-radius: 10px; background: transparent; color: var(--foreground); - cursor: pointer; padding: 10px 12px; text-align: left; transition: @@ -1728,7 +1718,6 @@ button:active { border-radius: 6px; background: transparent; color: var(--foreground); - cursor: pointer; text-align: left; transition: background 150ms ease-out; } @@ -1937,7 +1926,6 @@ button:active { border: 0; background: transparent; color: var(--foreground-60); - cursor: pointer; font-size: 12px; text-decoration: none; padding: 5px 8px; @@ -2434,7 +2422,6 @@ button:active { padding: 0; background: transparent; border: 0; - cursor: pointer; text-align: left; font: inherit; color: inherit; @@ -2948,7 +2935,6 @@ button:active { border-radius: 4px; background: transparent; color: var(--foreground-60); - cursor: pointer; } .maka-plan-card-menu-trigger:hover { @@ -3562,7 +3548,6 @@ button:active { color: var(--foreground); white-space: normal !important; text-align: left; - cursor: pointer; transition: background-color 150ms ease-out, transform 180ms cubic-bezier(0.16, 1, 0.3, 1); @@ -3807,7 +3792,6 @@ button:active { border-radius: 6px; background: transparent; color: var(--foreground-50); - cursor: pointer; -webkit-app-region: no-drag; transition: background 120ms ease, color 120ms ease, box-shadow 120ms ease; } @@ -3848,7 +3832,6 @@ button:active { padding: 1px 6px; font-size: 11px; font-weight: 600; - cursor: pointer; -webkit-app-region: no-drag; margin-right: 4px; margin-bottom: 3px; @@ -3926,7 +3909,6 @@ button:active { color: var(--foreground); font-size: 11px; box-shadow: none; - cursor: pointer; } .maka-model-switcher-trigger:hover:not(:disabled):not([data-disabled]) { @@ -4023,7 +4005,6 @@ button:active { white-space: nowrap; flex: 0 0 auto; -webkit-app-region: no-drag; - cursor: pointer; transition: transform 0.12s var(--ease-out-strong); } .maka-chat-header-alert:hover { @@ -4129,7 +4110,6 @@ button:active { border-radius: 999px; background: transparent; color: var(--foreground-60); - cursor: pointer; padding: 3px 11px; font-size: 12px; font-weight: 500; @@ -4247,7 +4227,6 @@ button:active { border-radius: 999px; background: var(--background); color: var(--foreground-70); - cursor: pointer; box-shadow: var(--shadow-medium); opacity: 1; transition: @@ -4483,7 +4462,6 @@ button:active { border-radius: 5px; background: var(--background); color: var(--accent); - cursor: pointer; padding: 2px 7px; font-size: 10px; font-weight: 600; @@ -4503,7 +4481,6 @@ button:active { border-radius: 4px; background: transparent; color: var(--foreground-50); - cursor: pointer; transition: background-color 150ms ease-out, color 150ms ease-out; } @@ -4704,7 +4681,6 @@ button:active { border-radius: 6px; background: transparent; color: var(--foreground); - cursor: pointer; padding: 4px 8px; text-align: left; font-size: 11px; @@ -5727,7 +5703,6 @@ button:active { border-radius: 6px; background: transparent; color: var(--foreground-60); - cursor: pointer; transition: background-color 150ms ease-out, color 150ms ease-out, @@ -5827,7 +5802,6 @@ button:active { border-radius: 6px; background: transparent; color: var(--foreground-55); - cursor: pointer; font: inherit; font-size: 12px; line-height: 1.25; @@ -6141,7 +6115,6 @@ button:active { } .maka-permission-raw > summary { - cursor: pointer; list-style: none; padding: 5px 10px; color: var(--foreground-60); @@ -6417,7 +6390,6 @@ button:active { border-radius: 6px; background: transparent; color: var(--foreground-70); - cursor: pointer; padding: 3px 6px; text-align: left; } @@ -6814,7 +6786,6 @@ button:active { border: 0; border-radius: 999px; background: var(--foreground-20); - cursor: pointer; padding: 2px; /* PR-UI-LAYOUT-35: smooth bg transition + focus ring (was * missing — keyboard users couldn't see the switch state @@ -7071,7 +7042,6 @@ button:active { border-radius: 8px; background: var(--background-elevated); color: var(--foreground); - cursor: pointer; overflow: hidden; padding: 8px; text-align: left; @@ -7266,7 +7236,6 @@ button:active { border-radius: 4px; background: transparent; color: var(--foreground-50); - cursor: pointer; transition: background 150ms ease-out, color 150ms ease-out; } .settingsPasswordToggle:hover { @@ -7299,7 +7268,6 @@ button:active { color: var(--accent); font-size: 13px; font-weight: 500; - cursor: pointer; transition: background 120ms ease, border-color 120ms ease, color 120ms ease; } .settingsBotAction:hover { @@ -7435,7 +7403,6 @@ button:active { border: 0; background: transparent; color: var(--foreground-55); - cursor: pointer; padding: 4px 0; font-size: 12px; text-decoration: underline; @@ -7506,7 +7473,6 @@ button:active { border-radius: 8px; background: transparent; color: var(--foreground-55); - cursor: pointer; } .settingsWechatQrClose:hover { @@ -7590,7 +7556,6 @@ button:active { border-radius: 8px; background: var(--background); color: var(--accent); - cursor: pointer; padding: 0 12px; font-size: 12.5px; font-weight: 500; @@ -7723,7 +7688,6 @@ button:active { border-radius: 12px; background: transparent; color: var(--foreground-70); - cursor: pointer; padding: 7px 10px; text-align: left; } @@ -7998,7 +7962,6 @@ button:active { border-radius: 999px; background: transparent; color: var(--foreground-60); - cursor: pointer; padding: 0 8px; font-size: 11px; } @@ -8101,7 +8064,6 @@ button:active { border-radius: 6px; background: transparent; color: var(--foreground-50); - cursor: pointer; line-height: 1; } @@ -8150,7 +8112,6 @@ button:active { border-radius: 6px; background: transparent; color: var(--foreground-70); - cursor: pointer; padding: 4px 8px; text-align: left; } @@ -8542,7 +8503,6 @@ button:active { background: transparent; color: var(--accent); font: inherit; - cursor: pointer; } .settingsInlineTextButton:hover { @@ -8877,7 +8837,6 @@ button:active { color: var(--foreground); padding: 6px 8px; text-align: left; - cursor: pointer; } .catalogTab:hover { @@ -8921,7 +8880,6 @@ button:active { color: var(--foreground); padding: 10px; text-align: left; - cursor: pointer; transition: border-color 150ms ease-out, background 150ms ease-out, @@ -9261,7 +9219,6 @@ button:active { color: var(--foreground); font-size: 11px; line-height: 1.5; - cursor: pointer; width: 100%; } @@ -9298,7 +9255,6 @@ button:active { background: transparent; color: var(--foreground); text-align: left; - cursor: pointer; transition: background 150ms ease-out, border-color 150ms ease-out, box-shadow 150ms ease-out; } @@ -9553,7 +9509,6 @@ button:active { color: var(--foreground); padding: 10px 12px; text-align: left; - cursor: pointer; } .enabledProviderChip { @@ -9904,7 +9859,6 @@ button:active { border-radius: 999px; background: transparent; color: var(--accent); - cursor: pointer; padding: 0 14px; font-size: 12px; font-weight: 700; @@ -9945,7 +9899,6 @@ button:active { border-radius: 999px; background: var(--settings-surface-muted); color: var(--text-muted); - cursor: pointer; } .providerConfigSheetClose:hover { @@ -10093,7 +10046,6 @@ button:active { background: transparent; color: var(--foreground-70); border-radius: 6px; - cursor: pointer; padding: 0; transition: background 120ms ease, color 120ms ease, box-shadow 120ms ease; } @@ -10169,7 +10121,6 @@ button:active { background: var(--background); color: var(--foreground); font-size: 12px; - cursor: pointer; } .maka-artifact-error-retry:hover { @@ -10220,7 +10171,6 @@ button:active { border-radius: 6px; background: transparent; color: var(--foreground); - cursor: pointer; text-align: left; font-size: 11px; } @@ -10509,7 +10459,6 @@ button:active { border-radius: 6px; padding: 4px 8px; font-size: 11.5px; - cursor: pointer; } .maka-artifact-toolbar-button:hover { @@ -10631,7 +10580,6 @@ button:active { background: transparent; color: var(--foreground-70); font-size: 12px; - cursor: pointer; transition: background 120ms ease, color 120ms ease, border-color 120ms ease; } .maka-turn-footer-action:hover:not(:disabled) { @@ -10761,7 +10709,6 @@ button:active { background: var(--foreground-5); color: var(--foreground-70); font-size: 10px; - cursor: pointer; transition: background 150ms ease-out, color 150ms ease-out; } .maka-turn-lineage-badge:hover { @@ -10803,7 +10750,6 @@ button:active { color: var(--info-text); font-size: 11px; line-height: 1.4; - cursor: pointer; transition: background 150ms ease-out, color 150ms ease-out, border-color 150ms ease-out; align-self: flex-start; } @@ -10865,7 +10811,6 @@ button:active { .maka-reasoning-panel-header { list-style: none; - cursor: pointer; display: flex; align-items: center; gap: 6px; @@ -11091,7 +11036,6 @@ button:active { padding: 4px 10px; color: var(--foreground-80); font-size: 0.8125rem; - cursor: pointer; transition: background 120ms ease, border-color 120ms ease, color 120ms ease; } .settingsPermissionRefresh:hover { @@ -11502,7 +11446,6 @@ button:active { padding: 4px 10px; color: var(--foreground-80); font-size: 0.8125rem; - cursor: pointer; transition: background 120ms ease, border-color 120ms ease, color 120ms ease; } .settingsHealthRefresh:hover { @@ -12597,7 +12540,6 @@ button:active { gap: 10px; width: 100%; text-align: left; - cursor: pointer; color: var(--foreground); } @@ -12839,7 +12781,6 @@ button:active { color: var(--foreground); font: inherit; text-align: left; - cursor: pointer; } .maka-daily-review-session-button:disabled { From 4b381d1a02657dfc5134a491eebb6e81f47b6ade Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sun, 21 Jun 2026 19:33:25 +0800 Subject: [PATCH 3/5] test(desktop): guard the native cursor convention against drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cursor sweep removed ~66 ad-hoc `cursor: pointer` from controls but left no guard — the original inconsistency came from exactly this kind of unwatched drift. Add a static-analysis contract: every `cursor: pointer` must sit on a link selector (name contains `link`); controls may not reintroduce the hand. Mirrors the app-region-hygiene contract style. The live look-and-feel (arrow on controls, hand on links) is verified in a real Electron window. Claude-Session: https://claude.ai/code/session_01KtNufPddivogjCYaQijf5t --- .../cursor-convention-contract.test.ts | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 apps/desktop/src/main/__tests__/cursor-convention-contract.test.ts diff --git a/apps/desktop/src/main/__tests__/cursor-convention-contract.test.ts b/apps/desktop/src/main/__tests__/cursor-convention-contract.test.ts new file mode 100644 index 0000000000..069c071e09 --- /dev/null +++ b/apps/desktop/src/main/__tests__/cursor-convention-contract.test.ts @@ -0,0 +1,98 @@ +/** + * Static-analysis contract for the native cursor convention. + * + * Native macOS / Windows reserve the pointing-hand cursor (`cursor: pointer`) + * for hyperlinks; buttons, rows, tabs, toggles and other controls use the + * default arrow. The renderer had drifted into ~66 controls setting + * `cursor: pointer` ad hoc, so hover was inconsistent (sidebar showed a hand, + * titlebar an arrow). + * + * This gate keeps the convention from drifting back: every `cursor: pointer` + * declaration must sit on a LINK selector (its name contains `link`). A new + * genuine link may join by being named `*link*`; a control may not reintroduce + * the hand. The runtime look-and-feel (which element shows which cursor) is + * still verified in a real window — this is the source bound. + */ + +import { strict as assert } from 'node:assert'; +import { readFile } from 'node:fs/promises'; +import { describe, it } from 'node:test'; +import { join } from 'node:path'; + +const STYLES_PATH = join(process.cwd(), 'src', 'renderer', 'styles.css'); +const TOKENS_PATH = join(process.cwd(), 'src', 'renderer', 'maka-tokens.css'); + +/** A selector is allowed to carry the hand cursor only if it is a link. */ +const LINK_SELECTOR = /link/; + +describe('native cursor convention contract', () => { + for (const path of [STYLES_PATH, TOKENS_PATH]) { + it(`${path.split('/').slice(-1)[0]}: cursor:pointer lives only on link selectors`, async () => { + const css = await readFile(path, 'utf8'); + const offenders: string[] = []; + for (const rule of iterateRules(css)) { + if (!/cursor:\s*pointer/.test(rule.body)) continue; + if (!LINK_SELECTOR.test(rule.selector)) offenders.push(rule.selector); + } + assert.deepEqual( + offenders, + [], + `\`cursor: pointer\` (the link/hand cursor) must not sit on control selectors. Native macOS reserves the hand for links; controls use the default arrow. Offending selectors in ${path}:\n ${offenders.join('\n ')}\nIf one is genuinely a link, name it \`*link*\` so it joins the allowlist; otherwise drop the declaration (the arrow is the default).`, + ); + }); + } + + it('the internal markdown link keeps the hand cursor', async () => { + const css = await readFile(STYLES_PATH, 'utf8'); + const rule = findRule(css, '.maka-markdown-link-internal'); + assert.ok(rule, '.maka-markdown-link-internal rule must exist'); + assert.match( + rule!, + /cursor:\s*pointer/, + 'the link-styled in-app nav button must keep the hand cursor — it presents as a link', + ); + }); +}); + +/** Return the body of the first flat rule whose selector list includes `target`. */ +function findRule(css: string, target: string): string | null { + for (const rule of iterateRules(css)) { + if (rule.selector.split(',').some((s) => s.trim() === target)) { + return `${rule.selector} { ${rule.body.trim()} }`; + } + } + return null; +} + +/** Yield each flat `selector { body }` rule, recursing into at-rules. */ +function* iterateRules(css: string): Generator<{ selector: string; body: string }> { + let i = 0; + while (i < css.length) { + while (i < css.length && /\s/.test(css[i]!)) i++; + if (css.startsWith('/*', i)) { + const end = css.indexOf('*/', i + 2); + if (end === -1) return; + i = end + 2; + continue; + } + const braceIdx = css.indexOf('{', i); + if (braceIdx === -1) return; + const selector = css.slice(i, braceIdx).trim(); + let depth = 1; + let j = braceIdx + 1; + while (j < css.length && depth > 0) { + const ch = css[j]; + if (ch === '{') depth++; + else if (ch === '}') depth--; + j++; + } + if (depth !== 0) return; + const body = css.slice(braceIdx + 1, j - 1); + if (selector.startsWith('@')) { + yield* iterateRules(body); + } else { + yield { selector, body }; + } + i = j; + } +} From cf7e09573d9cc6e68371000ffd37d66168026252 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sun, 21 Jun 2026 20:00:12 +0800 Subject: [PATCH 4/5] refactor(desktop): collapse titlebar baseline to one source-of-truth var MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review (P3) flagged the titlebar baseline as over-engineered: a second explanatory variable (--maka-detail-panel-inset-top) plus per-cluster calc algebra (`- 5px - 12px` / `- 6px - 12px`), with the contract test pinning the full calc strings. That widens the drift surface the single baseline was meant to remove. - Drop --maka-detail-panel-inset-top; keep only the one baseline --maka-titlebar-control-safe-top. Each of the three clusters now references that baseline minus a single per-container correction constant (17/17/18px), documented inline. - Loosen the contract assertions from the exact calc string to the invariant that matters: each cluster references var(--maka-titlebar-control-safe-top). The correction constant is geometry, free to tune without a test edit. No behavioral change — all three clusters still center at y=20px. Full desktop suite green (1529). Claude-Session: https://claude.ai/code/session_01KtNufPddivogjCYaQijf5t --- .../localized-main-shell-contract.test.ts | 28 ++++++------- apps/desktop/src/renderer/styles.css | 40 +++++++------------ 2 files changed, 29 insertions(+), 39 deletions(-) diff --git a/apps/desktop/src/main/__tests__/localized-main-shell-contract.test.ts b/apps/desktop/src/main/__tests__/localized-main-shell-contract.test.ts index 0fb469e4e7..9d3c330aaf 100644 --- a/apps/desktop/src/main/__tests__/localized-main-shell-contract.test.ts +++ b/apps/desktop/src/main/__tests__/localized-main-shell-contract.test.ts @@ -479,10 +479,12 @@ describe('localized main shell contract', () => { assert.ok(workspaceFeedbackAction, '.maka-workspace-feedback-action rule must exist'); assert.match(workspaceTopActions, /position:\s*absolute/); // PR-TITLEBAR-ROW-BASELINE: the right-side workspace top-actions - // share the single titlebar baseline with the two left strips, - // anchored on --maka-titlebar-control-safe-top minus the detail - // panel's own top inset — never a hardcoded vertical offset. - assert.match(workspaceTopActions, /top:\s*calc\(var\(--maka-titlebar-control-safe-top\) - var\(--maka-detail-panel-inset-top\) - 12px\)/); + // share the single titlebar baseline with the two left strips — + // anchored on var(--maka-titlebar-control-safe-top), never a + // hardcoded vertical offset. We assert the baseline reference, not + // the per-container correction constant (geometry, free to tune): + // the invariant is that all three clusters key off the one baseline. + assert.match(workspaceTopActions, /top:\s*calc\(var\(--maka-titlebar-control-safe-top\) - \d+px\)/); assert.match(workspaceTopActions, /right:\s*24px/); assert.match(workspaceTopActions, /gap:\s*6px/); assert.match(workspaceFeedbackAction, /font-size:\s*11px/); @@ -535,14 +537,12 @@ describe('localized main shell contract', () => { // PR-TITLEBAR-ROW-BASELINE: the macOS window has ONE physical // titlebar row (traffic-light center). All three top icon clusters // — sidebar header strip, collapsed strip, workspace top-actions — - // center their 24px icons on --maka-titlebar-control-safe-top so the - // row can't jump when the sidebar collapses/expands. Strips nested in - // the detail panel subtract --maka-detail-panel-inset-top (its 4px - // margin + 1px border). No cluster may reintroduce an independent - // vertical magic number. + // center their 24px icons on the single --maka-titlebar-control-safe-top + // baseline so the row can't jump when the sidebar collapses/expands. + // Each cluster applies its own small per-container correction; no + // cluster may reintroduce an independent vertical baseline. assert.match(styles, /--maka-titlebar-control-safe-top:\s*20px/); - assert.match(styles, /--maka-detail-panel-inset-top:\s*5px/); - assert.match(collapsedTopbar, /padding:\s*calc\(var\(--maka-titlebar-control-safe-top\) - var\(--maka-detail-panel-inset-top\) - 12px\) 12px 0 var\(--maka-titlebar-control-safe-left\)/); + assert.match(collapsedTopbar, /padding:\s*calc\(var\(--maka-titlebar-control-safe-top\) - \d+px\) 12px 0 var\(--maka-titlebar-control-safe-left\)/); assert.match(collapsedTopbar, /-webkit-app-region:\s*drag/); const collapsedTopbarButton = extractCssRule(styles, '.maka-collapsed-topbar-button'); assert.ok(collapsedTopbarButton, '.maka-collapsed-topbar-button rule must exist'); @@ -553,9 +553,9 @@ describe('localized main shell contract', () => { assert.match(sidebarTopBar, /box-sizing:\s*border-box/); assert.match(sidebarTopBar, /padding-left:\s*calc\(var\(--maka-titlebar-control-safe-left\) - 10px\)/); // same shared vertical baseline as the collapsed strip + workspace - // top-actions; this strip sits in the sidebar header (6px pad), no - // detail-panel inset, so it subtracts the header pad inline. - assert.match(sidebarTopBar, /padding-top:\s*calc\(var\(--maka-titlebar-control-safe-top\) - 6px - 12px\)/); + // top-actions — references the one var(--maka-titlebar-control-safe-top), + // with its own per-container correction constant (not asserted). + assert.match(sidebarTopBar, /padding-top:\s*calc\(var\(--maka-titlebar-control-safe-top\) - \d+px\)/); assert.match(styles, /(?:^|\n)\.maka-nav-icon\s*\{[\s\S]*?width:\s*18px[\s\S]*?height:\s*18px/); assert.match(styles, /\.maka-sidebar-modules\b/); assert.doesNotMatch(styles, /\.maka-sidebar-module-hint\b/); diff --git a/apps/desktop/src/renderer/styles.css b/apps/desktop/src/renderer/styles.css index b6cd991f1c..7ee38d6e78 100644 --- a/apps/desktop/src/renderer/styles.css +++ b/apps/desktop/src/renderer/styles.css @@ -14,20 +14,13 @@ :root { --maka-titlebar-control-safe-left: 94px; - /* Vertical companion to --maka-titlebar-control-safe-left: the macOS - traffic-light vertical center in window space (main.ts - MAIN_WINDOW_TRAFFIC_LIGHT_POSITION.y = 14 + ~6px button radius). - The single baseline every titlebar-row icon cluster centers its - 24px icon on, so the sidebar header strip, the collapsed strip, and - the workspace top-actions share ONE row regardless of which panel - hosts them — collapsing/expanding the sidebar can't move the row. */ + /* The single titlebar baseline: the macOS traffic-light vertical center + in window space (main.ts MAIN_WINDOW_TRAFFIC_LIGHT_POSITION.y + button + radius). Every titlebar-row icon cluster anchors to this, so the + sidebar header strip, the collapsed strip, and the workspace + top-actions share ONE row — collapsing/expanding the sidebar can't + move it. Each cluster subtracts a small per-container correction. */ --maka-titlebar-control-safe-top: 20px; - /* Top inset the detail panel adds over window space (its 4px margin + - 1px border). Titlebar strips nested inside .maka-panel-detail - subtract this so their window-space center still lands on - --maka-titlebar-control-safe-top. Keep in sync with the - .maka-panel-detail.maka-floating-panel margin/border. */ - --maka-detail-panel-inset-top: 5px; } @theme inline { @@ -269,9 +262,9 @@ button:active { display: flex; align-items: flex-start; gap: 6px; - /* Top padding lands the 24px icon's vertical center on the shared - titlebar baseline, minus this strip's own detail-panel inset. */ - padding: calc(var(--maka-titlebar-control-safe-top) - var(--maka-detail-panel-inset-top) - 12px) 12px 0 var(--maka-titlebar-control-safe-left); + /* Centers the 24px icon on the shared titlebar baseline. Correction = + 12px half-icon + 5px detail-panel inset. */ + padding: calc(var(--maka-titlebar-control-safe-top) - 17px) 12px 0 var(--maka-titlebar-control-safe-left); -webkit-app-region: drag; } @@ -380,10 +373,9 @@ button:active { -webkit-app-region: no-drag; position: absolute; z-index: 4; - /* Shared titlebar baseline (see --maka-titlebar-control-safe-top). - Absolutely positioned within the detail panel, so it subtracts the - same detail-panel inset as the collapsed strip. */ - top: calc(var(--maka-titlebar-control-safe-top) - var(--maka-detail-panel-inset-top) - 12px); + /* Shared titlebar baseline; same 17px correction as the collapsed strip + (12px half-icon + 5px detail-panel inset). */ + top: calc(var(--maka-titlebar-control-safe-top) - 17px); right: 24px; display: inline-flex; align-items: center; @@ -661,11 +653,9 @@ button:active { justify-content: space-between; gap: 6px; box-sizing: border-box; - /* Same shared titlebar baseline as the collapsed strip + workspace - top-actions. This strip sits in the sidebar header (its 6px - padding-top), with no detail-panel inset, so it subtracts the - header pad inline instead of --maka-detail-panel-inset-top. */ - padding-top: calc(var(--maka-titlebar-control-safe-top) - 6px - 12px); + /* Same shared titlebar baseline; 18px correction = 12px half-icon + + 6px sidebar-header pad (no detail-panel inset here). */ + padding-top: calc(var(--maka-titlebar-control-safe-top) - 18px); padding-left: calc(var(--maka-titlebar-control-safe-left) - 10px); -webkit-app-region: drag; } From 00d8729298539938e3688adeb433b6d48b1f9da2 Mon Sep 17 00:00:00 2001 From: Yuhan Lei Date: Sun, 21 Jun 2026 21:34:45 +0800 Subject: [PATCH 5/5] refactor(desktop): tighten cursor-convention allowlist, trim titlebar comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address two non-blocking review notes on the titlebar/cursor PR: - cursor-convention-contract: replace the `/link/` name match — which let any selector whose name merely contains "link" (e.g. `.fake-link-button`) reintroduce `cursor: pointer` — with an exact allowlist. Only `.maka-markdown-link-internal` may carry the hand cursor; maka-tokens.css must carry none. Drop the 98-line hand-rolled depth parser for a small regex that walks each `cursor: pointer` back to its selector (still catches at-rule nesting). - styles.css: keep the `--maka-titlebar-control-safe-top` baseline and the three `calc(...)` placements, but compress their multi-line explanatory comments to one line each. No new Layout.tsx / module. Verified: `npm -w @maka/desktop test` (1528 pass); an injected `.fake-link-button { cursor: pointer }` now fails the gate while `.maka-markdown-link-internal` passes. Claude-Session: https://claude.ai/code/session_01VWXzHoyncpGYuwbaDEDizD --- .../cursor-convention-contract.test.ts | 113 ++++++------------ apps/desktop/src/renderer/styles.css | 17 +-- 2 files changed, 41 insertions(+), 89 deletions(-) diff --git a/apps/desktop/src/main/__tests__/cursor-convention-contract.test.ts b/apps/desktop/src/main/__tests__/cursor-convention-contract.test.ts index 069c071e09..ff3e7a78f7 100644 --- a/apps/desktop/src/main/__tests__/cursor-convention-contract.test.ts +++ b/apps/desktop/src/main/__tests__/cursor-convention-contract.test.ts @@ -2,16 +2,10 @@ * Static-analysis contract for the native cursor convention. * * Native macOS / Windows reserve the pointing-hand cursor (`cursor: pointer`) - * for hyperlinks; buttons, rows, tabs, toggles and other controls use the - * default arrow. The renderer had drifted into ~66 controls setting - * `cursor: pointer` ad hoc, so hover was inconsistent (sidebar showed a hand, - * titlebar an arrow). - * - * This gate keeps the convention from drifting back: every `cursor: pointer` - * declaration must sit on a LINK selector (its name contains `link`). A new - * genuine link may join by being named `*link*`; a control may not reintroduce - * the hand. The runtime look-and-feel (which element shows which cursor) is - * still verified in a real window — this is the source bound. + * for hyperlinks; every other control uses the default arrow. Only the + * link-styled in-app nav button (`.maka-markdown-link-internal`) may carry it. + * The runtime look-and-feel (which element shows which cursor) is still + * verified in a real window — this is the source bound. */ import { strict as assert } from 'node:assert'; @@ -22,77 +16,42 @@ import { join } from 'node:path'; const STYLES_PATH = join(process.cwd(), 'src', 'renderer', 'styles.css'); const TOKENS_PATH = join(process.cwd(), 'src', 'renderer', 'maka-tokens.css'); -/** A selector is allowed to carry the hand cursor only if it is a link. */ -const LINK_SELECTOR = /link/; +/** Only the link-styled in-app nav button may carry the hand cursor. */ +const CURSOR_POINTER_ALLOWLIST = ['.maka-markdown-link-internal']; -describe('native cursor convention contract', () => { - for (const path of [STYLES_PATH, TOKENS_PATH]) { - it(`${path.split('/').slice(-1)[0]}: cursor:pointer lives only on link selectors`, async () => { - const css = await readFile(path, 'utf8'); - const offenders: string[] = []; - for (const rule of iterateRules(css)) { - if (!/cursor:\s*pointer/.test(rule.body)) continue; - if (!LINK_SELECTOR.test(rule.selector)) offenders.push(rule.selector); - } - assert.deepEqual( - offenders, - [], - `\`cursor: pointer\` (the link/hand cursor) must not sit on control selectors. Native macOS reserves the hand for links; controls use the default arrow. Offending selectors in ${path}:\n ${offenders.join('\n ')}\nIf one is genuinely a link, name it \`*link*\` so it joins the allowlist; otherwise drop the declaration (the arrow is the default).`, - ); - }); +/** + * Selectors of every rule that declares `cursor: pointer`, sorted. Comments are + * stripped first; each hit walks back to the `{` that opens its rule and takes + * the selector after the previous block boundary. Depth is ignored, so a + * `cursor: pointer` hidden inside an at-rule (e.g. `@media`) is still caught. + */ +function selectorsWithHandCursor(css: string): string[] { + const stripped = css.replace(/\/\*[\s\S]*?\*\//g, ''); + const selectors: string[] = []; + const re = /cursor:\s*pointer/g; + for (let m = re.exec(stripped); m; m = re.exec(stripped)) { + const open = stripped.lastIndexOf('{', m.index); + selectors.push(stripped.slice(0, open).split(/[{}]/).pop()?.trim() ?? ''); } + return selectors.sort(); +} - it('the internal markdown link keeps the hand cursor', async () => { +describe('native cursor convention contract', () => { + it('styles.css: cursor:pointer lives only on the link-styled nav button', async () => { const css = await readFile(STYLES_PATH, 'utf8'); - const rule = findRule(css, '.maka-markdown-link-internal'); - assert.ok(rule, '.maka-markdown-link-internal rule must exist'); - assert.match( - rule!, - /cursor:\s*pointer/, - 'the link-styled in-app nav button must keep the hand cursor — it presents as a link', + assert.deepEqual( + selectorsWithHandCursor(css), + [...CURSOR_POINTER_ALLOWLIST].sort(), + 'Only `.maka-markdown-link-internal` may carry `cursor: pointer` — it presents as a link, and native macOS reserves the hand for links while every other control uses the default arrow. Drop the declaration, or reuse the link class if the element genuinely is the in-app link button.', ); }); -}); -/** Return the body of the first flat rule whose selector list includes `target`. */ -function findRule(css: string, target: string): string | null { - for (const rule of iterateRules(css)) { - if (rule.selector.split(',').some((s) => s.trim() === target)) { - return `${rule.selector} { ${rule.body.trim()} }`; - } - } - return null; -} - -/** Yield each flat `selector { body }` rule, recursing into at-rules. */ -function* iterateRules(css: string): Generator<{ selector: string; body: string }> { - let i = 0; - while (i < css.length) { - while (i < css.length && /\s/.test(css[i]!)) i++; - if (css.startsWith('/*', i)) { - const end = css.indexOf('*/', i + 2); - if (end === -1) return; - i = end + 2; - continue; - } - const braceIdx = css.indexOf('{', i); - if (braceIdx === -1) return; - const selector = css.slice(i, braceIdx).trim(); - let depth = 1; - let j = braceIdx + 1; - while (j < css.length && depth > 0) { - const ch = css[j]; - if (ch === '{') depth++; - else if (ch === '}') depth--; - j++; - } - if (depth !== 0) return; - const body = css.slice(braceIdx + 1, j - 1); - if (selector.startsWith('@')) { - yield* iterateRules(body); - } else { - yield { selector, body }; - } - i = j; - } -} + it('maka-tokens.css: defines no cursor:pointer', async () => { + const css = await readFile(TOKENS_PATH, 'utf8'); + assert.deepEqual( + selectorsWithHandCursor(css), + [], + 'Design tokens must not set `cursor: pointer`; the hand cursor belongs only on the link-styled nav button in styles.css.', + ); + }); +}); diff --git a/apps/desktop/src/renderer/styles.css b/apps/desktop/src/renderer/styles.css index 7ee38d6e78..9e7de1d004 100644 --- a/apps/desktop/src/renderer/styles.css +++ b/apps/desktop/src/renderer/styles.css @@ -14,12 +14,8 @@ :root { --maka-titlebar-control-safe-left: 94px; - /* The single titlebar baseline: the macOS traffic-light vertical center - in window space (main.ts MAIN_WINDOW_TRAFFIC_LIGHT_POSITION.y + button - radius). Every titlebar-row icon cluster anchors to this, so the - sidebar header strip, the collapsed strip, and the workspace - top-actions share ONE row — collapsing/expanding the sidebar can't - move it. Each cluster subtracts a small per-container correction. */ + /* Shared titlebar baseline = macOS traffic-light vertical center; every icon + cluster anchors here, each subtracting a small per-container correction. */ --maka-titlebar-control-safe-top: 20px; } @@ -262,8 +258,7 @@ button:active { display: flex; align-items: flex-start; gap: 6px; - /* Centers the 24px icon on the shared titlebar baseline. Correction = - 12px half-icon + 5px detail-panel inset. */ + /* 17px correction = 12px half-icon + 5px detail-panel inset. */ padding: calc(var(--maka-titlebar-control-safe-top) - 17px) 12px 0 var(--maka-titlebar-control-safe-left); -webkit-app-region: drag; } @@ -373,8 +368,7 @@ button:active { -webkit-app-region: no-drag; position: absolute; z-index: 4; - /* Shared titlebar baseline; same 17px correction as the collapsed strip - (12px half-icon + 5px detail-panel inset). */ + /* Same baseline; 17px correction = 12px half-icon + 5px detail-panel inset. */ top: calc(var(--maka-titlebar-control-safe-top) - 17px); right: 24px; display: inline-flex; @@ -653,8 +647,7 @@ button:active { justify-content: space-between; gap: 6px; box-sizing: border-box; - /* Same shared titlebar baseline; 18px correction = 12px half-icon + - 6px sidebar-header pad (no detail-panel inset here). */ + /* Same baseline; 18px correction = 12px half-icon + 6px sidebar-header pad. */ padding-top: calc(var(--maka-titlebar-control-safe-top) - 18px); padding-left: calc(var(--maka-titlebar-control-safe-left) - 10px); -webkit-app-region: drag;