feat(ui): Appearance settings — dark/light/system mode and named themes; fix Add lane silently doing nothing (#42)

- Settings → Appearance: mode Dark / Light / System (follows the OS live) and
  a named theme remembered per mode — dark: Default, Darcula, Apple Dark;
  light: Default, Apple Light, IntelliJ Light, Canvas. Each theme is a
  CSS-variable override block keyed by `data-theme` on <html>; the sidebar
  quick toggle gains System and swaps between the two remembered themes.
- An inline boot script in index.html applies the stored mode and theme
  before first paint, so a reload no longer flashes the default colours.
- Diff +/- rows are easier to tell apart on every theme: a /20 status tint,
  a 3px status bar and a bold coloured marker; the changed-span emphasis no
  longer vanishes on light themes.
- #42: the Add-lane modal (Repo mode / "Current branch") asks
  POST /api/lanes/ensure with `exact: true`. Without it a folder nested inside
  an existing lane resolved to that parent lane with created:false, so the
  modal closed and no new card appeared. When the folder really already has a
  lane, the modal now says which one instead of closing.

Closes #42
This commit is contained in:
2026-09-24 09:44:42 +07:00
parent 2ea2184372
commit 6cf8a82889
33 changed files with 1756 additions and 59 deletions
@@ -6945,6 +6945,52 @@ exports[`screen snapshots > Settings 1`] = `
<div
class="flex items-center gap-1.5 overflow-x-auto no-scrollbar flex-1"
>
<button
class="inline-flex items-center gap-1.5 text-xs whitespace-nowrap px-2.5 py-1.5 rounded-lg border transition-colors flex-shrink-0 border-border text-fg-secondary hover:text-fg-primary hover:bg-surface-3"
type="button"
>
<svg
class="lucide lucide-palette w-3.5 h-3.5"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="13.5"
cy="6.5"
fill="currentColor"
r=".5"
/>
<circle
cx="17.5"
cy="10.5"
fill="currentColor"
r=".5"
/>
<circle
cx="8.5"
cy="7.5"
fill="currentColor"
r=".5"
/>
<circle
cx="6.5"
cy="12.5"
fill="currentColor"
r=".5"
/>
<path
d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"
/>
</svg>
Appearance
</button>
<button
class="inline-flex items-center gap-1.5 text-xs whitespace-nowrap px-2.5 py-1.5 rounded-lg border transition-colors flex-shrink-0 bg-accent/15 border-accent/30 text-accent"
type="button"
@@ -7342,6 +7388,702 @@ exports[`screen snapshots > Settings 1`] = `
</div>
</div>
</div>
<section
class="scroll-mt-24"
id="appearance"
>
<h3
class="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1"
>
<svg
class="lucide lucide-palette w-4 h-4 text-fg-muted"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="13.5"
cy="6.5"
fill="currentColor"
r=".5"
/>
<circle
cx="17.5"
cy="10.5"
fill="currentColor"
r=".5"
/>
<circle
cx="8.5"
cy="7.5"
fill="currentColor"
r=".5"
/>
<circle
cx="6.5"
cy="12.5"
fill="currentColor"
r=".5"
/>
<path
d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"
/>
</svg>
Appearance
</h3>
<p
class="text-xs text-fg-muted mb-4"
>
Colour mode and theme. Saved in this browser only.
</p>
<div
class="card p-5 space-y-5"
>
<div>
<p
class="text-xs font-medium text-fg-secondary mb-2"
>
Mode
</p>
<div
class="inline-flex rounded-lg border border-border bg-surface-2 p-0.5"
>
<button
aria-pressed="true"
class="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors bg-accent/20 text-accent"
type="button"
>
<svg
class="lucide lucide-moon w-3.5 h-3.5"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"
/>
</svg>
Dark
</button>
<button
aria-pressed="false"
class="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors text-fg-secondary hover:bg-surface-3 hover:text-fg-primary"
type="button"
>
<svg
class="lucide lucide-sun w-3.5 h-3.5"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="4"
/>
<path
d="M12 2v2"
/>
<path
d="M12 20v2"
/>
<path
d="m4.93 4.93 1.41 1.41"
/>
<path
d="m17.66 17.66 1.41 1.41"
/>
<path
d="M2 12h2"
/>
<path
d="M20 12h2"
/>
<path
d="m6.34 17.66-1.41 1.41"
/>
<path
d="m19.07 4.93-1.41 1.41"
/>
</svg>
Light
</button>
<button
aria-pressed="false"
class="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors text-fg-secondary hover:bg-surface-3 hover:text-fg-primary"
type="button"
>
<svg
class="lucide lucide-monitor w-3.5 h-3.5"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<rect
height="14"
rx="2"
width="20"
x="2"
y="3"
/>
<line
x1="8"
x2="16"
y1="21"
y2="21"
/>
<line
x1="12"
x2="12"
y1="17"
y2="21"
/>
</svg>
System
</button>
</div>
</div>
<div>
<p
class="text-xs font-medium text-fg-secondary mb-2"
>
Dark themes
<span
class="ml-2 text-fg-muted font-normal"
>
· in use now
</span>
</p>
<div
class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3"
>
<button
aria-label="Dark themes: Default"
aria-pressed="true"
class="rounded-lg border p-1.5 text-left transition-colors border-accent ring-2 ring-accent/40"
type="button"
>
<div
aria-hidden="true"
class="dark rounded-md border border-border bg-surface-0 p-2 text-fg-primary"
data-mode="dark"
>
<div
class="rounded bg-surface-3 p-2 shadow-[var(--shadow-card)] space-y-1.5"
>
<div
class="flex items-center gap-1.5"
>
<span
class="h-2 w-2 rounded-full bg-accent"
/>
<span
class="h-1.5 w-10 rounded-full bg-fg-primary/70"
/>
</div>
<div
class="font-mono text-[10px] leading-tight"
>
<span
class="text-syntax-keyword"
>
const
</span>
<span
class="text-syntax-function"
>
x
</span>
<span
class="text-syntax-operator"
>
=
</span>
<span
class="text-syntax-string"
>
"ok"
</span>
</div>
<div
class="flex gap-1"
>
<span
class="h-1.5 flex-1 rounded-full bg-status-success/40"
/>
<span
class="h-1.5 flex-1 rounded-full bg-status-danger/40"
/>
</div>
</div>
</div>
<span
class="block px-1 pt-1.5 text-xs text-fg-primary"
>
Default
</span>
</button>
<button
aria-label="Dark themes: Darcula"
aria-pressed="false"
class="rounded-lg border p-1.5 text-left transition-colors border-border hover:border-border-light"
type="button"
>
<div
aria-hidden="true"
class="dark rounded-md border border-border bg-surface-0 p-2 text-fg-primary"
data-mode="dark"
data-theme="darcula"
>
<div
class="rounded bg-surface-3 p-2 shadow-[var(--shadow-card)] space-y-1.5"
>
<div
class="flex items-center gap-1.5"
>
<span
class="h-2 w-2 rounded-full bg-accent"
/>
<span
class="h-1.5 w-10 rounded-full bg-fg-primary/70"
/>
</div>
<div
class="font-mono text-[10px] leading-tight"
>
<span
class="text-syntax-keyword"
>
const
</span>
<span
class="text-syntax-function"
>
x
</span>
<span
class="text-syntax-operator"
>
=
</span>
<span
class="text-syntax-string"
>
"ok"
</span>
</div>
<div
class="flex gap-1"
>
<span
class="h-1.5 flex-1 rounded-full bg-status-success/40"
/>
<span
class="h-1.5 flex-1 rounded-full bg-status-danger/40"
/>
</div>
</div>
</div>
<span
class="block px-1 pt-1.5 text-xs text-fg-primary"
>
Darcula
</span>
</button>
<button
aria-label="Dark themes: Apple Dark"
aria-pressed="false"
class="rounded-lg border p-1.5 text-left transition-colors border-border hover:border-border-light"
type="button"
>
<div
aria-hidden="true"
class="dark rounded-md border border-border bg-surface-0 p-2 text-fg-primary"
data-mode="dark"
data-theme="apple-dark"
>
<div
class="rounded bg-surface-3 p-2 shadow-[var(--shadow-card)] space-y-1.5"
>
<div
class="flex items-center gap-1.5"
>
<span
class="h-2 w-2 rounded-full bg-accent"
/>
<span
class="h-1.5 w-10 rounded-full bg-fg-primary/70"
/>
</div>
<div
class="font-mono text-[10px] leading-tight"
>
<span
class="text-syntax-keyword"
>
const
</span>
<span
class="text-syntax-function"
>
x
</span>
<span
class="text-syntax-operator"
>
=
</span>
<span
class="text-syntax-string"
>
"ok"
</span>
</div>
<div
class="flex gap-1"
>
<span
class="h-1.5 flex-1 rounded-full bg-status-success/40"
/>
<span
class="h-1.5 flex-1 rounded-full bg-status-danger/40"
/>
</div>
</div>
</div>
<span
class="block px-1 pt-1.5 text-xs text-fg-primary"
>
Apple Dark
</span>
</button>
</div>
</div>
<div>
<p
class="text-xs font-medium text-fg-secondary mb-2"
>
Light themes
</p>
<div
class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3"
>
<button
aria-label="Light themes: Default"
aria-pressed="true"
class="rounded-lg border p-1.5 text-left transition-colors border-accent ring-2 ring-accent/40"
type="button"
>
<div
aria-hidden="true"
class=" rounded-md border border-border bg-surface-0 p-2 text-fg-primary"
data-mode="light"
>
<div
class="rounded bg-surface-3 p-2 shadow-[var(--shadow-card)] space-y-1.5"
>
<div
class="flex items-center gap-1.5"
>
<span
class="h-2 w-2 rounded-full bg-accent"
/>
<span
class="h-1.5 w-10 rounded-full bg-fg-primary/70"
/>
</div>
<div
class="font-mono text-[10px] leading-tight"
>
<span
class="text-syntax-keyword"
>
const
</span>
<span
class="text-syntax-function"
>
x
</span>
<span
class="text-syntax-operator"
>
=
</span>
<span
class="text-syntax-string"
>
"ok"
</span>
</div>
<div
class="flex gap-1"
>
<span
class="h-1.5 flex-1 rounded-full bg-status-success/40"
/>
<span
class="h-1.5 flex-1 rounded-full bg-status-danger/40"
/>
</div>
</div>
</div>
<span
class="block px-1 pt-1.5 text-xs text-fg-primary"
>
Default
</span>
</button>
<button
aria-label="Light themes: Apple Light"
aria-pressed="false"
class="rounded-lg border p-1.5 text-left transition-colors border-border hover:border-border-light"
type="button"
>
<div
aria-hidden="true"
class=" rounded-md border border-border bg-surface-0 p-2 text-fg-primary"
data-mode="light"
data-theme="apple-light"
>
<div
class="rounded bg-surface-3 p-2 shadow-[var(--shadow-card)] space-y-1.5"
>
<div
class="flex items-center gap-1.5"
>
<span
class="h-2 w-2 rounded-full bg-accent"
/>
<span
class="h-1.5 w-10 rounded-full bg-fg-primary/70"
/>
</div>
<div
class="font-mono text-[10px] leading-tight"
>
<span
class="text-syntax-keyword"
>
const
</span>
<span
class="text-syntax-function"
>
x
</span>
<span
class="text-syntax-operator"
>
=
</span>
<span
class="text-syntax-string"
>
"ok"
</span>
</div>
<div
class="flex gap-1"
>
<span
class="h-1.5 flex-1 rounded-full bg-status-success/40"
/>
<span
class="h-1.5 flex-1 rounded-full bg-status-danger/40"
/>
</div>
</div>
</div>
<span
class="block px-1 pt-1.5 text-xs text-fg-primary"
>
Apple Light
</span>
</button>
<button
aria-label="Light themes: IntelliJ Light"
aria-pressed="false"
class="rounded-lg border p-1.5 text-left transition-colors border-border hover:border-border-light"
type="button"
>
<div
aria-hidden="true"
class=" rounded-md border border-border bg-surface-0 p-2 text-fg-primary"
data-mode="light"
data-theme="intellij-light"
>
<div
class="rounded bg-surface-3 p-2 shadow-[var(--shadow-card)] space-y-1.5"
>
<div
class="flex items-center gap-1.5"
>
<span
class="h-2 w-2 rounded-full bg-accent"
/>
<span
class="h-1.5 w-10 rounded-full bg-fg-primary/70"
/>
</div>
<div
class="font-mono text-[10px] leading-tight"
>
<span
class="text-syntax-keyword"
>
const
</span>
<span
class="text-syntax-function"
>
x
</span>
<span
class="text-syntax-operator"
>
=
</span>
<span
class="text-syntax-string"
>
"ok"
</span>
</div>
<div
class="flex gap-1"
>
<span
class="h-1.5 flex-1 rounded-full bg-status-success/40"
/>
<span
class="h-1.5 flex-1 rounded-full bg-status-danger/40"
/>
</div>
</div>
</div>
<span
class="block px-1 pt-1.5 text-xs text-fg-primary"
>
IntelliJ Light
</span>
</button>
<button
aria-label="Light themes: Canvas"
aria-pressed="false"
class="rounded-lg border p-1.5 text-left transition-colors border-border hover:border-border-light"
type="button"
>
<div
aria-hidden="true"
class=" rounded-md border border-border bg-surface-0 p-2 text-fg-primary"
data-mode="light"
data-theme="canvas"
>
<div
class="rounded bg-surface-3 p-2 shadow-[var(--shadow-card)] space-y-1.5"
>
<div
class="flex items-center gap-1.5"
>
<span
class="h-2 w-2 rounded-full bg-accent"
/>
<span
class="h-1.5 w-10 rounded-full bg-fg-primary/70"
/>
</div>
<div
class="font-mono text-[10px] leading-tight"
>
<span
class="text-syntax-keyword"
>
const
</span>
<span
class="text-syntax-function"
>
x
</span>
<span
class="text-syntax-operator"
>
=
</span>
<span
class="text-syntax-string"
>
"ok"
</span>
</div>
<div
class="flex gap-1"
>
<span
class="h-1.5 flex-1 rounded-full bg-status-success/40"
/>
<span
class="h-1.5 flex-1 rounded-full bg-status-danger/40"
/>
</div>
</div>
</div>
<span
class="block px-1 pt-1.5 text-xs text-fg-primary"
>
Canvas
</span>
</button>
</div>
</div>
</div>
</section>
<section
class="scroll-mt-24"
id="pricing"