Compare commits
7 Commits
claude/aut
...
749403697a
| Author | SHA1 | Date | |
|---|---|---|---|
| 749403697a | |||
| eb3d2f90ab | |||
| 6469663c25 | |||
| 22d554e85b | |||
| fa734c869f | |||
| d089fdb37f | |||
| 2da0a5bab8 |
39
src/app.css
39
src/app.css
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user