Commit Graph

5 Commits

Author SHA1 Message Date
Anso eead195529 feat(settings): dress the page to match the audit (#849)
* feat(settings): dress the page to match the audit (cyan rail, italic serif, two-column rows)

Brings the full-page Settings route into the Sencho voice. The page now
opens with a full-width PageMasthead (cyan rail, mono crumb, italic
serif title, contextual stat strip) above a sidebar and main-content
panel, each as a rounded-xl card inset on the dark background.

Sidebar drops the duplicate "Settings" header and the candy tier badges.
Group headers carry mono labels with visible/total counts; gated rows
get a neutral uppercase lock chip and dim. Active rows keep the cyan
2px rail.

Five new primitives (SettingsSection, SettingsField, SettingsCallout,
SettingsActions / SettingsPrimaryButton, TierLockChip) replace the
stacked label-input-help shadcn defaults and the per-section ad-hoc
chrome. AccountSection, AppearanceSection, LicenseSection, SystemSection,
NotificationsSection, DeveloperSection, AppStoreSection, AboutSection,
and SupportSection are migrated to the new layout. The list-driven
sections (Webhooks, Routing, Users, Labels, Security, CloudBackup,
ApiTokens, Registries, NodeManager, SSO) keep their list cards but get
the new chrome and primary CTAs.

Each section can publish contextual stats to the masthead via a small
context channel: 2FA state on Account, plan/trial/renews on License,
edited count on System, channel counts on Notifications, etc.

* refactor(settings): drop react-router-dom and align with DESIGN.md

The Settings page was the only surface using react-router-dom for sub-section
navigation. Every other primary view (Home, Fleet, Resources, App Store,
Schedules, etc.) drives view switching through a single activeView useState in
EditorLayout. This change removes the dependency end-to-end:

- App.tsx drops BrowserRouter
- EditorLayout adds 'settings' to the activeView union; SettingsPage renders
  inside the same flex-1 overflow-y-auto p-6 wrapper as siblings
- UserProfileDropdown receives an onOpenSettings callback instead of
  useNavigate. SettingsPage owns currentSection via props lifted to
  EditorLayout, so cross-component navigation (openLabelManager,
  onManageNodes, ConfigurationStatus rows) can route to a sub-section
- SettingsSidebar items become buttons (no more NavLink); SectionGate's
  redirect-on-invisible falls back through SettingsPage's safeSection memo
- e2e/nodes.spec.ts updates the Nodes selector from link to button role
- react-router-dom removed from package.json + package-lock.json

The visual treatment is brought into alignment with frontend/DESIGN.md,
which was rewritten this week to be the normative extract of the audit:

- PageMasthead: title text-3xl → text-[22px] Section rung italic; kicker
  11px → 10px Label rung; stat label tracking 0.22em → 0.18em; stat value
  font-medium for mono Stat-rung family discipline
- SettingsField helper: mono → sans Body rung 14/22; success tone now uses
  --success green (was incorrectly mapped to brand cyan)
- SettingsCallout: title tracking 0.18em; subtitle Body rung 14px; success
  tone now genuinely uses --success green; new brand tone for promotional
  callouts (Trial CTA, Admiral upgrade) that should read cyan
- SettingsActions: SettingsPrimaryButton renders mono uppercase tracked,
  size sm by default. DESIGN §9.10 requires "small mono uppercase, cyan-
  filled" for every Settings primary CTA
- TierLockChip: 9px → 10px Label rung floor
- SettingsSidebar: group header tracking 0.18em; ⌘K kbd 9px → 10px;
  aside gains text-card-foreground transition-colors per §10 canonical
  card class
- SettingsPage main panel: text-card-foreground transition-colors added;
  uses h-full overflow-auto p-6 to mirror FleetView's wrapper rhythm
- Field rows, section headers, action rows now consume var(--density-*)
  tokens with literal fallbacks so Settings respects the comfortable/
  compact toggle

* fix(e2e): update mfa openAccountSettings to match settings redesign

Settings now opens to the Account section by default when accessed from
the profile dropdown, and the Account section no longer renders an h2
heading element. Update the openAccountSettings helper to open the
correct section and assert on the Password h3 heading that SettingsSection
renders instead.

* test(e2e): fix MFA enrolment assertion after settings redesign

The 2FA enrolment badge was replaced with a kicker/field pattern.
Assert on the 'enrolled' text that the new design renders instead of
the removed Enabled badge.

* test(e2e): fix low-backup-codes warning assertions after settings redesign

Update two assertions in the 'low backup codes warning' test that
referenced UI text removed in the settings redesign:
- '1 backup code remaining' -> '1 remaining' (SettingsField body text)
- 'Regenerate now' button -> callout subtitle text, which uniquely
  identifies the zero-codes error card without hitting strict-mode
  from two identically-labelled Regenerate buttons on the page

* test(e2e): navigate to root before re-opening settings for mock refresh

The settings redesign uses a nested full-page route. Navigating to the
same URL a second time does not remount the component, so AccountSection
retains cached MFA state and the 0-codes branch never fetches. A
page.goto('/') ensures full unmount before the second openAccountSettings
call, so the refreshed mock is actually hit.

* test(e2e): scroll zero-codes callout into view before asserting visibility

The callout sits below the Disable 2FA section in the MFA settings page
and is scrolled out of the clipped content area on initial render.
scrollIntoViewIfNeeded() brings it into the visible viewport before the
toBeVisible assertion.

* test(e2e): scroll Radix ScrollArea viewport for zero-codes callout assertion

The settings page wraps content in a Radix ScrollArea whose Root has
overflow:hidden, so the browser's native scrollIntoView cannot scroll
the inner viewport. Wait for the callout to attach (confirms mock data
loaded), then programmatically set scrollTop on the Radix viewport
element before asserting visibility.

* test(e2e): use toBeAttached for zero-codes callout to avoid Radix clip issue

The callout renders below the Disable 2FA section, outside the visible
clip area of the Radix ScrollArea Root (overflow:hidden) on a standard
viewport. Playwright's visibility check uses the clip intersection, so
toBeVisible() fails even after programmatic scroll. toBeAttached()
confirms the component rendered the warning card for backupCodesRemaining:0
without depending on the element's scroll position.
2026-04-30 19:37:38 -04:00
Anso 9a1c043189 refactor(settings): replace modal with nested full-page route (#848)
* refactor(settings): replace modal with nested full-page route

Settings sections are now URL-addressable at /settings/:sectionId, rendered
nested inside EditorLayout alongside the stack sidebar. Browser back/forward
navigates between sections. Deep links (e.g. /settings/cloud-backup) load
the section directly on hard reload.

- Add react-router-dom v7; BrowserRouter wraps the full app tree
- New SettingsPage (scroll memory, Cmd+K palette), SettingsSidebar (NavLink
  active styling, back-arrow), SectionGate (visibility + tier lock card)
- Rename SectionId 'appstore' to 'app-store' so slug === SectionId
- Decouple SystemSection, DeveloperSection, AppStoreSection from modal-
  passed props; each fetches its own data on mount
- Replace onLabelsChanged prop chain with SENCHO_LABELS_CHANGED window event
- Drop onOpenSettings prop from UserProfileDropdown, HomeDashboard,
  ConfigurationStatus; each calls useNavigate directly
- Delete SettingsModal.tsx

* fix(settings): validate sectionId against registry before property write

Prevents prototype pollution (CodeQL js/remote-property-injection #243).
URL param sectionId is checked against SETTINGS_ITEMS before being used
as a property key on scrollPositionsRef.

* fix(settings): eliminate remote property injection via Map and registry-sourced key

Two-part fix for CodeQL js/remote-property-injection:

1. currentSection is now derived from SETTINGS_ITEMS.find().id (trusted
   registry data) instead of the raw sectionId URL param. The tainted
   string never flows into any property access.

2. scrollPositionsRef uses Map<SectionId, number> with .get()/.set()
   instead of a plain object. Map operations do not write to the prototype
   chain, removing the prototype pollution vector entirely.

* test(e2e): align settings selectors with full-page route

The settings refactor (4475afd) replaced the modal with a nested route.
The new sidebar renders sub-sections as NavLinks (role link, not button)
and adds a "Filter settings" button that collides with the loose
/settings/i regex used in mfa and nodes specs.

- Use exact 'Settings' match for the profile-dropdown menu row
- Switch the Nodes sub-section selector from button to link role
2026-04-30 12:57:02 -04:00
Anso 0bf061a745 feat(settings): group sections, add ⌘K search, scope breadcrumb (#680)
* feat(settings): group sections, add ⌘K search, scope breadcrumb

Restructures the Settings Hub sidebar into four labelled groups
(Identity, System, Alerts, Advanced), adds a ⌘K command palette for
section search, and surfaces the active scope (global vs node-scoped)
in the content breadcrumb.

- New `settings/registry.ts` centralises group/item metadata, tier gates,
  glyph assignments, visibility rules, and keyword hints consumed by both
  the sidebar and the command palette
- Cyan 2px left rail + gradient on the active sidebar item; mono-uppercase
  group headers; tier chips inline for locked items
- Scoped ⌘K handler via onKeyDownCapture on DialogContent so the hub no
  longer hijacks the global sidebar shortcut while open
- ScrollArea gains an opt-in `block` prop so the Nodes management table
  can overflow horizontally without Radix's default `display: table`
  wrapper clipping action buttons
- Docs reference updated with the grouped sidebar, scope breadcrumb, and
  ⌘K walkthrough plus refreshed screenshots

* refactor(settings): drop duplicate section headers, redesign system limits, always-visible tier chips

- Remove redundant section titles in every settings page; the dialog header now owns the title and description
- Rework System Limits into a compact row panel with inline-edit chips (warn state, focus ring) and an ON/OFF toggle pill
- Show tier chips on sidebar and command palette whether locked or unlocked, so Skipper/Admiral scope is always legible
- Keep right-aligned action buttons on pages that had a title+button header (Users, Labels, Nodes, API Tokens, Registries)

* fix(settings): seed NumberChip draft on edit instead of via effect

ESLint rule react-hooks/set-state-in-effect flagged the sync effect that
mirrored the external value into local draft state. Replace it with a
startEdit handler that seeds draft from value at click time, so the
button path always reads value directly and no cascading render is
triggered on prop change.

* fix(settings): restore heading role and clean sidebar accessible names

- Wrap the settings dialog title in an h2 so screen readers and E2E locators see a heading again after the in-section headers were removed
- Mark the sidebar glyph aria-hidden so the button's accessible name is just the item label (fixes anchored name matchers)
- Align the MFA E2E helper with the renamed Account section heading
2026-04-18 16:17:24 -04:00
Anso d393d06885 Replace Sonner with custom Sera UI-inspired toast system (#296)
* refactor(ui): replace Sonner with custom Sera UI-inspired toast system

Replace the Sonner toast library with a custom implementation inspired by
Sera UI's glassmorphism design. The new system uses an external store pattern
with useSyncExternalStore for React integration, keeping the same
toast.success()/error()/warning() API so all 172 call sites required only
an import path change.

Key changes:
- New toast-store.ts: singleton store with identical API to Sonner
- New toast.tsx: Sera UI-faithful Notification component with Framer Motion
  animations, frosted glass (backdrop-blur-xl), type gradient overlays,
  animated progress bar (green→blue→sky gradient), and hover:scale-105
- Removed sonner and next-themes dependencies
- Rewired all 19 consumer files to import from the new store

* fix(ui): resolve ESLint errors in toast system

- Use const for listeners Set (prefer-const)
- Initialize startRef with 0 instead of Date.now() to satisfy
  react-hooks/purity rule, set actual value inside useEffect
2026-03-31 21:34:49 -04:00
Anso 7637091e84 feat(ui): glassmorphism redesign with settings decomposition (#274)
* feat(ui): add glassmorphism design tokens and utility classes

Introduce glass design system foundation: translucent oklch color
variables for both light and dark themes, glass/glass-border/glass-highlight
tokens, semantic status colors (success/warning/info), .glass and
.glass-strong utility classes with backdrop-filter, reduced shadow
values, and standardized spring animation presets in lib/motion.ts.

* feat(ui): apply glass treatment to core components

Update card, dialog, input, button, popover, sheet, tooltip,
dropdown-menu, context-menu, select, alert-dialog, and tabs
components with glassmorphism styling: translucent backgrounds via
new CSS variables, backdrop-blur layers, glass-border luminous
edges, and glass-highlight hover states.

* refactor(settings): decompose Settings Modal into section components

Extract 10 inline sections from the 1,987-line SettingsModal into
dedicated files under components/settings/. Introduce section
registry pattern replacing 14 conditional blocks. Add shared types,
sidebar navigation grouping with separators, glass treatment on
sidebar and nav buttons, and responsive modal height. SettingsModal
shell shrinks to ~380 lines.

* refactor(ui): unify all tabs to animate-ui TabsHighlight with glass styling

Migrate 4 tab instances (EditorLayout, FleetView, ResourcesView,
NotificationsSection) from inconsistent patterns (manual layoutId,
underline border-b-2, default fade) to the shared TabsHighlight
primitive with glass-highlight indicator and springs.snappy
transition. Standardize EditorLayout nav highlight spring config,
apply glass-highlight to sidebar stack list hover/active states,
and update mobile nav styling.

* refactor(ui): migrate hardcoded colors to semantic CSS variables

Replace hardcoded Tailwind color classes across ~19 component files
with semantic CSS variable classes: emerald/green to success,
orange/amber to warning, blue to info. Preserves brand/decorative
colors (Crown amber, Admiral blue). Enables consistent theming of
status indicators across the entire application.

* refactor(ui): Linear dark precision aesthetic — solid surfaces, depth cues, text hierarchy

Replace glassmorphism with Linear.app-inspired design: solid surface tokens
(card #111111, sidebar #0d0d0d, root #0a0a0a), backdrop-blur restricted to
floating overlays only (blur(10px) saturate(1.15)), desaturated teal accent,
font-weight 500 everywhere, monochrome chart palette, and three depth cues:
root ambient glow, luminous card top-edge, steep text brightness ramp.

* refactor(ui): precision polish — fix muddy dark, snowblind light, add design anchors

- Replace 34 hardcoded rgba values with theme-aware stat-* CSS tokens
- Fix light theme: solid white cards, off-white background, readable text
- Add card-border tokens with sharper directional lighting (top edge 2x)
- Add chart-grid/chart-tick tokens for theme-aware axis rendering
- Upgrade body glow: teal-tinted (dark), warm amber (light)
- Terminal-inspired sidebar: monospaced UP/DN status codes, Geist Mono
- Add tabular-nums to stat values to prevent layout jitter
- Light mode cards get shadow-sm for depth against off-white background

* refactor(ui): Linear materiality pass — ghosted nav, translucent sidebar, font unity

- De-escalate Delete button from solid destructive to ghost with hover fill
- Make sidebar translucent (80% opacity + backdrop-blur) so body glow bleeds through
- Bump dark nav accent to 0.07 for ghosted backlit selection
- Unify all terminal/editor fonts to Geist Mono (was JetBrains/Consolas mix)
- Add Monaco editor fontFamily for YAML/env editing consistency
- Add threshold-based color to Host RAM and Host Disk stat values (warn/crit)

* refactor(ui): material simulation — inherent depth, layer separation, recessed terminal

- Bump dark background 0.065→0.08, card surfaces 0.10→0.12 for 4% layer separation
- Add card-bevel token (inset top shimmer) for permanent structural depth
- Add button-inner-glow token for physical key feel on outline buttons
- Recess terminal with inset shadow and dimmed label
- Reduce action icon strokeWidth to 1.5 for refined industrial feel
- Add teal LED backlight bar on active nav item via blur pseudo-element

* fix(ui): parse usagePercent string to number for getValueColor

usagePercent is typed as string in SystemStats but getValueColor
expects number, causing TS2345 in CI builds.
2026-03-30 12:48:42 -04:00