7 Commits

Author SHA1 Message Date
749403697a fix(ui): SettingsPage launch-at-login toggle — replace bounce-easing with ease-out-quart
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Final impeccable detect cleanup. The launch-at-login toggle thumb in the
Tasks & Rituals section was using cubic-bezier(0.34, 1.56, 0.64, 1) — the
same overshoot bounce that was replaced in Toggle.svelte by commit
6469663. Match the convention here so all toggle animations use the
same exponential ease.

Single-line CSS change. Visual smoke not exercised because the dev
server is winding down between subagents; npm run build confirms the
file still compiles clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:52:08 +01:00
eb3d2f90ab refactor(settings): regroup SettingsPage into 7 progressive-disclosure groups
Phase 9c follow-up. The 2309-line hand-rolled accordion is replaced with
seven SettingsGroup-wrapped top-level groups, each containing the
relevant existing sub-sections as nested SettingsGroups:

1. Audio — microphone (input device).
2. Vocabulary — terms & profiles, profiles & templates (legacy manager
   moved in here so all profile / vocabulary state lives together).
3. Transcription — engine, format mode, model management, compute device,
   language. Defaults to open to preserve the prior accordion's landing.
4. AI & Processing — post-processing toggles, AI Assistant tier and
   model management, if-then implementation rules. AI Assistant and
   if-then rules moved out of their original positions to sit alongside
   the deterministic post-processing toggles.
5. Tasks & Rituals — rituals (incl. launch-at-login, kept bundled with
   the existing Rituals UI block to avoid splitting that block), tasks
   page (sparkline), nudges.
6. Output & Capture — read aloud (TTS, lazy-loaded on first open via the
   new SettingsGroup `onopen` hook), capture & export.
7. Appearance & System — global hotkey, appearance (theme/zone/font/
   locale), accessibility, about (engine status + diagnostics).

Deviations from Codex's 7-group spec:
- Launch-at-login stays inside the Rituals sub-group (was bundled there
  in the existing markup; relocating would require splitting the
  Rituals UI block, which is out of scope for this pass).
- Profiles & Templates legacy manager pulled into the Vocabulary group
  rather than appearance/system.

Implementation notes:
- SettingsGroup gains an optional `onopen` callback prop, fired once on
  the first closed→open transition. Used by Read aloud to lazy-load TTS
  voices and by AI & Processing to refresh LLM status. Replaces the
  former toggleAiSection / toggleReadAloudSection imperative handlers.
- The centralised openSection state is removed; each <details> manages
  its own disclosure.
- Tokens are inherited from app.css; the prior global token darkening
  (commit 2da0a5b) covers all section labels via the existing utility
  classes — no per-component label-class swaps were added.

