diff --git a/lib/public/css/overlays.css b/lib/public/css/overlays.css index 9a0bd89c..96353019 100644 --- a/lib/public/css/overlays.css +++ b/lib/public/css/overlays.css @@ -518,7 +518,7 @@ button.top-bar-pill.pill-accent:hover { background: color-mix(in srgb, var(--acc .popover-copy:hover { color: var(--text); border-color: var(--text-dimmer); } .popover-copy.copied { color: var(--success); border-color: var(--success); } -/* Skip permissions pill */ +/* Quiet native toolbar control for session permission mode. */ .session-permission-control { display: inline-flex; align-items: center; @@ -535,35 +535,40 @@ button.top-bar-pill.pill-accent:hover { background: color-mix(in srgb, var(--acc flex: 0 0 auto; gap: 1px; padding: 2px; - border: 1px solid color-mix(in srgb, var(--border, #45484f) 88%, transparent); - border-radius: 10px; - background: color-mix(in srgb, var(--surface-raised, rgba(0,0,0,.18)) 86%, transparent); - box-shadow: inset 0 1px 0 rgba(255,255,255,.04), 0 2px 8px rgba(0,0,0,.12); + border: 1px solid color-mix(in srgb, var(--border) 82%, transparent); + border-radius: 8px; + background: color-mix(in srgb, var(--bg-alt) 72%, transparent); } .session-permission-segmented button { position: relative; - flex: 0 0 auto; + flex: 0 0 48px; + width: 48px; + height: 22px; border: 0; border-radius: 7px; - padding: 4px 9px; - color: var(--text-muted, inherit); + padding: 3px 4px; + color: var(--text-secondary); background: transparent; font: inherit; + font-weight: 500; font-size: 11px; line-height: 1; white-space: nowrap; cursor: pointer; - transition: color .15s ease, background .15s ease, box-shadow .15s ease; -} -.session-permission-segmented button:hover:not(:disabled) { color: var(--text, #fff); background: rgba(255,255,255,.07); } -.session-permission-segmented button:focus-visible { outline: 2px solid var(--accent, #6f8cff); outline-offset: 1px; z-index: 1; } -.session-permission-segmented button.active { background: var(--accent, #6f8cff); color: #fff; box-shadow: 0 1px 4px rgba(0,0,0,.2); } -.session-permission-segmented button:disabled { cursor: not-allowed; opacity: .5; } + transition: color .15s ease, background .15s ease, box-shadow .15s ease, border-color .15s ease; +} +.session-permission-segmented button:hover:not(:disabled) { color: var(--text); background: color-mix(in srgb, var(--bg) 76%, transparent); } +.session-permission-segmented button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; z-index: 1; } +.session-permission-segmented button.active { border: 1px solid var(--border); background: var(--input-bg); color: var(--text); font-weight: var(--font-weight-heading, 600); box-shadow: 0 1px 2px rgba(var(--shadow-rgb), .12); } +:root.light-theme .session-permission-segmented button.active { background: var(--bg); } +.session-permission-segmented button:disabled { cursor: not-allowed; } +.session-permission-control:not(.permission-pending) .session-permission-segmented button:disabled { color: var(--text-muted); opacity: .65; } .session-permission-control.permission-pending .session-permission-segmented button:disabled { cursor: wait; } -.session-permission-segmented button.pending { color: #fff; } -.permission-spinner { display: inline-block; visibility: hidden; width: 10px; height: 10px; margin-left: 4px; vertical-align: -1px; border: 1.5px solid currentColor; border-right-color: transparent; border-radius: 50%; } +.session-permission-segmented button.pending { color: var(--text); background: color-mix(in srgb, var(--bg) 42%, transparent); } +.session-permission-segmented button.pending .permission-label { visibility: hidden; } +.permission-spinner { position: absolute; inset: 50% auto auto 50%; display: block; visibility: hidden; width: 10px; height: 10px; transform: translate(-50%, -50%); border: 1.5px solid currentColor; border-right-color: transparent; border-radius: 50%; } .session-permission-segmented button.pending .permission-spinner { visibility: visible; animation: permission-spin .7s linear infinite; } -@keyframes permission-spin { to { transform: rotate(360deg); } } +@keyframes permission-spin { to { transform: translate(-50%, -50%) rotate(360deg); } } .session-permission-status { flex: 0 1 auto; min-width: 0; @@ -592,12 +597,8 @@ button.top-bar-pill.pill-accent:hover { background: color-mix(in srgb, var(--acc } .permission-runtime-fallback .session-permission-status { color: var(--warning, #d49a55); } .permission-globally-forced .session-permission-status { color: var(--warning, #d49a55); } -.permission-label-short { display: none; } @media (max-width: 760px) { - .permission-label-long { display: none; } - .permission-label-short { display: inline; } .session-permission-control { column-gap: 4px; } - .session-permission-segmented button { padding-inline: 7px; } .session-permission-status { max-width: 132px; font-size: 9px; diff --git a/lib/public/css/pane.css b/lib/public/css/pane.css index 94a7d5a1..ed570aa2 100644 --- a/lib/public/css/pane.css +++ b/lib/public/css/pane.css @@ -365,31 +365,35 @@ body.pane-mode #input-area { line-height: 1; } -/* Header-scoped (two-class) selectors: pane.css loads BEFORE menus.css in - style.css, so the base .session-full-access rules would win a specificity - tie and keep the pill at its 22px title-bar size inside the 24px pane - header. Scope under .split-pane-header to outrank them. */ +/* Header-scoped selectors keep the compact permission control clear of the + title and close affordance while preserving the shared header rhythm. */ +.split-pane-header { + height: 28px; + min-height: 28px; +} + .split-pane-header .split-pane-full-access { - height: 22px; + height: 24px; align-self: center; margin-left: 0; padding: 0; gap: 4px; - font-size: 10px; + font-size: 11px; letter-spacing: 0.02em; flex-wrap: nowrap; flex: 0 0 auto; - overflow: hidden; } .split-pane-header .session-permission-segmented button { - min-height: 18px; - padding: 3px 6px; - font-size: 10px; + min-height: 20px; + height: 20px; + font-size: 11px; } -.split-pane-header .permission-label-long { display: none; } -.split-pane-header .permission-label-short { display: inline; } +.split-pane-header .session-permission-segmented { + height: 24px; + padding: 1px; +} .split-pane-header .session-permission-status { flex: 0 0 auto; diff --git a/lib/public/index.html b/lib/public/index.html index 13bd63d1..cdc1f35d 100644 --- a/lib/public/index.html +++ b/lib/public/index.html @@ -410,9 +410,9 @@

diff --git a/lib/public/modules/permission-control.js b/lib/public/modules/permission-control.js index 821c5a26..38f43c22 100644 --- a/lib/public/modules/permission-control.js +++ b/lib/public/modules/permission-control.js @@ -46,9 +46,9 @@ export function createPermissionControl(className, ariaLabel) { control.setAttribute("role", "group"); control.setAttribute("aria-label", ariaLabel || "Session permission mode"); control.innerHTML = '
' + - '' + - '' + - '' + + '' + + '' + + '' + '
'; control.querySelector(".session-permission-status").setAttribute("aria-atomic", "true"); return control; @@ -101,6 +101,7 @@ export function renderPermissionControl(control, state) { control.classList.toggle("permission-globally-forced", globallyForced); control.classList.toggle("permission-runtime-fallback", state.permissionMode === "auto" && !!state.effectivePermissionMode && state.effectivePermissionMode !== "auto"); control.setAttribute("aria-busy", pending ? "true" : "false"); + var buttonTitles = { default: "Ask permissions", auto: "Auto permissions", bypassPermissions: "Skip permissions" }; var buttons = control.querySelectorAll("[data-permission-mode]"); for (var i = 0; i < buttons.length; i++) { var mode = buttons[i].dataset.permissionMode; @@ -113,8 +114,7 @@ export function renderPermissionControl(control, state) { buttons[i].classList.toggle("pending", requested); buttons[i].setAttribute("aria-pressed", selected ? "true" : "false"); buttons[i].setAttribute("aria-busy", requested ? "true" : "false"); - if (unsupported) buttons[i].title = "Auto permissions are unavailable for this Claude session."; - else buttons[i].removeAttribute("title"); + buttons[i].title = unsupported ? "Auto permissions are unavailable for this Claude session." : buttonTitles[mode]; } var status = control.querySelector(".session-permission-status"); if (status) { diff --git a/test/fixtures/permission-mode-browser.html b/test/fixtures/permission-mode-browser.html index d20f8d28..3b91a98c 100644 --- a/test/fixtures/permission-mode-browser.html +++ b/test/fixtures/permission-mode-browser.html @@ -1,24 +1,23 @@ - + Production permission control browser fixture @@ -42,6 +42,14 @@

Production permission controls

+ + @@ -81,6 +89,9 @@

Production permission controls

var log = document.getElementById("fixture-log"); var onlineButton = document.getElementById("toggle-online"); var sessionButton = document.getElementById("switch-session"); + var themeMode = document.getElementById("theme-mode"); + var splitWidth = document.getElementById("split-width"); + var splitPane = document.querySelector(".fixture-split-pane"); createStore({ currentSlug: "fixture-project", @@ -166,6 +177,16 @@

Production permission controls

store.subscribe(renderProductionControls); renderProductionControls(); + themeMode.addEventListener("change", function () { + document.documentElement.classList.toggle("light-theme", this.value === "light"); + document.documentElement.classList.toggle("dark-theme", this.value !== "light"); + writeLog("Production theme tokens switched to " + this.value + "."); + }); + splitWidth.addEventListener("change", function () { + splitPane.style.setProperty("--fixture-split-width", this.value + "px"); + writeLog("Split Driver header width set to " + this.value + "px."); + }); + document.getElementById("release-held").addEventListener("click", function () { if (!heldRequest) { writeLog("No held production request."); return; } var request = heldRequest; diff --git a/test/permission-mode-ui.test.js b/test/permission-mode-ui.test.js index e3bccfb7..88ccd6f7 100644 --- a/test/permission-mode-ui.test.js +++ b/test/permission-mode-ui.test.js @@ -122,11 +122,10 @@ test("standalone header exposes Ask Auto Skip permissions and runtime status", f var html = read("lib/public/index.html"); var start = html.indexOf('id="header-full-access-btn"'); var control = html.slice(start, start + 1200); - assert.match(control, /data-permission-mode="default">.*permission-label">Ask/s); - assert.match(control, /data-permission-mode="auto">.*permission-label">Auto/s); + assert.match(control, /data-permission-mode="default"[^>]*aria-label="Ask permissions"[^>]*title="Ask permissions"[^>]*>.*permission-label">Ask/s); + assert.match(control, /data-permission-mode="auto"[^>]*aria-label="Auto permissions"[^>]*title="Auto permissions"[^>]*>.*permission-label">Auto/s); assert.match(control, /permission-spinner/); - assert.match(control, /aria-label="Skip permissions"/); - assert.match(control, />Skip permissions]*aria-label="Skip permissions"[^>]*title="Skip permissions"[^>]*>.*permission-label">Skip]*aria-live="polite"|aria-live="polite"[^>]*session-permission-status/); assert.doesNotMatch(control, /role="switch"|session-full-access-track/); }); @@ -218,9 +217,12 @@ test("production rendering shows Auto only for Claude and disables it only when assert.equal(unavailable.buttons[1].disabled, true); assert.match(unavailable.buttons[1].title, /unavailable for this Claude session/); assert.equal(unavailable.status.textContent, "Auto unavailable for this Claude session"); + f.api.renderPermissionControl(unavailable.control, { projectSlug: "project-a", sessionId: 7, vendor: "claude", + permissionMode: "default", effectivePermissionMode: "default", permissionCapabilities: { auto: true }, connected: true }); + assert.equal(unavailable.buttons[1].title, "Auto permissions"); }); -test("pending rendering marks the control and requested segment busy with an inline spinner", function () { +test("pending rendering marks the control and requested segment busy with a centered spinner", function () { var f = permissionClientFixture(); var ui = controlFixture(); f.api.sendPermissionMode("project-a", 7, "auto"); @@ -480,8 +482,24 @@ test("browser fixture mounts production controls and routes mock socket results assert.match(sharedCss, /\.session-permission-control\.permission-pending \.session-permission-status:not\(\.hidden\) \{/); assert.match(sharedCss, /\.permission-spinner \{[^}]*visibility: hidden/s); assert.match(paneCss, /\.split-pane-header \.session-permission-status/); - assert.match(paneCss, /\.split-pane-header \.split-pane-full-access \{[^}]*height: 22px[^}]*font-size: 10px/s); - assert.match(paneCss, /\.split-pane-header \.session-permission-segmented button \{[^}]*min-height: 18px[^}]*font-size: 10px/s); - assert.match(paneCss, /\.split-pane-header \.permission-label-long \{ display: none; \}/); - assert.match(paneCss, /\.split-pane-header \.permission-label-short \{ display: inline; \}/); + assert.match(sharedCss, /button\.active \{[^}]*background: var\(--input-bg\)[^}]*color: var\(--text\)[^}]*font-weight: var\(--font-weight-heading/s); + assert.match(sharedCss, /:root\.light-theme \.session-permission-segmented button\.active \{[^}]*background: var\(--bg\)/s); + assert.match(sharedCss, /button \{[^}]*font-weight: 500/s); + assert.match(sharedCss, /permission-control:not\(\.permission-pending\).*button:disabled \{[^}]*color: var\(--text-muted\)[^}]*opacity: \.65/s); + assert.match(sharedCss, /button\.pending \.permission-label \{ visibility: hidden; \}/); + assert.match(sharedCss, /\.permission-spinner \{ position: absolute;[^}]*transform: translate\(-50%, -50%\)/s); + assert.match(sharedCss, /@keyframes permission-spin \{[^}]*translate\(-50%, -50%\) rotate\(360deg\)/s); + assert.match(paneCss, /\.split-pane-header \{[^}]*height: 28px[^}]*min-height: 28px/s); + assert.match(paneCss, /\.split-pane-header \.split-pane-full-access \{[^}]*height: 24px[^}]*font-size: 11px/s); + assert.match(paneCss, /\.split-pane-header \.session-permission-segmented button \{[^}]*height: 20px[^}]*font-size: 11px/s); + assert.match(paneCss, /\.split-pane-header \.session-permission-segmented \{[^}]*height: 24px[^}]*padding: 1px/s); + assert.doesNotMatch(paneCss, /\.split-pane-header \.split-pane-full-access \{[^}]*overflow: hidden/s); + assert.match(fixture, /id="theme-mode"/); + assert.match(fixture, /id="split-width"/); + assert.match(fixture, /value="390"/); + assert.match(fixture, /classList\.toggle\("light-theme"/); + assert.match(fixture, /\.fixture-split-pane \{ --fixture-split-width: 356px; flex: 0 0 var\(--fixture-split-width\)/); + assert.match(fixture, /\.fixture-split-pane \{ flex: none; \}/); + assert.match(fixture, /setProperty\("--fixture-split-width"/); + assert.doesNotMatch(fixture, /style\.flexBasis/); });