#2533. theme.css claimed "removing this block is a rename sweep across the
components, tracked separately" — written in 67a529a, never filed, which made
the comment itself an instance of the survey's presence-without-reference
pattern. This is that sweep.
73 alias declarations deleted; 69 files rewritten; every --color-*-style name
now references its --fs-* token directly. Mechanical by construction: the map
IS the alias block, applied longest-name-first with a boundary guard so
--color-text never matched inside --color-text-muted. Zero survivors outside
theme.css, verified by grep rather than assumed.
One deliberate survivor: --color-shadow stays DECLARED, because it was never
an alias — it is a literal value the design system has no token for. Marked
in place as a recorded gap: promote it to an --fs-* token when a second app
needs it, don't copy the line.
Nothing is lost mode-wise: the aliases' resolve-at-use-time trick (which
absorbed 48 dark-mode overrides) lives one layer down in the --fs-* tokens'
own derivations, which is why the sweep is a pure rename. Both CSS checkers
green.
Why now rather than never: check_snippets_against_design_system reports every
--color-* reference as "unknown — renders as NOTHING", and nine recipe
snippets recorded from components.css carried the deprecated names, making
them prior art pointing the wrong way. With the sweep in, the checker's
report over re-recorded snippets should be EMPTY — the acceptance test that
proves the checker was right all along (#2517's correction).
Refs #2533
Operator reported four things looking wrong. Two were the same bug, and it is
not a design drift — it is deleted CSS.
Removing a rule from a scoped stylesheet leaves its modifiers behind. The
selector still exists, so nothing reads as unused, and the element renders with
no base styling at all:
.btn-workspace base gone, :hover survived — the Workspace link rendered
as raw browser blue, underlined
.milestone-header base gone, .clickable and :hover survived. Every child is
written for a flex ROW (.ms-name { flex: 1 }, the progress
track, .ms-pct), so without the parent they stacked and a
one-line milestone became five. That is the "projects
section uses space poorly" — a deletion, not a redesign.
.milestone-group no rule at all; the card around each milestone
.ds-header only its h1 descendant survived
vue-tsc cannot see any of it. A dead style typechecks perfectly.
scripts/check_dangling_styles.py finds the shape: an element whose every static
class has no base rule anywhere, while at least one carries modifier rules. It
reports 11 more. Reported and not gated, because a genuinely bare wrapper is
legitimate — the signal is the count growing. Runs in the lint lane, stdlib
only, and knows no class name or convention (rule #115).
Also from the same report:
- The header pill bar was `position: absolute; left: 50%`, so it did not
participate in layout: out of room, it OVERLAPPED the brand and the utility
cluster instead of pushing them. A sixth link reached that at ~1270px, an
ordinary window. Now `1fr auto 1fr` — a 1fr track has an auto minimum, so
neither side can be squeezed under its content and the two stay equal, which
is what keeps the bar centred in the viewport rather than in the leftover
space. Overflow becomes the header growing, not two things sharing pixels.
- The token preview put its checkerboard on the whole specimen stage, so every
swatch sat in a frame of checks and the pattern read as the loudest thing on
the page. The checks now sit UNDER the colour as a second background layer:
an opaque value hides them, a 15% tint shows exactly as much as it should.
Text-bearing specimens lose the box entirely, and name/value/purpose are one
line each with the full text on hover — they wrapped freely before, so a card
was two lines tall or five depending on how long its color-mix() happened to
be, and the grid had no rhythm.
- .btn-cta joins the shared button family: the gradient-and-glow brand moment
the system carries tokens for, which had been living in one view's scoped
block. That is what made it deletable. The header actions are now one size
and one family instead of four sizes and two.
- The shared button shape gained inline-flex + gap, so a button carrying an
icon centres it without each caller rebuilding the row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
#2277 counted ~150 "raw colour literals bypassing the tokens". Measuring them
told a different story: 184 sat in `var(--token, #fallback)` position, and a
check against theme.css shows every one of those tokens IS declared. So the
fallbacks could not render. Not drift — vestigial.
They were also not this palette. The most common were Tailwind and Flat-UI
defaults — #6366f1 indigo, #22c55e green, #f59e0b amber, #3b82f6 blue,
#e74c3c and #27ae60 — a second, unsanctioned colour scheme sitting in the
codebase looking like the app's colours to anyone reading it.
Removing them is not tidying. #2319's lesson is that a fallback is WORSE than a
missing token: a missing token renders as nothing and someone eventually
notices, while a fallback renders something plausible forever. These 184 were
one token rename away from silently repainting the app in Tailwind. The design
token check would catch the rename — but the fallback is precisely the thing
that would make it invisible if the check were ever bypassed.
Literal count 152 -> 45, which matters beyond the number: a report that is
mostly unreachable noise is one people stop reading, and then it stops working
while still passing. What remains should be genuinely worth looking at.
Done with a paren-aware transform, not a regex — `var(--x, rgba(0,0,0,.5))`
nests parens and `[^)]+` would cut at the first one and leave `))` behind.
Verified after: every changed line is a fallback strip and nothing else, and
every var() reference still resolves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
editor-shared.css defined thirteen button rules with hand-written geometry.
Ten are now thin aliases onto the shared variants — same class names, because
these are used across six views and pointing a name somewhere is cheaper than
rewriting every call site (the .btn-small precedent).
Three were DEAD: .btn-assist-toggle, .btn-close-assist and .btn-toggle-view had
no template reference and no dynamic binding anywhere in the app. Verified
before deleting rather than assumed from the name — a class with no user is
indistinguishable from one bound dynamically until you look.
Named honestly in the file: CSS has no @extend, so each alias carries the
variant's declarations rather than inheriting them. That is duplication this
migration cannot remove. But it is duplication of a REFERENCE — var(--color-
action-primary) — not of a value, so a palette change still moves everything at
once, which is the property that actually mattered.
Also gone: eight hardcoded geometries (0.4rem 1rem, 0.85rem, and so on) that
now come from --fs-space and --fs-size tokens.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
The five auth views each defined .btn-submit identically — full-width, filled,
0.6rem — and LoginView additionally defined .btn-oauth. Those rules are now
gone entirely rather than tokenised: the template composes `btn-primary
btn-block` and `btn-ghost btn-block`, and there is nothing left per-file to
drift.
That is the difference between this and the earlier chunks. Consolidating the
core four moved geometry into one place but left every semantic name defining
its own; this removes the definition.
Two variants added, both earned rather than invented:
- .btn-text — no fill, no border. The most common shape in the dense surfaces
(dismiss, cancel-beside-confirm, clear-search) where a border would draw a
box around something that should read as an action on the adjacent text.
Distinct from ghost, which IS a box.
- .btn-danger-outline — already existed independently in three views before
this sheet, which is what makes it a variant and not a one-off. It is what a
delete looks like when it must not shout.
.btn-block composes with a variant rather than being one, because width is
orthogonal to appearance.
Also corrected the sheet's own header, which claimed "no template changes" —
true when it was written, false as of this commit. It now states the actual
model: a button is variant + size, composed in the template. Semantic per-view
names are named as the thing that drifted, and why: a name says what a button
is FOR and nothing about what it should look like, so two buttons doing the
same job in two views had no reason to match, and didn't.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
Measured before deciding: across the ~100 bespoke button rules, vertical
padding does not spread — it clusters. ~27 at 0.4–0.45rem, ~28 at 0.25–0.3rem,
~23 at 0.1–0.15rem. Those are three different components that happen to share a
name prefix: a page action, a row action, and an affordance that lives inside a
card.
Collapsing them to the single size the shared sheet had would have visibly
broken every card layout, which is why the one-off migration stopped here for a
decision rather than proceeding on the assumption that a button is a button.
default 8px 16px page action — what the house style specifies
.btn-compact 4px 12px toolbar, table row, list item controls
.btn-inline 2px 4px dismiss ×, confirm tick, add-chip
.btn-small and .btn-sm already sat at the compact step, so they are kept as
aliases for it — no template churn, and the two spellings stop being a third
thing that might drift.
.btn-inline is deliberately below the spacing scale's first step on the
vertical axis: 4px of padding on an 11px label already exceeds the line box
these sit in. Stated in the file so it reads as a measured exception rather
than someone ignoring the scale.
DesignView renders all three as real specimens. A size scale described in prose
is one nobody can check; rendered from the actual classes, it cannot claim
something the app does not do.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
21 buttons filled with --color-primary, which resolves to Scribe's violet
accent: every auth Submit (Login, Register, Invite, Forgot, Reset), plus
Invite, Add, Confirm, Restore, Generate, Log-save, Subtask-confirm,
Toggle-open, the modal primary, the version-restore, the inline-assist button,
the milestone-plan actions, the task-advance hover, and both empty-state CTAs.
The house style is explicit that the accent never appears on an action button:
action colours are universal across the family precisely so a Save button looks
identical in every app, while the accent carries identity. Doing both makes the
accent mean two things and neither clearly.
Operator's call, and the reasoning is worth keeping: the violet-on-Scribe-
actions treatment was a deliberate early choice to give the web UI its own
personality, made when much more of the app was user-facing. That is no longer
true, so consistency is now worth more than the distinction it was buying.
Found in two passes, which is the part worth noting. The first scan looked for
`.btn-*` and found 13. Seven more were the same thing under different names —
.modal-btn-primary, .vh-btn-restore, .inline-assist-btn, .empty-action,
.task-advance-btn — plus .ms-plan-actions .btn-primary, a compound override
flagged in the previous commit. Searching by naming convention finds what was
named consistently, which is never the whole set.
Deliberately NOT changed: progress-bar fills, active tab / page / selection
states, tag-pill hover, the duration badge, the skip link, the assist pulse.
Those are identity and active-state, which is exactly where the accent belongs.
After this the accent appears only there, which is what makes it read as
identity rather than as decoration.
Colour swaps only — 25 lines changed, no geometry, no structure, no templates.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
`.btn-primary` was defined five times in five scoped stylesheets and all five
had drifted: three paddings, three font sizes, three disabled opacities — and
ProjectListView had no disabled style at all, so a disabled button there looked
enabled. Nothing detected any of it. A scoped duplicate is not a rule
violation, not a broken reference, and not a recorded snippet, so no existing
check could see it (#2273).
assets/components.css is now the single definition of the core four —
primary, secondary, ghost, danger — plus the small modifier, in design-system
tokens throughout. Operator's call to align to the system rather than to the
majority of current values, so buttons move to the 8px radius and 12px label
the system specifies, from the app's 4px/14.4px.
Class names are unchanged, so there are no template edits: the existing surface
is repurposed, not rebuilt.
STAGING PROPERTY that makes this safe to land ahead of the rest: a Vue
`<style scoped>` rule compiles to `.btn-primary[data-v-…]` (specificity 0,2,0)
and beats a plain global selector (0,1,0). So the shared sheet changes nothing
for a view still carrying its own copy, and every intermediate state of the
remaining migration is coherent rather than half-applied.
Two divergences corrected on the way, both worth naming:
- SnippetEditorView's `.btn-secondary` was a GHOST in disguise — outline
styling under the secondary name, while the house style says secondary is
filled bronze. It now looks like what it is called.
- SnippetDetailView and SnippetListView tinted a ghost button's label with the
ACCENT on hover. The house style reserves the accent for identity and active
state, never general chrome.
DesignView reported "no shared button exists" as an honest gap and declined to
draw a look-alike. That gap is closed, so it now renders the app's real
classes — the specimens cannot drift from the app without drifting the app.
Net -177 lines. Follows: the ~20 semantic one-offs (.btn-save, .btn-delete,
.btn-toggle …) and the compound overrides in ProjectView, one of which puts the
accent on a primary action button.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
76 hardcoded `color: #fff` now resolve to --fs-text-on-action, a new token
that is parchment in BOTH modes.
The design system said they should supersede to --fs-text-primary, on the
recorded reasoning that "there is no 'text on action' colour, there is just the
text colour." That is true on dark and wrong on light. --fs-text-primary
inverts to #14171A; the surfaces underneath it do not invert at all — every one
of these 76 sits on an action colour, a semantic colour, the accent, or the CTA
gradient, all of which hold a single value across modes.
Sweeping as recorded would have put obsidian text on moss green: roughly 2.4:1,
against a house style whose stated floor is WCAG AA. It would have looked
correct to me, because I checked it in the mode where it was correct.
--color-accent-fg had the same defect independently and is repointed too.
The token check now reports zero superseded literals, down from 30 files, and
raw colour literals drop 246 -> 169.
Closes#2275.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
Closes the gap the theme.css repoint exposed (#2319, part of #2277).
`check_code_against_tokens` could always answer "does this code use the sheet
correctly?" — it was only ever fed recorded SNIPPETS. The app's own components,
where sixteen unresolvable references were living quietly, were checked by
nothing at all.
That was structural rather than an oversight: the drift panel runs in the browser
and cannot read source files, and the server has no repo access. CI is the only
place holding both the sources and the ability to run the check — and it only
became cheap once theme.css became a generated artifact, so the source of truth
is a committed file with no network and no credentials.
**The sheet now carries its own SUPERSEDES block.** That is what keeps the
checker instance-agnostic (rule #115): it knows nothing about any palette, and
reads both the declarations and the discouraged literals out of whatever
stylesheet it is pointed at. Hardcoding "#fff means use the text token" would
have baked one install's kit into the tool.
Two severities, split on whether the count is already zero:
FAIL an unresolvable var() reference — zero today, so this is a ratchet
holding a line already reached. It cannot false-positive either: the
name is declared or it is not.
REPORT superseded literals (32 files) and raw colour literals (246). Gating
those means a permanently-red job, and a check nobody reads is worse
than no check.
**Comments are stripped before scanning, and that fired on the first real run.**
A comment explaining why a literal is avoided necessarily contains that literal —
DesignSystemsView's stylesheet documents exactly that about `#fff`, and the
checker reported the explanation as a violation. A checker that flags the
documentation of a rule teaches people to stop documenting rules.
Also narrowed `--fs-weight-medium`'s supersedes to the keywords. `600` and `700`
are real violations of the two-weight rule, but a bare number matches too much to
find by literal scan — `z-index: 600` is not a font weight. That needs a
property-aware check, which is a different tool.
Verified end to end: the generator's output parses back through the checker's
reader, so the two halves cannot drift into disagreeing about the format.
theme.css is now generated from design system 2 (Scribe, inheriting FabledSword)
plus a compatibility alias layer, so the record decides the styling rather than
describing it after the fact.
**Dark is now the base layer.** The kit is dark-mode-first, so `:root` carries
the dark palette and `[data-theme="light"]` overrides it — the inverse of how
this file read before. `useTheme` already sets the attribute explicitly to
"light" or "dark", so the flip needed no JS change. It also closes the one-way
scoping gap #251 recorded: there IS a `[data-theme="light"]` block now, so a
container can add light as well as dark.
**60 dark overrides became 12.** The other 48 were restating relationships the
derivations now express: `--color-bg` follows `--fs-surface-page` because an
alias resolves at use time, so it needs stating once rather than per mode.
## What the audit found, which is the actual deliverable
**12 dead tokens, removed.** Declared in both modes, referenced by nothing:
seven from the removed chat subsystem (bubbles, input bar), plus `--glow-soft`,
`--color-action-ghost-border`, `--radius-pill` and two chat widths. 17% of the
file was styling a feature that no longer exists.
**16 names referenced but NEVER declared** — not by this change, not by the file
before it. Fourteen carried hardcoded fallbacks, so pages rendered and nothing
ever failed, but the fallback was what rendered, every time. Several were off
the palette entirely:
--color-primary-bg fell back to rgba(99,102,241,0.15) — an indigo
--color-destructive fell back to #b85a4a — not the oxblood
--color-status-cancelled fell back to #6b7280 — a grey from no palette here
--color-muted fell back to #888
All 16 now resolve to real tokens. Expect small visual shifts exactly where a
fallback had drifted; the shift is the fix.
**Two tokens the app needed and never had**: `--fs-status-cancelled` (Scribe has
had a cancelled task status since the lifecycle was built and never had a colour
for it) and `--fs-layout-header` (referenced with a 52px fallback, so 52px was
always the real value — just not one anybody could look up).
## The system grew to cover what the app improvised
Per the operator: the kit wasn't growing with the app, and this is the result.
Recorded as tokens with GAP RECORDED FROM PRACTICE in their rationale — action
hover states, disabled opacity, the modal scrim, both code backgrounds, the
table stripe, the CTA gradient and glows, the accent-deep and accent-wash tints,
and the layout dimensions.
Scribe's own system gained its domain semantics — task status, priority, overdue,
wikilink — all DERIVED from family colours, so twelve rows of duplicated hex
became twelve formulas and zero new values. Priority maps onto the semantic
ladder deliberately: low is info, medium is warning, high is error.
95 tokens resolved, none valueless, 34 derived, no broken references, no cycles.
Per the design rule "italic is for emphasis, not for design", removed
every chrome `font-style: italic` declaration across the frontend.
42 declarations gone across 24 files: empty-state placeholder copy,
loading messages, "no results" hints, ghost text, voice/role labels,
field placeholder text, brand wordmark, et al.
Markdown content emphasis is unaffected — `<em>` and `<i>` tags from
`*emphasis*` markup still render italic via browser default styling
(prose.css doesn't override em behavior). User-typed emphasis in
notes, journal entries, and chat messages keeps its italic.
Specific spots that lost the decorative italic:
- KnowledgeView .filter-label, .empty-narrator
- NoteEditorView .ef-label, link-suggest related
- ChatPanel .empty-msg, .empty-greeting, .role-label
- ChatMessage .role-assistant .role-label (the "Fable" voice tag —
was italic per the doc's Illuminated Transcript spec, but per the
new typography rule the speaker tag stays regular and lets the
border-left + glow do the bubble framing on their own)
- AppHeader .brand-text ("Fabled" wordmark)
- editor-shared.css .title-input::placeholder
- ProjectView .project-title-input::placeholder
- HomeView .urgency-loading
- WorkspaceTaskPanel .empty-group
- WeatherCard .weather-unavailable
- SharedWithMeView .empty-msg
- DiffView empty-state spans
- ToolCallCard .tool-event-more
- SettingsView 7 spots (.you-label, .geo-pending, hint text, etc.)
- + several other empty-state / hint text spots
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per the surface-phase spec for the Notes/Tasks viewers and editors:
Long-form line-height
- prose.css: bumped global .prose from 1.6 to 1.7. Applies to Note
viewer body, Task viewer body, anywhere markdown renders into a
reading surface. Chat assistant bubble already had the explicit
override; now consistent with the rest.
Button reclassification per Hybrid rule
- Shared editor-shared.css:
- btn-save: accent gradient → Moss (action-primary). Saving is
"operating the software", not a brand moment.
- btn-delete: --color-danger (Error terracotta) → Oxblood
(action-destructive). Layout updated for inline-flex so the
Trash2 icon at call sites lines up alongside the label.
- NoteEditorView, TaskEditorView: Delete buttons now contain a
Trash2 icon per Hybrid's "destructive paired with icon" rule.
- NoteViewerView, TaskViewerView:
- btn-edit (and TaskViewer's btn-advance): accent gradient → Moss.
Switching to edit / advancing status are workflow actions.
- btn-convert, btn-share: ghost-on-hover-to-accent → Bronze
action-secondary (alternate paths).
Two-weights-only
- Snapped every font-weight: 600/700 to 500 across editor-shared.css
and the five Knowledge-cluster views.
Out of scope for this PR (deliberate punt)
- Smaller utility buttons (.btn-suggest-tags, .btn-link-all,
.btn-add-subtask, the AI-assist generate/proofread/accept/reject
set, etc.) — currently ghost-styled, generally compliant. Will
revisit only if they read off in practice.
- Filter chip / sub-task list-row border audit — deferred since
current styles already lean on background tint for affordance.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Applies the FabledSword + Scribe iteration palette to theme.css end to
end: indigo (#7c3aed) → dusty violet (#5B4A8A) accent, cool grey →
Obsidian/Iron/Pewter dark surfaces, warm parchment (#F5F1E8) light
mode, Inter body + JetBrains Mono code loaded alongside Fraunces, and
neutral hairline scrollbars (chrome is structural, not branded).
Adds the action token set (--color-action-primary Moss,
--color-action-secondary Bronze, --color-action-destructive Oxblood,
--color-action-ghost-border Pewter) but does not yet reclassify any
buttons — surface-phase work. Buttons remain dusty-violet gradients in
the meantime, by design.
Removes deprecated --color-accent-warm; replaces concrete usages with
--color-text-secondary (dates, flavor copy) or --color-warning (paused
status). Sweeps hardcoded indigo literals in component scoped CSS so
they don't bypass the token system.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase B of chat view refinement. Bumps chat reading column to 1200px,
lifts the RAG scope chip out of the footer and into the ChatView header
(next to the title), and collapses the listen toggle / volume slider /
stop-playback buttons into a single speaker popover on the input bar.
Listen mode + TTS are now available in briefing and workspace chats too,
since the controls live on the shared ChatInputBar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces the full-variant flex layout with a single CSS grid owning the
messages column, input bar, and context sidebar. Messages and input bar
share a centered ~820px reading track (--chat-reading-width token) so
the mic button can't get pushed off-screen on wide monitors, and the
context sidebar spans grid-row 1/-1 so it runs the full height from the
chat header down to the bottom edge instead of stopping above the input
bar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- renderMarkdown() accepts interactiveCheckboxes option: removes disabled=""
and stamps data-task-index on each checkbox in the marked HTML output
- NoteViewerView detects list notes by body content (- [ ] / - [x] pattern)
and passes interactiveCheckboxes: true when rendering
- onBodyChange() handles checkbox change events: toggles the matching line
in the body, optimistically updates the store, then PATCHes the note
- prose.css adds .prose--checklist rules for marked output: no bullet,
flex row, accent-color, line-through on checked items via :has()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Define --color-surface in theme.css (light: #f0f0f8, dark: #1a1b22)
— was used across 10+ components with no-op fallbacks; now properly
defined alongside --color-bg and --color-bg-card
- Extract shared date formatters into utils/dateFormat.ts:
fmtTime, fmtDateTime, fmtRelativeDateTime, fmtDayLabel, fmtCompact
- Replace duplicate inline formatters in CalendarView, HomeView
(formatUpcomingTime), and KnowledgeView (formatEventDate)
- CalendarView: replace hardcoded rgba(99,102,241,0.4) hover colour
with color-mix(in srgb, var(--color-primary) 40%, transparent);
fix --color-input-bg fallback to use var(--color-bg); remove
hardcoded hex fallbacks now that --color-surface is defined
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- theme.css: darken user bubble text (#3a3a4a light / #b0b0c8 dark) for better readability
- AppHeader: highlight Chat nav link when on any /chat/* route (was only active on exact /chat)
- summary.md: document workspace, graph, queue persistence, ToolCallCard direct-API, dedup scoping
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Design direction: "Illuminated Transcript" — the AI response is the
primary artifact; user prompts are subordinate margin notes.
ChatMessage.vue:
- User bubble: ghosted background (rgba 3-4% opacity), muted text,
thin near-invisible border. De-emphasized, recessed.
- Assistant bubble: left indigo accent border (2px), elevated shadow
via --color-bubble-asst-shadow. Commands attention.
- Assistant role label: indigo (var(--color-primary)); user label muted.
- Removed old white-override content rules (no longer on dark bg).
- Thinking block: full border → left-only indigo accent bar (35% opacity),
removed inner top border.
ChatView.vue:
- Streaming bubble: same left accent + shadow as assistant bubbles.
- Streaming role label: indigo to match ChatMessage.
- Thinking block in streaming: same treatment as ChatMessage.
theme.css:
- Added --color-bubble-user-bg/border/text for light + dark.
- Added --color-bubble-asst-shadow for light + dark.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- App.vue: e → edit on viewer pages; / → focus search (custom event); c → focus
chat on home or navigate to /chat; update shortcuts panel with Lists + Chat sections
- theme.css: add a:focus-visible to focus-ring rules (router-link cards now show
visible keyboard focus outline)
- SearchBar.vue: expose focus() via defineExpose for cross-component focus dispatch
- NotesListView / TasksListView: j/k vim-style navigation with kb-active-item
highlight; listen for shortcut:focus-search; cleanup on unmount
- HomeView.vue: listen for shortcut:focus-chat, call chatInputRef.focus()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace pure white backgrounds with off-white indigo-tinted values to
reduce glare, and switch the primary/accent color from Google blue to
the app's brand indigo (#6366f1) for consistency with the header and
email templates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend security & correctness:
- Add rate_limit.py: sliding-window rate limiter (asyncio) applied to login,
register, forgot/reset password endpoints (10/60s or 5/300s per IP)
- app.py: add security headers in after_request (X-Frame-Options, CSP,
X-Content-Type-Options, Referrer-Policy) using setdefault to preserve SSE headers
- auth.py: refactor duplicate login_required/admin_required into shared _check_auth()
- config.py: add TRUST_PROXY_HEADERS for proxy-aware client IP resolution
- routes/auth.py: rate limiting, _client_ip() helper, cleaned-up reset_password route
- routes/chat.py, notes.py, tasks.py: int() DoS fix on last_event_id; limit capped
at 500; date.fromisoformat() wrapped in try/except → 400 on invalid dates
- services/auth.py: fix Setting.user_id update bug (filter on NULL not user.id);
reset_password_with_token returns int|None (user_id) instead of bool
- services/backup.py: add _security_notice to full backup JSON export
- services/assist.py: system prompt explicitly preserves markdown list structure
and nested indented sub-items
Infrastructure:
- docker-compose.yml: add healthcheck on app service (/api/health, 10s interval)
- .dockerignore: prevent secrets/node_modules/__pycache__/.env.* leaking into build
Frontend bug fixes:
- TaskCard.vue, TaskViewerView.vue: fix isOverdue() timezone bug (ISO string compare)
- useAssist.ts: accept() now resets state to idle when document changed since proposal
- stores/chat.ts: fix memory leak in _pollUntilLoaded() (try/catch around fetchStatus)
- TiptapEditor.vue: selection offset uses closest-match strategy (not first-match)
- utils/markdown.ts: explicit DOMPurify config with FORCE_BODY; remove as const
(DOMPurify expects mutable string[])
New features:
- Auto-save (5-minute interval) in NoteEditorView and TaskEditorView — only when
editing an existing dirty record; silent on error, shows "Auto-saved" toast
- sectionParser.ts: top-level bullet/numbered list items are now individual sections
in the AI Assist panel (previously treated as one undifferentiated block)
- editor-shared.css: extracted ~500 lines of CSS duplicated between both editors;
includes .inline-assist-btn at global scope (required for teleported elements)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace plain textarea with Tiptap (ProseMirror) WYSIWYG editor for notes
and tasks. Headings, bold, lists, and code blocks now render inline while
editing. Tags and wikilinks get visual highlighting via decoration plugins,
and autocomplete uses @tiptap/suggestion with heading disambiguation.
Layout: pin navbar with app-shell flex column (100dvh), sticky editor
toolbar above scrolling content, AI Assist panel fixed at bottom 1/3 with
full-height section selector and prompt. Increase max-width to 960px
across all views. Hide assist controls during streaming/review.
Other fixes: markdown paste handling, auto-syncing assist sections via
body watcher, trailing newline on AI accept, ChatView height fix.
Add README.md describing the app, usage guide, and technical overview.
Update summary.md with Phase 5.2 roadmap and current status.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Improve chat context: build_context() now returns metadata about auto-found
notes, emitted as an SSE event so the frontend can display context pills
showing which notes influenced the response. Users can promote notes for
deeper context (+) or exclude irrelevant ones (x). A note picker lets users
manually attach notes. Multi-word search uses per-term AND matching, and
auto-search iterates keywords individually for broader OR-style coverage.
Standardize styling: introduce CSS design tokens (--radius-sm/md/lg/pill,
--color-success/warning/overlay, --focus-ring) and migrate all components
to use them. Fix header alignment to full-width, add active nav link state,
replace hardcoded colors with CSS variables, and normalize button padding.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>