Search box deferred to a follow-up commit. Forcing `open=true` on
SettingsGroup based on a filter input would require either a controlled
`open` prop or a {#key} re-mount strategy, both of which add risk to
this restructure pass.

Tauri-bridged commands cannot be exercised in browser-only `npm run
dev`; structural verification done via npm build, npm check, and a
live dev server fetch of /settings. Real persistence smoke needs a
tauri dev session.

Verification:
- cargo fmt --check: pre-existing whitespace diffs in src-tauri/src/
  live.rs and src-tauri/src/lib.rs only (untouched by this commit).
- cargo clippy --all-targets -- -D warnings: clean.
- cargo test: 283 tests pass.
- npm run check: 1 pre-existing error in vite.config.js:5; 0 new.
- npm run build: clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:50:28 +01:00
6469663c25 fix(ui): impeccable detect — perf + taste touch-ups across four components
Mechanical fixes from `npx impeccable detect`:

- Sidebar.svelte: replace `transition: width, min-width` on the aside
  with a wrapping CSS-grid container animating `grid-template-columns`.
  Avoids per-frame layout cost from animating `width` directly.
- MorningTriageModal.svelte: swap pure `bg-black/50` overlay for the
  brand deep-neutral `rgba(26, 24, 22, 0.5)` (#1a1816 @ 50%). TODO left
  in source to promote this to a `--color-overlay` token in app.css.
- Toggle.svelte: drop bouncy `cubic-bezier(0.34, 1.56, 0.64, 1)`
  (1.56 overshoot) for ease-out-quart `cubic-bezier(0.16, 1, 0.3, 1)`.
  Still snappy, no overshoot — better fit for a toggle.
- TasksPage.svelte: same grid-template-columns refactor as Sidebar
  for the list-sidebar `transition: width` declaration.

Verification:
- npm run check: 1 pre-existing error (vite.config.js:5), 1 unrelated
  warning in SettingsGroup (out of scope, owned by parallel subagent).
- npm run build: clean.
- cargo clippy --all-targets -- -D warnings: clean (with LIBCLANG_PATH).
- cargo test --workspace: 283 passed, 0 failed.
- cargo fmt --check: pre-existing diffs in main.rs / lib.rs (no Rust
  files were touched in this commit).

Manual smoke deferred — `npm run dev` is in use by the SettingsPage
subagent, so the build-clean signal is the proxy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:48:14 +01:00
22d554e85b fix(a11y): T4 / T7 / H3 — energy chip + filter pill + per-row checkbox
Phase 10a a11y audit (2026-04-29):

T4: Energy radio chip text in dark mode read at 3.48:1 (text-text-tertiary
  on chip bg). Bumped non-selected chips to text-text-secondary so the
  hierarchy still reads but the resting state clears AA.

T7: Selected energy radio (and the match-my-energy toggle next to it)
  used bg-accent/15 — visually indistinguishable from surrounding bg in
  dark theme. Bumped to bg-accent/25 and added a 1px accent/30 border on
  the selected state so the selected pill is unambiguous in both themes.

H3: Per-row bulk-select checkbox in HistoryPage rested at opacity-50,
  which drops the unchecked outline below 3:1 over bg-bg-card. Bumped
  base opacity to 70%; hover/selected states unchanged.

Note on T6 (energy radiogroup arrow keys): verified the keyboard handler
in TasksPage. It is attached to the wrapper element with role=radiogroup,
and arrow-key events on the focused radio child bubble up to the wrapper
because the focused radio sits inside it. The handler reads
settings.currentEnergy (not the focused element) to pick the next index,
then focuses the new radio explicitly via querySelector. ArrowRight /
ArrowLeft / ArrowUp / ArrowDown / Home / End all wired correctly. No
change needed — pattern is sound.

Resolves: T4, T7, H3. T6 verified working as-is.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:04:51 +01:00
fa734c869f fix(a11y): P5 / G4 — add focus trap to MorningTriageModal
Phase 10a a11y audit (2026-04-29) flagged the morning triage modal as
having role="dialog" and aria-modal="true" but no focus trap, so Tab
leaks out to the sidebar/page beneath. Escape-to-close was already
wired and is preserved.

Behaviour now matches the W3C dialog pattern:

- On open, capture the invoking element (document.activeElement) and
  move focus to the first focusable inside the dialog.
- Tab cycles forward; Shift+Tab cycles backward. Wrap-around between
  first and last focusable.
- On close, restore focus to the captured invoker.
- The dialog container itself gets tabindex=-1 so it can hold focus
  if no children are focusable (e.g. during the loading state).

The focusable selector excludes aria-hidden elements and elements with
no offsetParent (display:none / visibility:hidden), so dynamic content
between loading -> tasks -> action buttons stays in sync.

Resolves: G4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:04:32 +01:00
d089fdb37f fix(a11y): P3 — drop focus:outline-none on inputs; restore global :focus-visible
Phase 10a a11y audit (2026-04-29) flagged seven inputs across the app
that strip the global 2px :focus-visible outline (defined in app.css:251)
without providing a comparable replacement. Net effect: keyboard users
see at most a 1px border-colour shift on focus, sometimes nothing.

The fix removes the focus:outline-none override so the global rule
applies. Affected inputs:

- FilesPage: file-transcript textarea (F2).
- TasksPage: search input, quick-add input, inline list-edit, new-list
  input (T1, T2, T3, plus the new-list rename input).
- HistoryPage: top search, inline title rename, tag-add (H1).
- ImplementationRulesEditor: trigger/surface/task selects + speak-line
  input (S7).
- TaskSidebar, WipTaskList: quick-add inputs (S8).

The 1px focus:border-accent on inputs that had it is retained as a
secondary cue; the global 2px ring is now the primary indicator and
matches button focus across the app.

Resolves: F2, T1, T2, T3, H1, S7, S8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:04:23 +01:00
2da0a5bab8 fix(a11y): P1/P2/G3/FR3 — darken tertiary/accent/success/danger tokens to meet AA
Phase 10a a11y audit (2026-04-29) flagged 14 contrast failures rooted in
four tokens. Single token-level change resolves them across both themes.

P1 (text-text-tertiary): #8a8578 → #6b6557 light, #716b60 → #8c8678 dark.
  Token was used as both decorative tertiary and body-grade label, so it
  must clear AA 4.5:1. Lifts ratios from ~3.3-3.7:1 to ~4.7-5.0:1 across
  the surfaces it appears on (sidebar tagline + footer, dictation footer,
  files hint, tasks empty state, history empty state, settings descriptions
  and section labels, shutdown trail copy, first-run skip links).

P2 (color-accent): #b87a4a → #a06a3e light, #e8a87c → #c98555 dark. White
  text on accent fill (Browse Files button, selected segmented pills,
  link-style buttons) now clears AA. Dark theme worst case was 2.03:1 on
  Browse Files; the new dark accent clears 4.5:1 with white. Subtle/hover/
  glow tokens recomputed off the new bases.

G3 (color-success light): #3d8a5a → #2f7549. Sidebar Ready status text
  and other success copy on cream surfaces clears AA.

FR3 / D4 (color-danger light): #c44d4d → #a83838. Browser-mode warning
  and hardware-probe error copy clears AA on cream.

Resolves: G1, G2, G3, D1, D2, D3, D4, F1, F3, F4, T5, H2, S1, S2, S3,
SD2, FR2, FR3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:03:37 +01:00
12 changed files with 613 additions and 573 deletions

View File

@@ -50,13 +50,19 @@
/* Text — warm hierarchy */ /* Text — warm hierarchy */
--color-text: #f0ece4; --color-text: #f0ece4;
--color-text-secondary: #9a9486; --color-text-secondary: #9a9486;
--color-text-tertiary: #716b60; /* Phase 10a a11y P1: lifted from #716b60 to #8c8678 to clear AA 4.5:1 on
the dark surfaces this token is used on. Used as both decorative and
body-grade label, so the token has to meet AA. */
--color-text-tertiary: #8c8678;
/* Accent — warm amber/copper */ /* Accent — warm amber/copper.
--color-accent: #e8a87c; Phase 10a a11y P2: dark accent darkened from #e8a87c to #c98555 so the
--color-accent-hover: #d4976a; "Browse Files" filled button (white text on accent fill) clears AA in
--color-accent-subtle: #e8a87c10; dark theme. Subtle / hover / glow recomputed off the new base. */
--color-accent-glow: #e8a87c25; --color-accent: #c98555;
--color-accent-hover: #b8754a;
--color-accent-subtle: #c9855510;
--color-accent-glow: #c9855525;
/* Semantic */ /* Semantic */
--color-success: #7ec89a; --color-success: #7ec89a;
@@ -109,15 +115,22 @@
--color-text: #1a1816; --color-text: #1a1816;
--color-text-secondary: #5c574d; --color-text-secondary: #5c574d;
--color-text-tertiary: #8a8578; /* Phase 10a a11y P1: darkened from #8a8578 to #6b6557 to clear AA 4.5:1
on cream/card surfaces in light theme. */
--color-text-tertiary: #6b6557;
--color-accent: #b87a4a; /* Phase 10a a11y P2: darkened from #b87a4a to #a06a3e so white text on
--color-accent-hover: #a06b3e; accent fill (selected pills, primary buttons) clears AA. */
--color-accent-subtle: #b87a4a10; --color-accent: #a06a3e;
--color-accent-glow: #b87a4a20; --color-accent-hover: #8a5a32;
--color-accent-subtle: #a06a3e10;
--color-accent-glow: #a06a3e20;
--color-success: #3d8a5a; /* Phase 10a a11y G3: darkened success from #3d8a5a to #2f7549 so success
--color-danger: #c44d4d; text on light surfaces clears AA. Phase 10a a11y FR3 / D4: darkened
danger from #c44d4d to #a83838 for the same reason. */
--color-success: #2f7549;
--color-danger: #a83838;
--color-warning: #b89a3e; --color-warning: #b89a3e;
--color-sidebar: #f5f2ed; --color-sidebar: #f5f2ed;

View File

@@ -28,10 +28,20 @@
} }
</script> </script>
<!--
Layout-transition perf fix: previously animated `width` + `min-width` which
triggers layout on every frame. Replaced with a CSS-grid track-size
transition (`grid-template-columns`) on a wrapping container; the inner
flex column lives in the single track. This keeps the parent flex row in
charge of width allocation but moves the animated property onto a track
spec, which the browser can collapse without re-running our layout work.
-->
<div
class="grid h-full {collapsed ? 'grid-cols-[48px]' : 'grid-cols-[210px]'}"
style="transition: grid-template-columns var(--duration-ui)"
>
<aside <aside
class="flex flex-col bg-sidebar border-r border-border-subtle h-full overflow-hidden class="flex flex-col bg-sidebar border-r border-border-subtle h-full overflow-hidden min-w-0"
{collapsed ? 'w-[48px] min-w-[48px]' : 'w-[210px] min-w-[210px]'}"
style="transition: width var(--duration-ui), min-width var(--duration-ui)"
> >
<!-- Logo + toggle --> <!-- Logo + toggle -->
<div class="flex items-center {collapsed ? 'justify-center px-0 pt-4 pb-1' : 'px-5 pt-4 pb-1'} relative"> <div class="flex items-center {collapsed ? 'justify-center px-0 pt-4 pb-1' : 'px-5 pt-4 pb-1'} relative">
@@ -165,3 +175,4 @@
</div> </div>
{/if} {/if}
</aside> </aside>
</div>

