christin #9

Merged
christin merged 62 commits from christin into main 2026-07-21 11:14:36 +02:00
Owner
No description provided.
fix(ci): install jq and curl before forgejo-release in the docker:27 container
All checks were successful
Release / build-and-release (push) Successful in 4m26s
8df6ddb2fd
forgejo-release@v2 only installs jq/curl itself when missing, via a
hardcoded apt-get, which doesn't exist on this Alpine-based container
("apt-get: command not found"). Installing both ourselves makes its own
"which jq curl" check pass so it skips that fallback.
Root cause of the current "same soup" look: no semantic design tokens,
25/27 components hardcode raw Tailwind arbitrary classes, and light
mode is a ~700-line class-matching override file instead of a real
theme. This spec covers Phase 1 of 4: a Tailwind v4 @theme token
foundation (validated against the dataviz skill's palette checks),
plus the sidebar/nav and login/auth screens as the first components
migrated onto it. Later phases (dashboard/charts, composer, remaining
pages) get their own specs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Nine-task plan implementing the approved Phase 1 spec: token
foundation, shared card/button/input utility redefinition, sidebar
collapse persistence module (TDD), i18n keys, Sidebar.tsx and
Auth.tsx migration, App.tsx layout-width change, orphaned-override
cleanup, and release bookkeeping + manual verification checklist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extends the @theme block with surface/text/accent/status/chart tokens
(dark default) and their html[data-theme='light'] counterparts, per
docs/superpowers/specs/2026-07-20-redesign-phase1-foundation-design.md.
Purely additive — no component migrated yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements the spec's Card and primary-button contracts as shared
utilities instead of hardcoded hex values, and drops their now-redundant
light-mode class-matching overrides (the token redefinition from the
previous commit already covers both themes). Affects every component
that already uses these utilities, not just the ones this phase
migrates individually.
Active nav state now uses --color-accent-wash/--color-accent instead of
the tri-color gradient+glow-border treatment. Adds a collapse toggle
(persisted by the caller); sub-items and secondary labels hide in the
20px icon rail, parent buttons keep a title tooltip.

Note: App.tsx doesn't pass the new required props yet — this commit
does not build in isolation, the next commit (App.tsx wiring) fixes it.
Both are being merged together via the same PR.
The authenticated shell's banners, tab content, and footer now use the
full available width (padding still applies via <main>'s p-8) instead
of being capped at 1280px with unused space on wider screens. Auth
screens are unaffected — they were never inside this wrapper, they
already center themselves with their own max-w-md card.
Replaces the three floating gradient blobs with a single radial
accent-wash. All text/border/status colors now use semantic tokens.
Text-only buttons (forgot-password link, provider buttons, back links)
get an explicit bg-transparent so they read as secondary regardless of
the shared global button default, which Phase 1 intentionally leaves
untouched (see plan Global Constraints).
Code review of Task 7 (Auth.tsx button migration) found that the global
!important button rule in src/index.css (@layer base) can never be
overridden by Tailwind utility classes, because cascade layers reverse
!important precedence: an earlier layer's !important beats a later
layer's !important or normal declaration regardless of specificity.
Tailwind utilities compile into @layer utilities, which is later and
non-important, so no button's own classes could ever win. This
retroactively broke the already-committed Task 5 (Sidebar.tsx) as well,
confirmed empirically with Playwright (a button carrying Sidebar's exact
classes still rendered with the old navy gradient/white text).

Add a :not(.token-styled) escape hatch to the dark and light-mode global
button selectors (and the light-mode :hover variant), and mark every
button touched by this redesign phase (9 in Sidebar.tsx, 9 in Auth.tsx)
with the token-styled class so their own Tailwind classes apply. Also
fixes two contradictory classes found in review: the registration-complete
back-to-login button was missing bg-transparent, and the auth-provider
button had bg-transparent wrongly clobbering its intentional
bg-surface-sunken background.

Buttons without token-styled (Dashboard, Composer, Accounts, Settings,
admin pages, etc.) are unaffected since :not(.token-styled) still matches
everything the bare selector matched before.

Verified with npm run build (exit 0) and empirically with Playwright
against a live preview server, confirming the old gradient/white/800-weight
values no longer appear on migrated buttons.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes a Critical finding from the Task 7 re-review: the 2FA "send code
by e-mail" button in Auth.tsx was missed by the previous token-styled
rollout, leaving it subject to the legacy global button{...!important}
gradient/white-text/800-weight rule in index.css instead of its own
Tailwind classes.
These class-matching rules only ever existed to reskin classes that
Sidebar.tsx and Auth.tsx no longer emit after this phase's migration.
Rules still needed by unmigrated components (Dashboard, Composer,
Accounts, providers.ts badge colors, etc.) are untouched.

Verified with a literal (grep -F) full-src scan rather than the brief's
plain-regex check, since class strings like `bg-[#5BC8F5]/10` are
misinterpreted as BRE bracket expressions by unescaped grep and the
brief's checked-path list omitted src/utils, where provider badge
colors are dynamically referenced.
Critical: src/index.css's light-mode .gradient-btn override never got the
:not(.token-styled) escape hatch that 0caa60e added to the sibling generic
button rules, so token-styled gradient-btn buttons (Auth.tsx login/register/
forgot/reset/2FA submit) still rendered the old pale lavender-blue gradient
in light mode instead of --color-accent. Added :not(.token-styled) to the
.gradient-btn/button.gradient-btn selectors only, leaving the unmigrated
from-[#5BC8F5]/from-blue-600 selectors untouched. Verified empirically via
Playwright: light mode now rgb(124, 58, 237), dark mode unregressed at
rgb(144, 133, 233).

Minor: Sidebar.tsx pluginSections header still used hardcoded text-slate-500
instead of the text-text-muted token used by its migrated sibling
adminPluginSections.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Scopes Phase 2 to exactly the two Dashboard tabs the reference mockup
covers (Übersicht, Wachstum) rather than all 6 tabs/20 widgets in one
pass. Reuses all Phase 1 tokens/contracts, assigns the validated
chart-1..4 categorical palette in fixed order to the growth charts,
and reiterates the token-styled escape-hatch requirement for every
button touched (the expensive lesson from Phase 1). Leaner process
per Christin's explicit request: larger tasks, single review pass,
spot-check verification instead of exhaustive per-button checks.
Three larger tasks (topbar+accordion, stat cards+Top Posts, charts+
tooltip) instead of Phase 1's nine, per Christin's request for a
leaner process. Every button edit carries token-styled per the Phase 1
lesson. Single spot-check Playwright pass at the end instead of
per-task exhaustive verification.
Account/period selector buttons and the shared summary/table accordion
toggles (used by every widget) move off hardcoded cyan/violet hex onto
the Phase 1 token system. Every touched button gets token-styled so
the global button{!important} rule doesn't repaint it.
Stat-card top-stripe/icon colors now use the fixed chart-1..4 mapping
(Failed Posts uses status-critical, a state signal not a series
identity). Top Posts sort buttons, select, and list rows move onto
surface/text tokens; every touched button gets token-styled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
daily-engagement's 3 series and the two single-series growth charts
use the validated chart-1/2/3 hues in fixed order; follower-events and
weekly-growth's positive/negative bars use status-good/status-critical
(polarity, not identity). Tooltip styling reads CSS custom properties
via inline style objects (no cascade-layer risk, not a <button>).
Chart hex is dark-mode-only for now (recharts SVG props don't reliably
resolve custom properties here) — same colors render in light mode
too; full light-mode chart theming is a Phase 3 candidate if wanted.
Adds ENGAGEMENT_METRIC_COLORS (rose/blue/violet for Favourites/Boosts/
Replies), reused by the Engagement Breakdown donut and (retroactively)
Top Posts' icon colors, which were swapped between boosts/replies
relative to the donut since Phase 2 — now consistent everywhere.
weekday-engagement drops its 7-hue rainbow bar fill for a single
chart-2 green with an opacity accent on the best day. boost-reply-rate
and cumulative-engagement move off legacy cyan/violet hex onto the
chart-token palette. All widget titles/labels/empty-states in this
tab move from text-white/text-slate-* to the text-primary/-secondary/
-muted tokens.
New .card-inset utility (bg-surface-sunken, no border, per the Card
contract) replaces the repeated border-white/6-bg-white/2 nested-tile
pattern across hashtag-overview's stat tiles, hashtag-combinations'
combo cards, and top-hashtags' list/chart cards. Legacy cyan brand
accents (Hash icon, coverage progress bar, sort-toggle active state,
bar-chart fill) move to --color-accent/chart-3. All titles/labels move
to text-primary/-secondary/-muted tokens.
Heatmap empty-cell/low-sample colors (best-times, best-times-hourly,
posting-calendar) become theme-reactive CSS variables
(--color-heatmap-empty/-lowsample) instead of dark-only hardcoded
rgba — previously near-invisible in light mode. media-performance
reuses Task 1's ENGAGEMENT_METRIC_COLORS. visibility-breakdown's four
categories move onto the fixed chart-1..4 sequence in data order,
replacing an unordered ad hoc hex map. All titles/labels move to
text-primary/-secondary/-muted tokens.
Task 3's review found best-times, best-times-hourly, and
posting-calendar's axis numbers and less/more legend captions were
left on text-slate-500/600 instead of the text-text-muted token —
the widget titles and empty states were migrated but these smaller
caption labels were missed. All 11 occurrences fixed.
Confidence badges (high/medium/low) move from hardcoded emerald/amber/
slate to --color-status-good/-warning and a neutral surface-sunken
treatment for low confidence — confidence is a real state, matches the
Card contract's status-token rule. Action box uses --color-accent
(it's a call to action), Experiment box uses chart-2 green, giving
reason/action/experiment three distinct intentional colors instead of
two arbitrary ones. Tip cards, fact tiles, and example cards reuse
Task 2's .card-inset utility. Plugin-source badge and example-link
hover move off legacy cyan onto --color-accent.
Final whole-branch review flagged the experiment info-box's literal
#199e70 as the one non-theme-reactive spot in an otherwise fully
tokenized Tipps tab: on light-mode's near-white card background it
renders the dark-theme green at ~3:1 contrast (below WCAG AA for
small text), whereas the light chart-2 token (#0f9d58) reaches ~4.4:1.
text-chart-1/2/3 with opacity modifiers (from-chart-2/20, to-chart-2/5)
are already used and shipped in the Phase 2 stat cards, so
border-chart-2/15 bg-chart-2/5 text-chart-2 is a safe, proven
substitution — no new utility needed, the plan's stated reason for
avoiding it didn't hold up.
Small decorative SVG polyline, no recharts dependency — a ~32px trend
line needs none of ResponsiveContainer/axis/grid/tooltip machinery.
aria-hidden, matching how other decorative chart fills are already
treated in this app. Used by the new Dashboard landing page (Task 4).
New sidebar.overview, sidebar.themeMode, sidebar.profileMenu keys and
an updated sidebar.composer label (plural, matches the reference
mockup's exact text) across de/en/it. New dashboardHome.* namespace
for the upcoming Dashboard landing page (Task 4), mirroring the
existing dashboard.* namespace's key-naming conventions.
Behavior-preserving move, not a rewrite — Dashboard.tsx's Tipps tab
renders identically before and after. Done now because the new
Dashboard landing page (Task 4) needs the exact same tip-copy
resolution logic for its Tipps preview, and duplicating ~1070 lines
of tip-id-keyed business logic would create two copies that drift
apart the next time a tip type changes. Closures (locale-dependent
formatNumber/formatPercent/formatSignedNumber, pluginDiscovery,
language, t, dayNames) become explicit parameters since the function
no longer lives inside the Dashboard component. Note: dayNames was
not listed in the plan's original signature but is required by the
best_weekdays case blocks — added as an 8th parameter following the
same closure-to-parameter pattern as the other six.
Task 3's implementation found the moved function also depends on
dayNames, a real gap in this plan's original 7-param signature.
Updates Task 3's own call-site line and Task 4's not-yet-implemented
DashboardHome.tsx code to match the actual shipped 8-param signature.
New page matching the reference mockup's Dashboard panel: 4 stat
cards with Sparkline trend lines (Follower/Beiträge/Engagement/
Reichweite — Reichweite reuses the follower-count value, same
simplification the existing Total Reach stat card already uses, no
distinct backend reach metric exists), a Top-Beiträge preview (top 5,
links to Beiträge), a Follower-Wachstum chart, and a Tipps preview
(top 3, links into the Übersicht page's Tipps tab via the existing
dashboard_tab localStorage key). Compact account+period popover
replaces the bulkier two-row selector from the existing Übersicht
page, same underlying state/logic, new container shape only. Reuses
Task 1's Sparkline, Task 2's i18n keys, Task 3's shared tip-copy
utility. Not yet wired into App.tsx/Sidebar.tsx — Task 5.
Task 4's review flagged that a negative growth percentage (an account
losing followers) would still render in status-good green — the
polarity coloring pattern this codebase already uses elsewhere
(follower-events/weekly-growth bars, Phase 2) applies status-critical
for the negative case, status-good only for actual growth.
Sidebar gains an 'overview' nav item (existing six-tab Dashboard.tsx,
now reachable on its own instead of nested under 'Dashboard'), a
theme-mode toggle, and a profile card with a dropdown for the
previously-flat report-bug/logout rows — matching the reference
mockup's sidebar bottom section. App.tsx: 'dashboard' now renders the
new DashboardHome landing page (Task 4), 'overview' renders the
existing Dashboard.tsx unchanged. A small window-event bridge
(fedisuite-navigate) lets DashboardHome trigger tab switches without
needing a setActiveTab prop, keeping its prop shape identical to
Dashboard's own (accounts/postSummary/onAccountsRefreshed only).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Final whole-branch review found the new profile card (Task 5) shipped
with no identity — Sidebar accepted a userEmail prop but App.tsx never
passed one, so the avatar rendered "?" and the name/email line was
empty on every build. api.user.getProfile() was already called here
for timezone/theme/language; its response also carries email, so this
wires it through with one state variable and one prop, no new API
call. UserProfile has no separate display-name field on the backend,
so the card shows email only (matches Minor #2 from the review — not
independently fixable without a backend change, left as-is).
The docs/superpowers/ tree was already deleted; two leftover comments
still pointed at now-nonexistent spec files.
Dashboard (landing page) and Übersicht (the six analysis tabs) each held
their own independent selectedAccountId/selectedDays state. Since App.tsx
conditionally mounts/unmounts them on tab switch, picking a period on one
page never carried over to the other -- and reset on every navigation.
The reference mockup shows identical stat numbers on both pages, which
only makes sense if the selection is shared.

Lifts selectedAccountId/selectedDays (plus the existing server-side period
persistence) into App.tsx and passes them down as props. Both components
now render the same account/period instead of drifting apart. New
src/utils/period.ts holds the shared PERIOD_OPTIONS/DEFAULT_PERIOD so both
consumers can't diverge again.
Purely structural move, no CSS token migration yet: left column holds
account/platform selector, content editing, media, toolbar and thread
auto-split; right column holds the live-preview card and a scheduling
card, matching the target mockup's Inhalt/Vorschau+Planung split. Every
JSX block relocated verbatim -- no logic, state, handler, or content
changes. Dropped now-redundant per-block mb-4 margins in the left column
in favor of the new glass-card's space-y-6, for consistent spacing.
Account/platform selector, content editing, media strips, toolbar and
thread auto-split now use the semantic token system (text-text-*,
bg-surface-*, border-border-subtle, text-accent/bg-accent-wash) instead
of hardcoded slate/white-opacity/old-brand-gradient classes, matching
Dashboard.tsx's already-migrated account-selector pattern. All recolored
buttons carry token-styled per the established cascade-bug escape hatch.
Status colors (red/amber/yellow for errors/warnings/CW) intentionally
left as semantic colors, not accent.

Also fixes a genuinely pre-existing bug found during review: the
existing-media remove button used bg-red-500 without token-styled, so
the global button !important rule (added months after this button was
written) silently overrode it to the default gradient look. Now
bg-status-critical + token-styled, actually renders red as intended.
Preview card and scheduler+submit card now use the semantic token
system (text-text-*, border-border-subtle, text-accent/bg-accent-wash,
input-glass) instead of hardcoded slate/white-opacity/old-brand-gradient
classes, matching the left column's Task 2 conventions.

Adds a "Planung"/"Scheduling"/"Pianificazione" heading to the scheduler
card (new composer.schedulingSectionTitle i18n key, all three locales)
so it visually matches the mockup's labeled Planung card instead of
starting bare with the schedule toggle.

Submit buttons deliberately left untouched (no token-styled): they're
matched by :not(.token-styled) light-mode override selectors in
index.css for .gradient-btn and the blue/cyan secondary gradient --
adding token-styled would opt them out of overrides they still need.
The Sidebar's theme toggle only updated local React state via a raw
setTheme, never calling api.user.updateTheme -- so the choice silently
reverted on the next login, since App.tsx re-fetches and applies the
still-unchanged server-side theme on every login. Settings.tsx already
persisted correctly through its own save-button flow.

Adds a changeTheme wrapper (same fire-and-forget pattern as the existing
changePeriod) and wires it into Sidebar only; Settings keeps the raw
setTheme since it already persists before calling onThemeChange, so
routing it through the wrapper too would just double the network call.
Extracts the compact account+period picker out of DashboardHome.tsx into
a new shared src/components/AccountPeriodPicker.tsx, used identically on
both the Dashboard landing page and the Übersicht page. Two changes
requested by Christin:

- Each account row in the picker's dropdown now shows a platform badge
  (Mastodon/Pixelfed/etc.) next to the name, since users can run several
  Fediverse accounts with the same display name across platforms and
  couldn't tell them apart here before.
- Übersicht's old account-tile-grid + separate period-button-row topbar
  is replaced with this exact same picker component, so both pages now
  look and behave identically -- consistent with the account/period
  state they already share via App.tsx.
Follower, Beiträge, Engagement and Reichweite now show a colored
(green/red) percentage next to the value, matching the mockup.

- Follower: reuses the existing followerGrowthPercent (start vs end of
  the selected period's already-fetched snapshot series).
- Beiträge: same formula against the statuses field of that same
  snapshot series (already returned by getStatsHistory, no new fetch).
- Engagement: no cumulative snapshot exists for a rate metric, so this
  compares the second half of the period's daily engagement-rate series
  against the first half (already-fetched, no new fetch) as a trend
  proxy -- documented as such in a comment.
- Reichweite: reuses followerGrowthPercent, consistent with it already
  reusing the follower count as its value (no distinct reach metric).

Badges are hidden (not shown as a misleading "+0.0%") when there isn't
enough data to compute a real trend, via explicit null guards rather
than falling back to 0.
Same gap as the period/theme fixes: selectedAccountId lived only in
App.tsx's React state, so switching accounts in the Dashboard/Übersicht
picker never survived a reload or a new login -- App.tsx's existing
default-account effect would always re-pick users.default_account_id
(or the first account) instead.

Adds a third column in the same family as dashboard_period/theme:
users.dashboard_selected_account_id, deliberately separate from
default_account_id (the explicit "set as default" preference from the
Accounts page, via PUT /api/user/default-account) so merely browsing
accounts in the dashboard picker never overwrites that explicit choice.

- server/init-db.js: new idempotent column migration.
- server/index.js: GET/PUT /api/user/dashboard-selected-account
  (ownership-checked like default-account), plus nulling it out inside
  the existing account-deletion transaction.
- src/services/api.ts: matching getDashboardSelectedAccount/
  saveDashboardSelectedAccount methods.
- src/App.tsx: loads the persisted account once on login, and a new
  changeAccount wrapper (same fire-and-forget pattern as changePeriod/
  changeTheme) wired into both DashboardHome and Dashboard's
  onSelectAccount, replacing the raw setSelectedAccountId. The existing
  default-account-picking effect is left untouched and still correctly
  reconciles when the persisted account no longer exists.
The six tab buttons (Übersicht, Wachstum, Engagement, Hashtags,
Optimieren, Tipps) still used the old brand gradient
(from-[#5BC8F5]/to-[#9B5CF6], text-white, glow shadow) instead of the
established token system. Now bg-accent-wash/text-accent (active) and
text-text-secondary/hover:text-text-primary (inactive), the exact same
pattern Sidebar.tsx uses for its nav buttons, plus the mandatory
token-styled marker so the color actually renders.
Found while auditing buttons for Phase 4c: .gradient-btn's own background
(var(--color-accent)) has no !important, so the global
button:not(.token-styled) fallback rule always wins over it in dark mode
unless the button also carries token-styled -- the same cascade-layer
trap Phase 1 fixed once, just never closed for this utility class. Only
Auth.tsx happened to pair gradient-btn with token-styled; every other of
the ~20 gradient-btn call sites across the app (Accounts, Settings,
TwoFactorSettings, DataExport, Notifications, Composer, two modals) has
been silently rendering the old navy fallback in dark mode this whole
time, while looking correct in light mode (which already had its own
:not(.token-styled) carve-out). Same story for Composer's blue/cyan
secondary schedule/reschedule buttons.

Adds the missing dark-mode carve-outs, mirroring the existing light-mode
ones, so gradient-btn means "--color-accent fill" everywhere without
requiring token-styled on every call site. Fallback hex for the
blue/cyan gradient taken directly from this project's own Tailwind
build output to guarantee an exact match.
Borders removed from all card-like containers (post rows -> card-inset,
edit modal -> glass-card), search input -> input-glass, all colors
mapped to tokens, all 6 buttons token-styled (icon-only ones also get
icon-btn). Post-status badges mapped to status tokens where the status
is a real state (published=good, failed=critical, processing=warning),
scheduled/draft stay accent/neutral as informational rather than
warning states.

Review caught a scannability regression: scheduled-status, direct-
visibility, and thread-part badges had all collapsed to the identical
bg-accent-wash/text-accent, even though all three can appear together
on one post row. Differentiated them: direct visibility now uses the
previously-unused status-serious token (fits a restrictive-audience
setting), thread-part badge uses a neutral surface tint (purely
structural info, not a state), scheduled keeps accent as the primary
state indicator.
Same treatment as Postings: card-perimeter borders removed (plugin-
provider card + stat tiles -> card-inset, empty-state dashed border
dropped), badges lost their borders, in-card dividers retoned not
removed, auth-error alert box kept its border in status color. The 4
gradient-btn-only buttons deliberately left without token-styled per
the CSS fix in 25806e6; the other 4 (reconnect/disconnect/set-default)
all token-styled, icon-only ones also icon-btn.

Two review rounds caught what a plain hex/slate/white grep sweep
misses -- named Tailwind palette colors: the online-status dot
(bg-green-500 -> bg-status-good, a real status not a category color),
the "default account" text badge (bg-amber-500 -> bg-accent-wash,
unifying it with the adjacent star icon which already used accent for
the same concept), and the "importing" status pill silently losing its
border while its three sibling states (retry/completed/failed) kept
theirs -- restored for visual family consistency.
Settings.tsx, TwoFactorSettings.tsx, SessionManagement.tsx, DataExport.tsx
-- same treatment as Postings/Accounts: card-perimeter borders removed
(nested form/QR/recovery-code boxes -> card-inset, session/file rows ->
card-inset), alert/warning boxes kept their status-colored border,
gradient-btn/theme-choice-btn buttons deliberately left without
token-styled per the established CSS exemptions, every other button
that got new token colors is token-styled. QR code's functional
bg-white left untouched.

Danger Zone card treated as one large alert box (border-status-critical
kept on top of glass-card, matching the alert-exception pattern at
section scale rather than just inline banners).

Review caught a focus-ring inconsistency: three inputs (password-
mismatch confirm field, two danger-zone fields) were hand-rolled
instead of using input-glass, out of an unverified worry about
conflicting with a conditional error-state border. Verified via the
compiled CSS cascade order that input-glass + a conditional
border-status-critical override works cleanly with no conflict, so
switched all three to input-glass + override, matching every sibling
field's focus treatment instead of a thinner, harsher one-off ring.
AdminPanel.tsx, AdminPluginsPage.tsx, AdminUsersPage.tsx -- same
treatment as the rest of this series: card-perimeter borders removed
(markdown preview, plugin rows, expanded-row detail -> card-inset;
edit-user modal -> glass-card, dropping its #0f1117 hardcoded bg),
alert/warning boxes kept their status-colored border, hand-rolled
brand-gradient buttons replaced by gradient-btn (no token-styled, per
the 25806e6 CSS fix), every other button token-styled. Plugin's 6-state
status badge mapped to status/neutral/accent tokens matching the real
PLUGIN_LOAD_STATUS enum. Enable/disable plugin button keeps its
destructive-red cue when the action would disable an active plugin.
Notice-preview banner deliberately left on amber (verified against
App.tsx's real end-user banner, which is still unmigrated -- changing
the admin preview would make it lie about what users actually see).

Review caught a real token-value collision: the admin-role badge
(bg-accent-wash/text-accent) and the followers-count badge
(bg-chart-1/text-chart-1) rendered as the exact same color on the same
row, since --color-chart-1 is defined identically to --color-accent in
both themes (by design, chart-1 IS the accent hue). Moved the followers
badge to chart-3 to restore the distinction the original purple/cyan
badges had.
The visible label now reads just "Einklappen"/"Collapse"/"Comprimi" via a
new sidebar.collapseShort key; the aria-label stays fully descriptive
("Seitenleiste einklappen" etc.) since a shorter label there would hurt
screen-reader clarity for no visual benefit -- only the on-screen text
needed shortening.
Christin: all buttons app-wide must look like the Sidebar's flat
bg-accent-wash/text-accent style -- no "old gradient" look anywhere,
neither the differentiated multi-stop pastel gradient gradient-btn used
in light mode nor Composer's blue/cyan secondary-action gradient.

- index.css: gradient-btn now renders as a flat var(--color-accent)
  fill in BOTH themes via two carve-outs (dark theme-agnostic + a
  light-scoped copy, needed because the light generic button fallback
  carries an extra html[data-theme='light'] type-selector that beats a
  theme-agnostic rule on specificity -- caught empirically via a
  Playwright render check, not by specificity math alone, after an
  earlier version of this fix shipped with only the unscoped copy and
  silently regressed light mode). Removed two other now-dead carve-out
  blocks (selected-tiles/tabs gradient, single-accent-tinted-button
  gradient) after verifying via full-repo grep that no button anywhere
  still uses those literal old-brand-hex class combinations.
- Composer.tsx: converted the 3 blue/cyan secondary schedule/reschedule
  buttons to the same bordered-secondary pattern used everywhere else
  in this app (bg-surface-sunken border-border-subtle text-text-secondary
  + token-styled), removing the gradient concept from them entirely.
- PluginRuntimeSettingsCard.tsx: fixed a scope-badge/plugin-name-badge
  color collision found in review (both were bg-surface-sunken,
  indistinguishable) -- scope badge now bg-accent-wash/text-accent.

This makes every gradient-btn call site across the whole app (Accounts,
Settings, TwoFactorSettings, DataExport, Notifications, Composer, admin
pages, plugin cards, ...) uniform without touching any of their
individual classNames -- the whole point of fixing this once in CSS.
Notifications.tsx, PostAnalysisModal.tsx, FocalPointModal.tsx,
ComposerMediaTile.tsx, PluginComposerExtensions.tsx,
PluginRuntimeSettingsCard.tsx (already committed separately),
PluginSettingsCard.tsx, PluginWebSandbox.tsx -- the last unmigrated
components in the app. Same treatment as the rest of Phase 4c: card
borders removed (-> card-inset/glass-card), alert boxes kept a
status-colored border, gradient-btn buttons normalized (no
token-styled, per the CSS fix), everything else token-styled.

Notifications' sticky account-selector card and per-notification cards
retoned to match the established Composer.tsx account-tile and
SessionManagement.tsx unread-row patterns respectively, per Christin's
explicit request to make them consistent.

PluginWebSandbox.tsx (a sandboxed third-party HTML/script mounter) was
scoped to exactly 3 spots -- an imperative className assignment, a
loading indicator, an error banner -- everything else (script
execution, the window.FediSuitePluginWeb bridge, the CSS-scoping
engine for plugin-supplied stylesheets) deliberately untouched, diff
confirmed surgically small in review.

The three fieldInputClass()-sharing plugin-form components
(PluginComposerExtensions/PluginRuntimeSettingsCard/PluginSettingsCard)
were migrated together for cross-component consistency, using the same
input-glass + conditional border-status-critical/40 error pattern
verified in the Settings.tsx migration.
Christin: the single stacked column of 10 sections felt unclear now
that the page has full content width available. Restructured into two
columns: "Konto & Sicherheit" (Email, Password, 2FA, Sessions, Data
Export) and "Präferenzen" (Timezone, Theme, Language, plugin settings),
Danger Zone stays full-width below both. Pure JSX repositioning --
every card's internal content, handlers, and state are unchanged, only
where each card sits on the page moved. New settings.groups.account/
preferences i18n keys for the column headings.

items-start on the grid keeps each column's height independent instead
of CSS Grid stretching the shorter one. Single column below the lg
breakpoint.
Christin: the tip cards had multiple levels of nested card-inset boxes
(facts as mini-tiles, Reason/Action/Experiment as three colored boxes,
Examples as a card-inset wrapping individually-boxed examples) --
"verschachtelte Kacheln... sehr unübersichtlich". Asked for a genuinely
different, more intuitive layout, not just a recolor of the same
nested-box structure.

Each tip is now one flat card. Facts collapse into a single label/value
row separated by a border instead of a grid of mini-tiles.
Reason/Action/Experiment become three icon+label+text rows (HelpCircle/
CheckCircle2/FlaskConical, muted/accent/chart-2) with no background box
at all -- color is never the only signal, each also has an icon and a
translated text label. Examples become a flat divided list instead of
nested cards. The tips container itself is now a 2-column grid at the
xl breakpoint to use the page's full width.

Review confirmed the data layer (getTipCopy/getConfidenceBadge/the
tips.map loop and all its conditionals) is completely untouched --
purely a presentational rebuild.
Christin flagged accessibility as a top priority for this whole
redesign, specifically calling out the new Zeitraum-/Account-Wähler.
Audited it and the Sidebar's profile menu (both hand-rolled popovers
built earlier this session) and found neither had: aria-haspopup,
aria-controls linking trigger to panel, a way to close via Escape, or
close-on-outside-click -- a keyboard-only user who opened either had
no way to dismiss it except re-activating the trigger, and screen
readers had no indication these buttons open a popup or whether it's
open.

Added to both: aria-haspopup="true" + aria-expanded + aria-controls on
the trigger, refs on trigger and panel, a mousedown listener that
closes on outside click (verified not to race with the trigger's own
click handler), and a keydown listener that closes on Escape and
returns focus to the trigger so it's never left stranded. The account
picker's two choice groups (account list, period list) are now wrapped
in role="group" with aria-labelledby pointing at their visible
headings, so screen readers announce a "Konto"/"Zeitraum" boundary
instead of a flat, unannounced button list.

Review caught one more real gap: none of the buttons involved had any
focus-visible styling, falling back to the bare browser default
outline against glass-card backgrounds -- added the same
focus:ring-2 focus:ring-accent/70 focus:ring-offset-2 pattern already
used elsewhere in the app (Dashboard.tsx's accordion toggles).
Toast.tsx backs every toast notification and every destructive-action
confirm() dialog across the whole app -- high blast radius, worth
catching in this sweep even though it wasn't one of the files Christin
named directly. Same conventions as the rest of the redesign: toast
type colors now reuse the Settings.tsx Feedback-component pattern
(bg-status-X/10 border-status-X/25 text-status-X for success/error,
accent for info), confirm dialog panel -> glass-card (dropping its
hardcoded bg-[#0f1117] border), destructive confirm button matches the
established bg-status-critical/90 + token-styled pattern.

Also fixed a live cascade-bug instance: the toast dismiss (X) button
had no explicit background class at all, meaning it was still caught
by the global button:not(.token-styled) fallback -- forcing an
unwanted navy background chip around the icon and overriding its
intentional text-current color inheritance. Now token-styled icon-btn.

Added two safe-dismiss paths for the confirm dialog that didn't exist
before: Escape and clicking the backdrop both resolve through
handleConfirm(false) -- the safe "cancel" path, never the destructive
one -- so a keyboard-only user is never stuck in an open confirm
dialog with no way out except the two buttons.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
FediSuite/FediSuite-Docker-Image!9
No description provided.