Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
19f9893
feat(theme): add accent/surface theme stores + before-render applier
thalida Jul 12, 2026
61c2c56
feat(theme): add preset token blocks; derive glow-accent from --cc-ac…
thalida Jul 12, 2026
513d205
feat(theme): Appearance tab with accent/surface swatch pickers
thalida Jul 12, 2026
2418f9a
fix(theme): correct default swatch color, rainbow order, radiogroup f…
thalida Jul 12, 2026
f1b38a7
feat(layout): default left sidebar width to a third of the viewport
thalida Jul 12, 2026
9ab03d4
feat(layout): accent-color the active activity-bar icon, drop the lef…
thalida Jul 12, 2026
cb79ce7
feat(theme): keep active-icon fill; neutral surface colors for shortc…
thalida Jul 12, 2026
f36d7b3
fix(layout): standardize pane-header padding; soften selected-icon fill
thalida Jul 12, 2026
387da86
feat(modal): soft backdrop blur behind modal/loading overlays
thalida Jul 12, 2026
c93c65b
fix(layout): uniform 12px pane-header padding on all sides
thalida Jul 12, 2026
a1a6788
fix(layout): pane-header padding to 8px; tabs header right padding to…
thalida Jul 12, 2026
5b11252
feat(new-project): add example placeholders to the source input
thalida Jul 12, 2026
254e47d
feat(overview): exact date + relative age for Founded and Last Updated
thalida Jul 12, 2026
d0156cf
refactor(theme): centralize pane content inset on --cc-pane-inset (sp…
thalida Jul 12, 2026
a7d18b0
feat(modal): even (non-downward) darker shadow + faint full border
thalida Jul 12, 2026
353cf11
fix(landing): tighten landing-card gap to space-4
thalida Jul 12, 2026
10c8598
fix(theme): make surface swatches legible via a full-ladder gradient
thalida Jul 12, 2026
f4ebc22
feat(theme): show preset names under accent + surface swatches
thalida Jul 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/src/components/NewProjectForm/NewProjectForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ export function NewProjectForm({ allowLocalRepos, prefill, onSubmit }: NewProjec
aria-invalid={fieldError ? 'true' : undefined}
autoComplete="off"
spellcheck={false}
placeholder={
kind === SourceKind.Remote ? 'https://github.com/owner/repo' : '~/projects/my-repo'
}
value={source}
onInput={(e) => onSourceInput((e.target as HTMLInputElement).value)}
/>
Expand Down
6 changes: 4 additions & 2 deletions app/src/components/PaneHeader/PaneHeader.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
* file-preview — mounts this same DOM so they look identical and
* adding a pane is a single buildPaneHeader() call. */
.pane-header {
padding: var(--cc-space-6) var(--cc-space-6) var(--cc-space-6) var(--cc-space-7);
/* Uniform inset on all sides so the leading focus button and the trailing
close button sit equidistant from the pane edges. */
padding: var(--cc-space-4);
border-bottom: 1px solid var(--cc-border-subtle);
flex-shrink: 0;
display: flex;
Expand All @@ -24,7 +26,7 @@
* line (their active underline overlaps it). Folding the title bar + separate
* strip into one row is the vertical-space win. */
.pane-header--tabs {
padding: 0 var(--cc-space-6) 0 0; /* first tab sits flush with the pane edge */
padding: 0 var(--cc-space-4) 0 0; /* first tab sits flush with the pane edge */
align-items: stretch;
}
.pane-header--tabs .pane-tabs {
Expand Down
18 changes: 9 additions & 9 deletions app/src/layout/LeftSidebar/LeftSidebar.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
position: relative;
flex: 0 0 auto;
/* Resizable: the drag handle mutates --sidebar-width; CSS owns `width` so the
handle never fights an inline style. Default width = activity bar +
panel. */
width: var(--sidebar-width, 360px);
handle never fights an inline style. Default is a third of the viewport
(matching the right sidebar) until the user drags a width; min/max below
clamp it on narrow/wide screens. */
width: var(--sidebar-width, calc(100vw / 3));
min-width: 280px;
max-width: 600px;
height: 100%;
Expand Down Expand Up @@ -69,11 +70,9 @@
align-items: center;
justify-content: center;
cursor: pointer;
border-left: 2px solid transparent;
transition:
color var(--cc-t-base),
background var(--cc-t-base),
border-color var(--cc-t-base);
background var(--cc-t-base);
padding: var(--cc-space-0);
}

Expand All @@ -82,10 +81,11 @@
background: var(--cc-overlay-bg-soft);
}

/* Selected tab: accent-colored glyph + a faint accent fill (softer than the
standard -bg-soft tint), no left border. */
.activity-bar-icon.active {
color: var(--cc-text-primary);
border-left-color: var(--cc-accent);
background: var(--cc-accent-bg-soft);
color: var(--cc-accent);
background: color-mix(in oklch, var(--cc-accent) 6%, transparent);
}

/* Lucide glyph sizing: CSS width/height override the SVG's own size attrs so
Expand Down
4 changes: 4 additions & 0 deletions app/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

import { render } from 'preact';
import './styles/index.css';
// Applies the persisted accent/surface theme to <html> before the first
// render (no flash). persistedSignal hydrates synchronously, so the module's
// effect sets data-cc-* before Preact mounts.
import '@/state/stores/settings/theme';
import { App } from '@/layout/App/App';

const mount = document.getElementById('app');
Expand Down
2 changes: 1 addition & 1 deletion app/src/state/settingsDrafts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function _committedValue(store: SignalLike, key: DraftKey): unknown {

export function setDraft(store: SignalLike, key: DraftKey, value: unknown): void {
if (isAutosave(store as object)) {
// Write-through: apply immediately, never stage (Updates / Preview tabs).
// Write-through: apply immediately, never stage (Updates / Appearance tabs).
store.value = key === null ? value : { ...store.value, [key]: value };
_emit();
return;
Expand Down
2 changes: 1 addition & 1 deletion app/src/state/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ const _AUTOSAVE_STORES = new WeakSet<object>();

/** Mark a settings store as write-through: its widgets apply on change
* (bypassing the draft/Save layer) instead of staging drafts. Used by the
* autosave tabs (Updates, Preview) whose settings are cheap and want instant
* autosave tabs (Updates, Appearance) whose settings are cheap and want instant
* feedback. */
export function markAutosave(store: object): void {
_AUTOSAVE_STORES.add(store);
Expand Down
4 changes: 2 additions & 2 deletions app/src/state/stores/settings/syntaxTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ export const SYNTAX_THEME_OPTIONS: SyntaxThemeOption[] = [
export const SYNTAX_THEME_DEFAULT = 'atom-one-dark';
export const SYNTAX_THEME = persistedSignal<string>('SYNTAX_THEME', SYNTAX_THEME_DEFAULT);

// SYNTAX_THEME is a setting (it lives in the Preview tab) but uses a plain
// SYNTAX_THEME is a setting (it lives in the Appearance tab) but uses a plain
// persistedSignal rather than settingSignal, so register it explicitly.
markSettingStore(SYNTAX_THEME);
// Autosave (write-through): the Preview tab applies on change, no Save step;
// Autosave (write-through): the Appearance tab applies on change, no Save step;
// "Reset all" (World-only) skips it — see settingsDrafts.ts.
markAutosave(SYNTAX_THEME);
66 changes: 66 additions & 0 deletions app/src/state/stores/settings/theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// state/stores/settings/theme.ts — Interface theme: accent + surface presets.
// Two persisted choices applied by overriding root custom properties via
// data-cc-accent / data-cc-surface on <html>; themes.css holds the per-preset
// token blocks and every color-mix variant in tokens.css cascades from them.
// Mirrors syntaxTheme.ts: plain persistedSignal + autosave (no draft/Save
// step). The applier is a module-scope effect (not a component) so it runs
// before the first render — main.tsx imports this before render() and
// persistedSignal hydrates synchronously, so the first paint is already themed.

import { effect } from '@preact/signals';
import { persistedSignal } from '@/state/persist';
import { markSettingStore, markAutosave } from '@/state/settingsSchema';

export interface ThemePresetOption {
value: string;
label: string;
}

// Accent presets in rainbow (hue) order. `blue` is the default; the applier
// omits the attribute for it so tokens.css :root stays the page source (the
// picker chip still previews blue via themes.css's [data-cc-accent='blue']).
export const ACCENT_PRESETS: ThemePresetOption[] = [
{ value: 'amber', label: 'Amber' },
{ value: 'green', label: 'Green' },
{ value: 'cyan', label: 'Cyan' },
{ value: 'blue', label: 'Blue' },
{ value: 'purple', label: 'Purple' },
{ value: 'pink', label: 'Pink' },
];

// Surface presets — plain tint names. `cool` is the default.
export const SURFACE_PRESETS: ThemePresetOption[] = [
{ value: 'cool', label: 'Cool' },
{ value: 'neutral', label: 'Neutral' },
{ value: 'green', label: 'Green' },
{ value: 'warm', label: 'Warm' },
];

export const ACCENT_THEME_DEFAULT = 'blue';
export const SURFACE_THEME_DEFAULT = 'cool';

export const ACCENT_THEME = persistedSignal<string>('ACCENT_THEME', ACCENT_THEME_DEFAULT);
export const SURFACE_THEME = persistedSignal<string>('SURFACE_THEME', SURFACE_THEME_DEFAULT);

// These live in the Appearance tab but use plain persistedSignals, so register
// + mark autosave explicitly (settingSignal would do this implicitly).
markSettingStore(ACCENT_THEME);
markAutosave(ACCENT_THEME);
markSettingStore(SURFACE_THEME);
markAutosave(SURFACE_THEME);

// Apply to <html>. The default preset removes its attribute so tokens.css
// wins; any other preset sets it so themes.css [data-cc-*] blocks override.
function applyThemeAttrs(): void {
if (typeof document === 'undefined') return;
const root = document.documentElement;
const accent = ACCENT_THEME.value;
const surface = SURFACE_THEME.value;
if (accent === ACCENT_THEME_DEFAULT) delete root.dataset.ccAccent;
else root.dataset.ccAccent = accent;
if (surface === SURFACE_THEME_DEFAULT) delete root.dataset.ccSurface;
else root.dataset.ccSurface = surface;
}

// Runs synchronously at module eval and re-runs on either signal's change.
effect(applyThemeAttrs);
4 changes: 2 additions & 2 deletions app/src/styles/empty-state.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
align-items: center;
justify-content: center;
gap: var(--cc-space-4);
padding: var(--cc-space-8) var(--cc-space-7);
padding: var(--cc-space-8) var(--cc-pane-inset);
color: var(--cc-text-faint);
text-align: center;
}
Expand All @@ -31,7 +31,7 @@
margin-bottom: var(--cc-space-2);
}
.empty-state--lg {
padding: var(--cc-space-9) var(--cc-space-7);
padding: var(--cc-space-9) var(--cc-pane-inset);
}
/* The card title + subtitle render tightly inside the state card: kill the
margin and cap the subtitle width regardless of which text-* class they use. */
Expand Down
1 change: 1 addition & 0 deletions app/src/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
with its own .tsx. */
@import './reset.css';
@import './tokens.css';
@import './themes.css';
@import './text.css';
@import './atoms.css';
@import './panes.css';
Expand Down
8 changes: 8 additions & 0 deletions app/src/styles/modal.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
position: fixed;
inset: 0;
background: var(--cc-bg-backdrop);
/* Soft blur of the scene/UI behind the card. */
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
Expand All @@ -26,6 +29,11 @@
flex-direction: column;
overflow: hidden;
font-family: inherit;
/* A faint full border (header-divider color) lifts the modal off the blurred
backdrop, and an even, slightly darker shadow (no downward offset) rings it
equally rather than casting downward. */
border: 1px solid var(--cc-border-subtle);
box-shadow: 0 0 40px oklch(0 0 0 / 0.7);
}

.modal-header {
Expand Down
8 changes: 5 additions & 3 deletions app/src/styles/panes.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
shells that don't scroll themselves.
.pane-body — like .pane, but the body itself scrolls vertically. Used
when the contents need to scroll inside the pane.
.pane-body--padded adds inset padding.
.pane-inset — standard content inset (var(--cc-pane-inset)) for any pane or
panel content wrapper. The one shared padding class; every
panel uses it so the inset lives in one place.
═══════════════════════════════════════════════════════════════════════════ */

.pane,
Expand All @@ -27,7 +29,7 @@
overflow-y: auto;
}

.pane-body--padded {
padding: var(--cc-space-7);
.pane-inset {
padding: var(--cc-pane-inset);
box-sizing: border-box;
}
6 changes: 3 additions & 3 deletions app/src/styles/rows.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
display: flex;
align-items: center;
gap: var(--cc-space-2); /* default gap; --tight overrides to a tighter one */
padding: var(--cc-space-2) var(--cc-space-7);
padding: var(--cc-space-2) var(--cc-pane-inset);
border-radius: var(--cc-radius-sm);
cursor: pointer;
user-select: none;
Expand All @@ -35,8 +35,8 @@
/* Bleed variant — extends hover background to the pane edge. Used by
collapsible summaries inside the controls-pane. */
.row--bleed {
margin-left: calc(-1 * var(--cc-space-7));
margin-right: calc(-1 * var(--cc-space-7));
margin-left: calc(-1 * var(--cc-pane-inset));
margin-right: calc(-1 * var(--cc-pane-inset));
margin-bottom: var(--cc-space-2);
}

Expand Down
71 changes: 71 additions & 0 deletions app/src/styles/themes.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/* styles/themes.css — Interface theme presets (issue #87).

Each preset overrides its base token(s); every color-mix variant in
tokens.css cascades from them. Applied via data-cc-accent / data-cc-surface
on <html> (see stores/settings/theme.ts).

The default preset (blue / cool) has a block too, but only the swatch-picker
chips ever match it: the applier removes the attribute from <html> for the
default, so on the page tokens.css :root stays the source. Each chip carries
its own data-cc-* so it previews its true color even while another preset is
active; without a default block the default chip would inherit the active
theme. The blue/cool values here mirror tokens.css :root.

OKLCH only. Accents hold the default blue's L/C profile (accent L.70/C.16,
light L.80/C.10, dark L.59/C.20) and vary hue. Surfaces hold the lightness
ladder (app .151 / chrome .162 / sidebar .181) and vary hue/chroma, so
contrast against the fixed text ladder is preserved by construction. */

/* ── Accent presets ─────────────────────────────────────────────────────── */
[data-cc-accent='blue'] {
--cc-accent: oklch(0.697 0.16 258.2);
--cc-accent-light: oklch(0.806 0.097 260);
--cc-accent-dark: oklch(0.588 0.196 268.5);
}
[data-cc-accent='cyan'] {
--cc-accent: oklch(0.697 0.16 200);
--cc-accent-light: oklch(0.806 0.097 202);
--cc-accent-dark: oklch(0.588 0.196 205);
}
[data-cc-accent='purple'] {
--cc-accent: oklch(0.697 0.16 295);
--cc-accent-light: oklch(0.806 0.097 297);
--cc-accent-dark: oklch(0.588 0.196 300);
}
[data-cc-accent='green'] {
--cc-accent: oklch(0.72 0.15 150);
--cc-accent-light: oklch(0.82 0.1 152);
--cc-accent-dark: oklch(0.6 0.17 155);
}
[data-cc-accent='pink'] {
--cc-accent: oklch(0.7 0.17 350);
--cc-accent-light: oklch(0.81 0.1 352);
--cc-accent-dark: oklch(0.59 0.2 355);
}
[data-cc-accent='amber'] {
--cc-accent: oklch(0.78 0.15 85);
--cc-accent-light: oklch(0.86 0.1 88);
--cc-accent-dark: oklch(0.66 0.15 80);
}

/* ── Surface presets ────────────────────────────────────────────────────── */
[data-cc-surface='cool'] {
--cc-bg-app: oklch(0.151 0.011 276.2);
--cc-bg-chrome: oklch(0.162 0.015 278.3);
--cc-bg-sidebar: oklch(0.181 0.019 279.4);
}
[data-cc-surface='neutral'] {
--cc-bg-app: oklch(0.151 0.002 277);
--cc-bg-chrome: oklch(0.162 0.003 277);
--cc-bg-sidebar: oklch(0.181 0.004 277);
}
[data-cc-surface='green'] {
--cc-bg-app: oklch(0.151 0.012 155);
--cc-bg-chrome: oklch(0.162 0.016 155);
--cc-bg-sidebar: oklch(0.181 0.02 155);
}
[data-cc-surface='warm'] {
--cc-bg-app: oklch(0.151 0.012 55);
--cc-bg-chrome: oklch(0.162 0.016 55);
--cc-bg-sidebar: oklch(0.181 0.02 55);
}
6 changes: 5 additions & 1 deletion app/src/styles/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@
--cc-space-8: 24px;
--cc-space-9: 32px;
--cc-space-base: var(--cc-space-4);
/* Semantic: the inset from a pane/panel edge to its content. Pane-body
paddings, row insets, and full-bleed row margins all key off this, so
changing it here reflows every panel's content inset in lockstep. */
--cc-pane-inset: var(--cc-space-6);

/* ── SCALES — Radius ──────────────────────────────────────────────────── */
--cc-radius-0: 0;
Expand Down Expand Up @@ -183,5 +187,5 @@
--cc-shadow-lg: 0 10px 40px oklch(0 0 0 / 0.6); /* modal, loading-card */
--cc-shadow-base: var(--cc-shadow-md);
--cc-shadow-text: 0 1px 2px oklch(0 0 0 / 0.5); /* canvas overlay text */
--cc-glow-accent: 0 0 6px oklch(0.697 0.16 258.2 / 0.35); /* range-pair fill */
--cc-glow-accent: 0 0 6px color-mix(in oklch, var(--cc-accent) 35%, transparent); /* range-pair fill */
}
1 change: 0 additions & 1 deletion app/src/views/CommitPane/CommitPane.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
at the bottom so body load doesn't shift content above it. */

.commit-body {
padding: var(--cc-space-7);
display: flex;
flex-direction: column;
gap: var(--cc-space-4);
Expand Down
9 changes: 7 additions & 2 deletions app/src/views/CommitPane/CommitPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,12 @@ export function CommitPane({ state, onClose, onFocus }: CommitPaneProps) {

if (!commit) {
return (
<Pane paneClass="commit-pane" title="Commit" onClose={onClose} bodyClass="commit-body">
<Pane
paneClass="commit-pane"
title="Commit"
onClose={onClose}
bodyClass="commit-body pane-inset"
>
<PaneEmpty
icon={GitCommitHorizontal}
title="No commit"
Expand Down Expand Up @@ -161,7 +166,7 @@ export function CommitPane({ state, onClose, onFocus }: CommitPaneProps) {
onFocus={typeof onFocus === 'function' ? () => onFocus(commit) : undefined}
focusTitle={`Focus the camera on this commit (${KEY_BINDINGS.FOCUS_SELECTION.label})`}
onClose={onClose}
bodyClass="commit-body"
bodyClass="commit-body pane-inset"
>
<div class="commit-message-subject">{commit.subject || '(no subject)'}</div>
{(commit.authors ?? []).map((author) => (
Expand Down
2 changes: 1 addition & 1 deletion app/src/views/ControlsPane/ActionsBar/ActionsBar.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
align-items: center;
justify-content: space-between;
gap: var(--cc-space-4);
padding: var(--cc-space-5) var(--cc-space-7);
padding: var(--cc-space-5) var(--cc-pane-inset);
border-top: 1px solid var(--cc-border-subtle);
}
.controls-actions-left,
Expand Down
Loading