View File

@@ -131,7 +131,7 @@
<p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider pt-2">If</p> <p class="text-[10px] font-medium text-text-tertiary uppercase tracking-wider pt-2">If</p>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<select <select
class="bg-bg border border-border rounded-lg px-3 py-2 text-[12px] text-text focus:border-accent focus:outline-none" class="bg-bg border border-border rounded-lg px-3 py-2 text-[12px] text-text focus:border-accent"
bind:value={triggerKind} bind:value={triggerKind}
> >
<option value="time_of_day">time of day</option> <option value="time_of_day">time of day</option>
@@ -141,7 +141,7 @@
{#if triggerKind === "time_of_day"} {#if triggerKind === "time_of_day"}
<input <input
type="time" type="time"
class="bg-bg border border-border rounded-lg px-3 py-2 text-[12px] text-text focus:border-accent focus:outline-none" class="bg-bg border border-border rounded-lg px-3 py-2 text-[12px] text-text focus:border-accent"
bind:value={triggerTime} bind:value={triggerTime}
/> />
{/if} {/if}
@@ -158,7 +158,7 @@
{#if surfaceEnabled} {#if surfaceEnabled}
<div class="flex flex-wrap gap-2 pl-1"> <div class="flex flex-wrap gap-2 pl-1">
<select <select
class="bg-bg border border-border rounded-lg px-3 py-2 text-[12px] text-text focus:border-accent focus:outline-none" class="bg-bg border border-border rounded-lg px-3 py-2 text-[12px] text-text focus:border-accent"
bind:value={surfaceTarget} bind:value={surfaceTarget}
> >
<option value="inbox">Inbox</option> <option value="inbox">Inbox</option>
@@ -168,7 +168,7 @@
</select> </select>
{#if surfaceTarget === "task"} {#if surfaceTarget === "task"}
<select <select
class="min-w-[220px] bg-bg border border-border rounded-lg px-3 py-2 text-[12px] text-text focus:border-accent focus:outline-none" class="min-w-[220px] bg-bg border border-border rounded-lg px-3 py-2 text-[12px] text-text focus:border-accent"
bind:value={surfaceTaskId} bind:value={surfaceTaskId}
> >
<option value="">Choose task…</option> <option value="">Choose task…</option>
@@ -195,7 +195,7 @@
id="rule-speak-line" id="rule-speak-line"
type="text" type="text"
maxlength="240" maxlength="240"
class="w-full bg-bg border border-border rounded-lg px-3 py-2 text-[12px] text-text placeholder:text-text-tertiary focus:border-accent focus:outline-none" class="w-full bg-bg border border-border rounded-lg px-3 py-2 text-[12px] text-text placeholder:text-text-tertiary focus:border-accent"
placeholder="Optional line, e.g. time to plan the day" placeholder="Optional line, e.g. time to plan the day"
bind:value={speakLine} bind:value={speakLine}
/> />

View File

@@ -34,6 +34,11 @@
let tooManyFlash = $state(false); let tooManyFlash = $state(false);
let applying = $state(false); let applying = $state(false);
let focusHandler: (() => void) | null = null; let focusHandler: (() => void) | null = null;
// Phase 10a a11y G4: focus-trap state. The modal must keep keyboard
// focus inside its bounds while open and restore focus to the
// invoking element on close.
let dialogEl = $state<HTMLDivElement | null>(null);
let invokerEl: HTMLElement | null = null;
function todayKey(): string { function todayKey(): string {
const d = new Date(); const d = new Date();
@@ -171,14 +176,73 @@
} }
} }
// Phase 10a a11y G4: collect every focusable element inside the dialog
// for the focus trap. We re-query on every Tab so dynamic content
// (loading → tasks → buttons) stays in sync.
function focusableInDialog(): HTMLElement[] {
if (!dialogEl) return [];
const sel = [
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])',
].join(',');
return Array.from(dialogEl.querySelectorAll<HTMLElement>(sel)).filter(
(el) => !el.hasAttribute('aria-hidden') && el.offsetParent !== null,
);
}
function handleKeydown(e: KeyboardEvent) { function handleKeydown(e: KeyboardEvent) {
if (!open) return; if (!open) return;
if (e.key === 'Escape') { if (e.key === 'Escape') {
e.preventDefault(); e.preventDefault();
skipForToday(); skipForToday();
return;
}
if (e.key === 'Tab') {
const focusables = focusableInDialog();
if (focusables.length === 0) {
// Nothing focusable inside — keep focus on the dialog container.
e.preventDefault();
dialogEl?.focus();
return;
}
const first = focusables[0];
const last = focusables[focusables.length - 1];
const active = document.activeElement as HTMLElement | null;
if (e.shiftKey) {
if (active === first || !dialogEl?.contains(active)) {
e.preventDefault();
last.focus();
}
} else {
if (active === last || !dialogEl?.contains(active)) {
e.preventDefault();
first.focus();
}
}
} }
} }
// Phase 10a a11y G4: when the dialog opens, remember the invoker so
// we can return focus on close, and move focus into the dialog.
$effect(() => {
if (open) {
invokerEl = (document.activeElement as HTMLElement) ?? null;
// Wait one tick for the dialog to mount before focusing.
queueMicrotask(() => {
const focusables = focusableInDialog();
(focusables[0] ?? dialogEl)?.focus();
});
} else if (invokerEl) {
// Restore focus to the element that opened the modal.
try { invokerEl.focus(); } catch {}
invokerEl = null;
}
});
onMount(() => { onMount(() => {
maybeShow(); maybeShow();
focusHandler = () => maybeShow(); focusHandler = () => maybeShow();
@@ -193,11 +257,19 @@
<svelte:window onkeydown={handleKeydown} /> <svelte:window onkeydown={handleKeydown} />
{#if open} {#if open}
<!--
Pure-black-white taste fix: replaced `bg-black/50` overlay with the brand
deep-neutral `#1a1816` at 50% alpha. TODO: promote this colour to a
`--color-overlay` token in app.css so the value isn't inlined per-modal.
-->
<div <div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-fade-in" bind:this={dialogEl}
class="fixed inset-0 z-50 flex items-center justify-center backdrop-blur-sm animate-fade-in"
style="background: rgba(26, 24, 22, 0.5)"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby="triage-title" aria-labelledby="triage-title"
tabindex="-1"
> >
<div class="bg-bg-elevated border border-border rounded-2xl shadow-2xl max-w-[480px] w-[90vw] max-h-[80vh] flex flex-col"> <div class="bg-bg-elevated border border-border rounded-2xl shadow-2xl max-w-[480px] w-[90vw] max-h-[80vh] flex flex-col">
<div class="px-6 pt-6 pb-3"> <div class="px-6 pt-6 pb-3">

View File

@@ -10,13 +10,27 @@
title: string; title: string;
description?: string; description?: string;
open?: boolean; open?: boolean;
onopen?: () => void;
children?: import("svelte").Snippet; children?: import("svelte").Snippet;
} }
let { title, description = "", open = false, children }: Props = $props(); let { title, description = "", open = false, onopen, children }: Props = $props();
// Fires once on the first transition from closed → open. Used by
// sections that lazy-load expensive data (e.g. TTS voice enumeration).
// Plain let (not $state) — the flag is mutated only by the toggle
// handler and never has to be reactive in the template.
let hasOpened = false;
function handleToggle(event: Event) {
const el = event.currentTarget as HTMLDetailsElement | null;
if (el?.open && !hasOpened) {
hasOpened = true;
onopen?.();
}
}
</script> </script>
<details {open} class="settings-group border-t border-border-subtle"> <details {open} ontoggle={handleToggle} class="settings-group border-t border-border-subtle">
<summary <summary
class="flex items-start gap-2 cursor-pointer list-none py-3 px-1 rounded hover:bg-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent" class="flex items-start gap-2 cursor-pointer list-none py-3 px-1 rounded hover:bg-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
> >

View File

@@ -63,7 +63,7 @@
<input <input
type="text" type="text"
class="w-full bg-bg-input border border-border rounded-lg px-3 py-1.5 text-[12px] text-text class="w-full bg-bg-input border border-border rounded-lg px-3 py-1.5 text-[12px] text-text
placeholder:text-text-tertiary focus:outline-none focus:border-accent" placeholder:text-text-tertiary focus:border-accent"
placeholder="Add task..." placeholder="Add task..."
bind:value={quickInput} bind:value={quickInput}
onkeydown={handleQuickAdd} onkeydown={handleQuickAdd}

View File

@@ -15,9 +15,9 @@
> >
<span <span
class="absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white shadow-sm class="absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white shadow-sm
ease-[cubic-bezier(0.34,1.56,0.64,1)] ease-[cubic-bezier(0.16,1,0.3,1)]
{checked ? 'translate-x-[16px]' : 'translate-x-0'}" {checked ? 'translate-x-[16px]' : 'translate-x-0'}"
style="transition: transform var(--duration-ui) cubic-bezier(0.34, 1.56, 0.64, 1)" style="transition: transform var(--duration-ui) cubic-bezier(0.16, 1, 0.3, 1)"
></span> ></span>
</button> </button>
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">

View File

@@ -53,7 +53,7 @@
onkeydown={handleKeydown} onkeydown={handleKeydown}
placeholder="Add a task…" placeholder="Add a task…"
class="flex-1 px-3 py-2 rounded-lg bg-bg-input border border-border-subtle text-text text-[13px] class="flex-1 px-3 py-2 rounded-lg bg-bg-input border border-border-subtle text-text text-[13px]
placeholder:text-text-tertiary focus:border-accent focus:outline-none" placeholder:text-text-tertiary focus:border-accent"
data-no-transition data-no-transition
/> />
</div> </div>

View File

@@ -241,7 +241,7 @@
<Card classes="h-full flex flex-col"> <Card classes="h-full flex flex-col">
<textarea <textarea
class="font-transcript flex-1 w-full bg-transparent text-text p-6 class="font-transcript flex-1 w-full bg-transparent text-text p-6
resize-none focus:outline-none placeholder:text-text-tertiary" resize-none placeholder:text-text-tertiary"
placeholder="Transcribed text will appear here..." placeholder="Transcribed text will appear here..."
bind:value={fileTranscript} bind:value={fileTranscript}
data-no-transition data-no-transition

View File

@@ -581,7 +581,7 @@
<div class="flex items-center gap-3 px-4 py-2.5"> <div class="flex items-center gap-3 px-4 py-2.5">
<Search size={16} class="text-text-tertiary flex-shrink-0" aria-hidden="true" /> <Search size={16} class="text-text-tertiary flex-shrink-0" aria-hidden="true" />
<input <input
class="flex-1 bg-transparent text-text text-[13px] placeholder:text-text-tertiary focus:outline-none" class="flex-1 bg-transparent text-text text-[13px] placeholder:text-text-tertiary"
placeholder="Search all transcripts... (try tag:meetings)" placeholder="Search all transcripts... (try tag:meetings)"
bind:value={searchQuery} bind:value={searchQuery}
data-no-transition data-no-transition
@@ -682,7 +682,7 @@
the click off the row-expand toggle. --> the click off the row-expand toggle. -->
<input <input
type="checkbox" type="checkbox"
class="w-3.5 h-3.5 flex-shrink-0 accent-accent cursor-pointer opacity-50 hover:opacity-100 {selected.has(item.id) ? 'opacity-100' : ''}" class="w-3.5 h-3.5 flex-shrink-0 accent-accent cursor-pointer opacity-70 hover:opacity-100 {selected.has(item.id) ? 'opacity-100' : ''}"
aria-label={`Select transcript ${item.title || item.id}`} aria-label={`Select transcript ${item.title || item.id}`}
checked={selected.has(item.id)} checked={selected.has(item.id)}
onclick={(e) => { e.stopPropagation(); toggleSelected(item.id); }} onclick={(e) => { e.stopPropagation(); toggleSelected(item.id); }}
@@ -786,7 +786,7 @@
<input <input
type="text" type="text"
class="w-full bg-bg-input border border-border rounded-lg px-3 py-2 text-[12px] class="w-full bg-bg-input border border-border rounded-lg px-3 py-2 text-[12px]
text-text placeholder:text-text-tertiary focus:outline-none focus:border-accent mb-3" text-text placeholder:text-text-tertiary focus:border-accent mb-3"
placeholder="Name this transcript..." placeholder="Name this transcript..."
value={item.title || ""} value={item.title || ""}
oninput={(e) => { item.title = e.target.value; }} oninput={(e) => { item.title = e.target.value; }}
@@ -828,7 +828,7 @@
<input <input
type="text" type="text"
class="bg-bg-input border border-border rounded-full px-2 py-0.5 text-[10px] class="bg-bg-input border border-border rounded-full px-2 py-0.5 text-[10px]
text-text placeholder:text-text-tertiary focus:outline-none focus:border-accent w-[110px]" text-text placeholder:text-text-tertiary focus:border-accent w-[110px]"
placeholder="+ add tag" placeholder="+ add tag"
onkeydown={(e) => handleAddTagKey(e, item)} onkeydown={(e) => handleAddTagKey(e, item)}
data-no-transition data-no-transition

File diff suppressed because it is too large Load Diff

View File

@@ -333,8 +333,8 @@
<button <button
class="text-[10px] px-2 py-0.5 rounded-md class="text-[10px] px-2 py-0.5 rounded-md
{checked {checked
? 'bg-accent/15 text-accent' ? 'bg-accent/25 text-accent border border-accent/30'
: 'text-text-tertiary hover:text-text-secondary'}" : 'text-text-secondary hover:text-text'}"
role="radio" role="radio"
aria-checked={checked} aria-checked={checked}
aria-label={opt.value ? `Set current energy to ${energyLabel(opt.value)}` : 'Clear current energy'} aria-label={opt.value ? `Set current energy to ${energyLabel(opt.value)}` : 'Clear current energy'}
@@ -346,8 +346,8 @@
<button <button
class="flex items-center gap-1 btn-md rounded-lg text-[10px] class="flex items-center gap-1 btn-md rounded-lg text-[10px]
{settings.matchMyEnergy {settings.matchMyEnergy
? 'bg-accent/15 text-accent border border-accent/30' ? 'bg-accent/25 text-accent border border-accent/30'
: 'text-text-tertiary hover:bg-hover hover:text-text border border-transparent'}" : 'text-text-secondary hover:bg-hover hover:text-text border border-transparent'}"
style="transition-duration: var(--duration-ui)" style="transition-duration: var(--duration-ui)"
onclick={toggleMatchMyEnergy} onclick={toggleMatchMyEnergy}
aria-pressed={settings.matchMyEnergy} aria-pressed={settings.matchMyEnergy}
@@ -377,7 +377,7 @@
<Search size={14} class="text-text-tertiary flex-shrink-0" aria-hidden="true" /> <Search size={14} class="text-text-tertiary flex-shrink-0" aria-hidden="true" />
<input <input
type="text" type="text"
class="flex-1 bg-transparent text-[12px] text-text placeholder:text-text-tertiary focus:outline-none" class="flex-1 bg-transparent text-[12px] text-text placeholder:text-text-tertiary"
placeholder="Search tasks..." placeholder="Search tasks..."
bind:value={searchQuery} bind:value={searchQuery}
data-no-transition data-no-transition
@@ -395,7 +395,7 @@
<Plus size={16} class="text-text-tertiary flex-shrink-0" aria-hidden="true" /> <Plus size={16} class="text-text-tertiary flex-shrink-0" aria-hidden="true" />
<input <input
type="text" type="text"
class="flex-1 bg-transparent text-[13px] text-text placeholder:text-text-tertiary focus:outline-none" class="flex-1 bg-transparent text-[13px] text-text placeholder:text-text-tertiary"
placeholder="Add a task to {activeListName}{activeBucket !== 'all' ? ` (${activeBucket})` : ''}... (Enter to save)" placeholder="Add a task to {activeListName}{activeBucket !== 'all' ? ` (${activeBucket})` : ''}... (Enter to save)"
bind:value={quickInput} bind:value={quickInput}
onkeydown={handleQuickAdd} onkeydown={handleQuickAdd}
@@ -458,11 +458,20 @@
<!-- Main content: sidebar + tasks --> <!-- Main content: sidebar + tasks -->
<div class="flex flex-1 min-h-0 px-7 pb-4 gap-3"> <div class="flex flex-1 min-h-0 px-7 pb-4 gap-3">
<!--
Layout-transition perf fix: was animating `width` directly. Now wraps
the list sidebar in a CSS-grid container whose `grid-template-columns`
track-size transitions between collapsed/expanded widths. Same visual
effect, single transitioning property the browser can handle as a
track-spec change rather than a width re-flow each frame.
-->
<!-- List sidebar --> <!-- List sidebar -->
<div <div
class="flex flex-col bg-bg-elevated rounded-2xl border border-border-subtle overflow-hidden class="grid {sidebarCollapsed ? 'grid-cols-[40px]' : 'grid-cols-[160px]'}"
{sidebarCollapsed ? 'w-[40px] min-w-[40px]' : 'w-[160px] min-w-[160px]'}" style="transition: grid-template-columns var(--duration-ui)"
style="transition: width var(--duration-ui)" >
<div
class="flex flex-col bg-bg-elevated rounded-2xl border border-border-subtle overflow-hidden min-w-0"
> >
<!-- Collapse toggle --> <!-- Collapse toggle -->
<button <button
@@ -485,7 +494,7 @@
<input <input
bind:this={editingInputEl} bind:this={editingInputEl}
type="text" type="text"
class="w-full bg-bg-input border border-accent rounded px-2 py-1 text-[10px] text-text focus:outline-none" class="w-full bg-bg-input border border-accent rounded px-2 py-1 text-[10px] text-text"
bind:value={editingName} bind:value={editingName}
onkeydown={(e) => { if (e.key === "Enter") finishRenaming(); if (e.key === "Escape") { editingListId = null; } }} onkeydown={(e) => { if (e.key === "Enter") finishRenaming(); if (e.key === "Escape") { editingListId = null; } }}
onblur={finishRenaming} onblur={finishRenaming}
@@ -545,7 +554,7 @@
bind:this={newListInputEl} bind:this={newListInputEl}
type="text" type="text"
class="w-full bg-bg-input border border-border rounded px-2 py-1 text-[10px] text-text class="w-full bg-bg-input border border-border rounded px-2 py-1 text-[10px] text-text
placeholder:text-text-tertiary focus:outline-none focus:border-accent" placeholder:text-text-tertiary focus:border-accent"
placeholder="List name..." placeholder="List name..."
bind:value={newListName} bind:value={newListName}
onkeydown={handleCreateList} onkeydown={handleCreateList}
@@ -561,6 +570,7 @@
</div> </div>
{/if} {/if}
</div> </div>
</div>
<!-- Task list --> <!-- Task list -->
<div class="flex-1 overflow-y-auto min-h-0"> <div class="flex-1 overflow-y-auto min-h-0">