From 2c16a7e9ba94cab407d7eccece365b4387b4d313 Mon Sep 17 00:00:00 2001 From: Fusion Agent Date: Wed, 19 Aug 2026 20:24:46 +0000 Subject: [PATCH] FN-058: add Medieval appearance theme Add a bundled pixel-font Medieval theme with parchment and wood visual styling across dashboard and desktop surfaces. - Add Medieval theme tokens, swatches, first-paint persistence, and selector integration.\n- Bundle Press Start 2P typography with responsive modal styling and documentation.\n- Add theme behavior tests, screenshots, and a published-package changeset.\n\nFiles changed:\n .changeset/fn-058-medieval-theme.md | 7 ++\n docs/dashboard-guide.md | 5 +-\n docs/settings-reference.md | 2 +-\n packages/core/src/types/ui/execution-and-ui.ts | 5 ++\n .../dashboard/app/__tests__/medieval-theme.test.ts | 71 +++++++++++++++++\n .../dashboard/app/components/ThemeSelector.css | 15 +++\n .../components/__tests__/ThemeDropdown.test.tsx | 2 +-\n .../components/__tests__/ThemeSelector.test.tsx | 14 ++--\n .../__tests__/CommandCenterControls.test.tsx | 10 ++-\n packages/dashboard/app/components/themeOptions.ts | 1 +\n .../dashboard/app/hooks/__tests__/useTheme.test.ts | 28 +++++++\n packages/dashboard/app/index.html | 4 +-\n packages/dashboard/app/main.tsx | 1 +\n packages/dashboard/app/public/theme-data.css | 86 +++++++++++++++++++++\n packages/dashboard/app/styles.css | 24 ++++++\n packages/dashboard/package.json | 1 +\n packages/desktop/src/renderer/index.html | 3 +-\n pnpm-lock.yaml | 8 ++\n screenshots/fn-058-medieval-theme-desktop.png | Bin 0 -> 19898 bytes\n screenshots/fn-058-medieval-theme-mobile.png | Bin 0 -> 14996 bytes\n 20 files changed, 269 insertions(+), 18 deletions(-) Fusion-Task-Id: FN-058 Fusion-Task-Lineage: b7ce1b83-59ba-4516-8073-c1e8c46bb181 Co-authored-by: Fusion --- .changeset/fn-058-medieval-theme.md | 7 ++ docs/dashboard-guide.md | 5 +- docs/settings-reference.md | 2 +- .../core/src/types/ui/execution-and-ui.ts | 5 + .../app/__tests__/medieval-theme.test.ts | 71 +++++++++++++++ .../app/components/ThemeSelector.css | 15 +++ .../__tests__/ThemeDropdown.test.tsx | 2 +- .../__tests__/ThemeSelector.test.tsx | 14 +-- .../__tests__/CommandCenterControls.test.tsx | 10 +- .../dashboard/app/components/themeOptions.ts | 1 + .../app/hooks/__tests__/useTheme.test.ts | 28 ++++++ packages/dashboard/app/index.html | 4 +- packages/dashboard/app/main.tsx | 1 + packages/dashboard/app/public/theme-data.css | 86 ++++++++++++++++++ packages/dashboard/app/styles.css | 24 +++++ packages/dashboard/package.json | 1 + packages/desktop/src/renderer/index.html | 3 +- pnpm-lock.yaml | 8 ++ screenshots/fn-058-medieval-theme-desktop.png | Bin 0 -> 19898 bytes screenshots/fn-058-medieval-theme-mobile.png | Bin 0 -> 14996 bytes 20 files changed, 269 insertions(+), 18 deletions(-) create mode 100644 .changeset/fn-058-medieval-theme.md create mode 100644 packages/dashboard/app/__tests__/medieval-theme.test.ts create mode 100644 screenshots/fn-058-medieval-theme-desktop.png create mode 100644 screenshots/fn-058-medieval-theme-mobile.png diff --git a/.changeset/fn-058-medieval-theme.md b/.changeset/fn-058-medieval-theme.md new file mode 100644 index 0000000000..895b01ecfb --- /dev/null +++ b/.changeset/fn-058-medieval-theme.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add a Medieval dashboard theme with parchment surfaces and wood-framed modals. +category: feature +dev: Bundles the local Press Start 2P pixel font without a CDN request. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index e35ee072e1..a3d93b7cdd 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -2248,11 +2248,12 @@ Tokens in the table are stable. Renaming or removing one requires a deprecation + -Dark/light modes via `data-theme`; fresh installs default to System mode so the resolved theme follows `prefers-color-scheme` until the user explicitly chooses Light, Dark, or System. 89 color themes via `data-color-theme` (lazy-loaded from `app/public/theme-data.css`), including Cobalt (saturated blue), Clay (warm terracotta), Moss (muted forest green), Aurora (navy/teal/violet dark surfaces with a misty blue-white light counterpart), Calm (low-stimulation slate/sage dark surfaces with a misty blue-white light counterpart), Dawn (indigo/plum dark surfaces with muted amber accents and a dawn-white light counterpart), Sage (soft green-gray surfaces with a muted olive accent and readable dark/light ramps), Midnight (deep-navy surfaces with restrained violet/indigo accents and a cool pale light counterpart), Velvet (deep plum/burgundy dark surfaces with rose-gold accents and a warm blush-white light counterpart), Factory Dark (graphite/steel dark surfaces, cool gunmetal borders, and restrained safety-orange accents with a concrete-gray light counterpart), and Factory Light (bright concrete/warm-white surfaces, steel-gray borders, a darkened safety-orange accent, and a warm-graphite dark counterpart), the Shadcn zinc-neutral theme with an orange default highlight/accent, Shadcn Custom (the same base with sanitized per-token color-picker overrides), and its color family: Shadcn Blue/Green/Red/Purple/Pink/Orange/Yellow, Shadcn Mono and Shadcn Mono Red/Blue/Green/Purple/Pink/Orange/Yellow (grayscale surfaces with color-specific accents; Shadcn Mono retains its selected id), Shadcn Black (pure black and white), Shadcn Gray (fully neutral zinc-gray accent), and Shadcn Gray Blue (blue-gray slate neutral surfaces with a muted slate-blue accent). Air is the minimal, borderless, paper-like preset with near-monochrome tokens and CSS-only chrome flattening. Glass Silver preserves the Glass theme's frosted translucent surfaces and transparent modal overlay behavior while using silver and graphite accents instead of purple/pink. +Dark/light modes via `data-theme`; fresh installs default to System mode so the resolved theme follows `prefers-color-scheme` until the user explicitly chooses Light, Dark, or System. 90 color themes via `data-color-theme` (lazy-loaded from `app/public/theme-data.css`), including Cobalt (saturated blue), Clay (warm terracotta), Moss (muted forest green), Aurora (navy/teal/violet dark surfaces with a misty blue-white light counterpart), Calm (low-stimulation slate/sage dark surfaces with a misty blue-white light counterpart), Dawn (indigo/plum dark surfaces with muted amber accents and a dawn-white light counterpart), Sage (soft green-gray surfaces with a muted olive accent and readable dark/light ramps), Midnight (deep-navy surfaces with restrained violet/indigo accents and a cool pale light counterpart), Velvet (deep plum/burgundy dark surfaces with rose-gold accents and a warm blush-white light counterpart), Medieval (restrained parchment surfaces, wood-framed generic modals, and local Press Start 2P pixel typography), Factory Dark (graphite/steel dark surfaces, cool gunmetal borders, and restrained safety-orange accents with a concrete-gray light counterpart), and Factory Light (bright concrete/warm-white surfaces, steel-gray borders, a darkened safety-orange accent, and a warm-graphite dark counterpart), the Shadcn zinc-neutral theme with an orange default highlight/accent, Shadcn Custom (the same base with sanitized per-token color-picker overrides), and its color family: Shadcn Blue/Green/Red/Purple/Pink/Orange/Yellow, Shadcn Mono and Shadcn Mono Red/Blue/Green/Purple/Pink/Orange/Yellow (grayscale surfaces with color-specific accents; Shadcn Mono retains its selected id), Shadcn Black (pure black and white), Shadcn Gray (fully neutral zinc-gray accent), and Shadcn Gray Blue (blue-gray slate neutral surfaces with a muted slate-blue accent). Air is the minimal, borderless, paper-like preset with near-monochrome tokens and CSS-only chrome flattening. Glass Silver preserves the Glass theme's frosted translucent surfaces and transparent modal overlay behavior while using silver and graphite accents instead of purple/pink. -Choose color themes from **Settings → Appearance** or from the Command Center **Overview** theme card. Settings merges its selector into the current-theme row, which opens the full color-theme list; Command Center keeps the compact `ThemeDropdown` trigger. Both use the same filterable list: type a visible theme name to narrow the displayed labels and color-chip swatches, then select an option to change the theme. Filtering alone never changes the chosen theme. The selected color theme, including Midnight and Velvet, persists in browser storage and is validated before React hydrates on both web and Electron startup. The left sidebar active-item highlight and resize accent use the active theme's `--accent`, so they follow the selected Shadcn accent instead of staying fixed blue. +Choose color themes from **Settings → Appearance** or from the Command Center **Overview** theme card. Settings merges its selector into the current-theme row, which opens the full color-theme list; Command Center keeps the compact `ThemeDropdown` trigger. Both use the same filterable list: type a visible theme name to narrow the displayed labels and color-chip swatches, then select an option to change the theme. Filtering alone never changes the chosen theme. The selected color theme, including Midnight, Velvet, and Medieval, persists in browser storage and is validated before React hydrates on both web and Electron startup. The left sidebar active-item highlight and resize accent use the active theme's `--accent`, so they follow the selected Shadcn accent instead of staying fixed blue. - **Base tokens** (`--bg`, `--surface`, etc.) — redefine in `:root`, `[data-theme="light"]`, and every theme block. - **Semantic tokens** (`--autopilot-pulse`, `--event-error-text`, `--badge-mission-*`, `--fab-*`) — `:root` + `[data-theme="light"]` only; no per-color-theme overrides. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index e40c57b06e..fa21375375 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -75,7 +75,7 @@ Defaults from `DEFAULT_GLOBAL_SETTINGS`; key scope from `GLOBAL_SETTINGS_KEYS`. |---|---|---:|---| | `anthropicAuthPreference` | `"api-key" \| "subscription"` | `"api-key"` | Which Anthropic credential wins when BOTH a raw API key and a Claude subscription OAuth login are configured. `"api-key"` keeps the historical precedence; `"subscription"` moves the raw key below the OAuth steps so a stale or revoked saved key cannot shadow a working subscription (the `401 invalid x-api-key` failure mode). Global, because credentials live in the global `~/.fusion/agent/auth.json`. Never removes a source — with one credential configured, resolution reaches it under either value. Surfaced in Settings → Authentication, where the two Anthropic cards show which credential is in use; the control appears only when both are connected. | | `themeMode` | `"dark" \| "light" \| "system"` | `"system"` | Dashboard theme mode. Fresh installs follow the operating system light/dark preference until the user chooses Light, Dark, or System. | -| `colorTheme` | `ColorTheme` | `"shadcn-ember"` | Dashboard color theme preset. Aurora is a built-in navy/teal/violet palette with a misty light counterpart; Calm is a low-stimulation slate/sage palette with a misty light counterpart; Dawn uses indigo/plum dark surfaces, amber accents, and dawn-white light surfaces; Midnight uses deep-navy surfaces with restrained violet/indigo accents and a cool pale light counterpart; Velvet uses deep plum/burgundy dark surfaces with rose-gold accents and a warm blush-white light counterpart; Factory Dark uses graphite/steel surfaces, cool gunmetal borders, and restrained safety-orange accents with a concrete-gray light counterpart; Factory Light uses bright concrete/warm-white surfaces, steel-gray borders, a darkened safety-orange accent, and a warm-graphite dark counterpart. Use `"shadcn-custom"` to show the separate custom shadcn color picker in Settings → Appearance and the Command Center theme card. | +| `colorTheme` | `ColorTheme` | `"shadcn-ember"` | Dashboard color theme preset. Medieval adds restrained aged-parchment surfaces, a wood-framed generic modal treatment, and the locally bundled Press Start 2P pixel font with no CDN/runtime font request. Aurora is a built-in navy/teal/violet palette with a misty light counterpart; Calm is a low-stimulation slate/sage palette with a misty light counterpart; Dawn uses indigo/plum dark surfaces, amber accents, and dawn-white light surfaces; Midnight uses deep-navy surfaces with restrained violet/indigo accents and a cool pale light counterpart; Velvet uses deep plum/burgundy dark surfaces with rose-gold accents and a warm blush-white light counterpart; Factory Dark uses graphite/steel surfaces, cool gunmetal borders, and restrained safety-orange accents with a concrete-gray light counterpart; Factory Light uses bright concrete/warm-white surfaces, steel-gray borders, a darkened safety-orange accent, and a warm-graphite dark counterpart. Use `"shadcn-custom"` to show the separate custom shadcn color picker in Settings → Appearance and the Command Center theme card. | | `shadcnCustomColors` | `Record` | `undefined` | Optional shadcn design-token override map for `"shadcn-custom"` only. Keys are CSS token names such as `--accent`, `--bg`, `--surface`, `--card`, `--border`, `--text`, `--text-muted`, workflow status tokens, and `--color-success`/`--color-warning`/`--color-error`; values must be sanitized `#RGB` or `#RRGGBB` hex colors. Missing or invalid entries fall back to the `shadcn-custom` base defaults and are not applied to other themes. | | `language` | `"en" \| "zh-CN" \| "zh-TW" \| "fr" \| "es" \| "ko" \| "pt-BR"` | `undefined` | UI language for the dashboard and TUI. When unset, the dashboard detects from localStorage → browser language and the CLI from `--lang` flag → environment locale, falling back to `en`. Validated at the store write boundary (`validateLocale`); invalid values are dropped. Reset to auto-detect via the dashboard's "Auto" language option or `fn settings set language auto` (clears the persisted key). | | `dashboardFontScalePct` | `number` | `100` | Dashboard font scale percentage used by Appearance settings. Valid range: `85` to `125`; applied pre-hydration via document root font-size so board typography (column headers/counts, task cards, and quick-entry text) scales with the setting from first paint. | diff --git a/packages/core/src/types/ui/execution-and-ui.ts b/packages/core/src/types/ui/execution-and-ui.ts index 212e8be950..64c2ec4967 100644 --- a/packages/core/src/types/ui/execution-and-ui.ts +++ b/packages/core/src/types/ui/execution-and-ui.ts @@ -109,6 +109,11 @@ export const COLOR_THEMES = [ "brutalist", "neon-city", "parchment", + /* + FNXC:DashboardTheming 2026-08-19-19:23: + Medieval is a persisted selectable theme. Keep this ID and order synchronized with selector metadata and both web/Electron first-paint validators so a saved medieval preference survives startup before React hydration. + */ + "medieval", "terminal", "glass", // FNXC:DashboardTheming 2026-07-01-00:00: Glass Silver is the silver/gray frosted sibling of Glass; keep this id in lockstep with dashboard/desktop validators and selector metadata so persisted explicit choices survive startup. diff --git a/packages/dashboard/app/__tests__/medieval-theme.test.ts b/packages/dashboard/app/__tests__/medieval-theme.test.ts new file mode 100644 index 0000000000..72ebf6107f --- /dev/null +++ b/packages/dashboard/app/__tests__/medieval-theme.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from "vitest"; +import { COLOR_THEMES as CORE_COLOR_THEMES } from "@fusion/core"; +import { COLOR_THEMES as DASHBOARD_COLOR_THEMES } from "../components/themeOptions"; +import { readAppFile } from "../test/cssFixture"; + +/* +FNXC:DashboardTheming 2026-08-19-19:23: +Medieval must remain one persisted theme across core, selectors, and web/Electron first paint. Its visual contract is intentionally limited to parchment tokens and generic modal paint so the existing modal interaction and mobile layout remain unchanged. +*/ +describe("Medieval color theme", () => { + const themeData = readAppFile("public/theme-data.css"); + const themeSelector = readAppFile("components/ThemeSelector.css"); + const styles = readAppFile("styles.css"); + const dashboardIndexHtml = readAppFile("index.html"); + const desktopIndexHtml = readAppFile("../../desktop/src/renderer/index.html"); + + it("keeps persisted, selector, and first-paint registries in exact order", () => { + const coreIds = [...CORE_COLOR_THEMES]; + const dashboardIds = DASHBOARD_COLOR_THEMES.map((theme) => theme.value); + const dashboardValidThemes = extractValidThemes(dashboardIndexHtml); + const desktopValidThemes = extractValidThemes(desktopIndexHtml); + + expect(CORE_COLOR_THEMES.filter((theme) => theme === "medieval")).toHaveLength(1); + expect(DASHBOARD_COLOR_THEMES).toContainEqual({ value: "medieval", label: "Medieval", className: "theme-swatch-medieval" }); + expect(dashboardIds).toEqual(coreIds); + expect(dashboardValidThemes).toEqual(coreIds); + expect(desktopValidThemes).toEqual(coreIds); + }); + + it("defines readable parchment tokens and a bundled pixel display face in both modes", () => { + for (const selector of ['[data-color-theme="medieval"]', '[data-color-theme="medieval"][data-theme="light"]']) { + const block = extractSelectorBlock(themeData, selector); + for (const token of ["--bg:", "--surface:", "--card:", "--surface-hover:", "--border:", "--text:", "--color-success:", "--color-warning:", "--color-error:", "--color-info:", "--cta-bg:", "--cta-text:", "--accent:", "--focus-ring:", "--shadow-glow:", "--font-primary:"]) expect(block).toContain(token); + expect(block).toContain('"Press Start 2P", ui-monospace, monospace'); + } + expect(themeData).toContain('[data-color-theme="medieval"] body'); + expect(readAppFile("main.tsx")).toContain('@fontsource/press-start-2p/400.css'); + }); + + it("uses mode-specific swatches and generic modal paint without changing modal behavior", () => { + for (const block of [extractSelectorBlock(themeData, ":root"), extractSelectorBlock(themeData, '[data-theme="light"]'), extractSelectorBlock(themeSelector, ".theme-swatch-medieval"), extractSelectorBlock(themeSelector, '[data-theme="light"] .theme-swatch-medieval')]) { + for (const sample of [1, 2, 3, 4]) expect(block).toContain(`medieval-swatch-sample-${sample}`); + } + const medievalModal = extractSelectorBlock(styles, '[data-color-theme="medieval"] .modal'); + expect(medievalModal).toContain("border-color:"); + expect(medievalModal).toContain("box-shadow:"); + expect(medievalModal).toContain("background-image:"); + expect(medievalModal).not.toMatch(/(?:position|width|max-height|z-index|resize|touch-action)\s*:/); + expect(styles).toContain('[data-color-theme="medieval"] .modal:not(.confirm-dialog)'); + expect(styles).toContain('Mobile\'s generic modal reset removes all borders'); + }); +}); + +function extractValidThemes(html: string): string[] { + const match = html.match(/var validThemes = \[([\s\S]*?)\];/); + if (!match) throw new Error("Could not find pre-hydration validThemes array"); + return [...match[1].matchAll(/["']([^"']+)["']/g)].map((themeMatch) => themeMatch[1]); +} + +function extractSelectorBlock(css: string, selector: string): string { + const startIdx = css.indexOf(`${selector} {`); + if (startIdx === -1) throw new Error(`Could not find selector block: ${selector}`); + const openBraceIdx = css.indexOf("{", startIdx); + let depth = 1; + for (let index = openBraceIdx + 1; index < css.length; index++) { + if (css[index] === "{") depth++; + if (css[index] === "}") depth--; + if (depth === 0) return css.slice(startIdx, index + 1); + } + throw new Error(`Could not find closing brace for selector block: ${selector}`); +} diff --git a/packages/dashboard/app/components/ThemeSelector.css b/packages/dashboard/app/components/ThemeSelector.css index fe08af185f..ab3d2d97ca 100644 --- a/packages/dashboard/app/components/ThemeSelector.css +++ b/packages/dashboard/app/components/ThemeSelector.css @@ -681,6 +681,13 @@ --swatch-sample-4: #be6e3c; } +.theme-swatch-medieval { + --swatch-sample-1: var(--medieval-swatch-sample-1); + --swatch-sample-2: var(--medieval-swatch-sample-2); + --swatch-sample-3: var(--medieval-swatch-sample-3); + --swatch-sample-4: var(--medieval-swatch-sample-4); +} + .theme-swatch-terminal { --swatch-sample-1: #0a0a0a; --swatch-sample-2: #0f120d; @@ -1368,3 +1375,11 @@ grid-template-columns: 1fr; } } + + +[data-theme="light"] .theme-swatch-medieval { + --swatch-sample-1: var(--medieval-swatch-sample-1); + --swatch-sample-2: var(--medieval-swatch-sample-2); + --swatch-sample-3: var(--medieval-swatch-sample-3); + --swatch-sample-4: var(--medieval-swatch-sample-4); +} diff --git a/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx b/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx index 665c961cf8..033c46fd04 100644 --- a/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx +++ b/packages/dashboard/app/components/__tests__/ThemeDropdown.test.tsx @@ -6,7 +6,7 @@ import { ThemeDropdown } from "../ThemeDropdown"; import { readAppFile } from "../../test/cssFixture"; // FNXC:Theme 2026-07-16-14:30: FN-8146 pins the historical Settings-grid set, including restored shadcn-mono, so a removal from COLOR_THEMES cannot make the all-themes checks pass circularly. -const EXPECTED_THEME_IDS = ['default', 'ocean', 'forest', 'sunset', 'zen', 'berry', 'high-contrast', 'industrial', 'monochrome', 'slate', 'ash', 'air', 'graphite', 'silver', 'solarized', 'factory', 'factory-mono', 'ayu', 'one-dark', 'nord', 'dracula', 'gruvbox', 'tokyo-night', 'catppuccin-mocha', 'github-dark', 'everforest', 'rose-pine', 'kanagawa', 'night-owl', 'palenight', 'monokai-pro', 'slime', 'brutalist', 'neon-city', 'parchment', 'terminal', 'glass', 'glass-silver', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia', 'cobalt', 'clay', 'moss', 'aurora', 'calm', 'dawn', 'sage', 'midnight', 'factory-dark', 'factory-light', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', 'shadcn-mono-red', 'shadcn-mono-blue', 'shadcn-mono-green', 'shadcn-mono-purple', 'shadcn-mono-pink', 'shadcn-mono-orange', 'shadcn-mono-yellow', 'shadcn-black', 'shadcn-gray', 'shadcn-gray-blue'] as const; +const EXPECTED_THEME_IDS = ['default', 'ocean', 'forest', 'sunset', 'zen', 'berry', 'high-contrast', 'industrial', 'monochrome', 'slate', 'ash', 'air', 'graphite', 'silver', 'solarized', 'factory', 'factory-mono', 'ayu', 'one-dark', 'nord', 'dracula', 'gruvbox', 'tokyo-night', 'catppuccin-mocha', 'github-dark', 'everforest', 'rose-pine', 'kanagawa', 'night-owl', 'palenight', 'monokai-pro', 'slime', 'brutalist', 'neon-city', 'parchment', 'medieval', 'terminal', 'glass', 'glass-silver', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia', 'cobalt', 'clay', 'moss', 'aurora', 'calm', 'dawn', 'sage', 'midnight', 'velvet', 'factory-dark', 'factory-light', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', 'shadcn-mono-red', 'shadcn-mono-blue', 'shadcn-mono-green', 'shadcn-mono-purple', 'shadcn-mono-pink', 'shadcn-mono-orange', 'shadcn-mono-yellow', 'shadcn-black', 'shadcn-gray', 'shadcn-gray-blue'] as const; function renderedThemeIds(listbox: HTMLElement) { return within(listbox).getAllByRole("option").map((option) => { diff --git a/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx b/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx index 9b52a64f40..c770fb63ef 100644 --- a/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx +++ b/packages/dashboard/app/components/__tests__/ThemeSelector.test.tsx @@ -5,7 +5,7 @@ import { ThemeSelector } from "../ThemeSelector"; // FNXC:Theme 2026-07-16-14:30: FN-8146 pins the historical Settings-grid set, including restored shadcn-mono, so a removal from COLOR_THEMES cannot make the all-themes checks pass circularly. // FNXC:Theme 2026-08-16-05:07: FN-8730 (5931e10345) added the Midnight preset; the census gains "midnight" between sage and factory-dark to match COLOR_THEMES order. -const EXPECTED_THEME_IDS = ['default', 'ocean', 'forest', 'sunset', 'zen', 'berry', 'high-contrast', 'industrial', 'monochrome', 'slate', 'ash', 'air', 'graphite', 'silver', 'solarized', 'factory', 'factory-mono', 'ayu', 'one-dark', 'nord', 'dracula', 'gruvbox', 'tokyo-night', 'catppuccin-mocha', 'github-dark', 'everforest', 'rose-pine', 'kanagawa', 'night-owl', 'palenight', 'monokai-pro', 'slime', 'brutalist', 'neon-city', 'parchment', 'terminal', 'glass', 'glass-silver', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia', 'cobalt', 'clay', 'moss', 'aurora', 'calm', 'dawn', 'sage', 'midnight', 'factory-dark', 'factory-light', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', 'shadcn-mono-red', 'shadcn-mono-blue', 'shadcn-mono-green', 'shadcn-mono-purple', 'shadcn-mono-pink', 'shadcn-mono-orange', 'shadcn-mono-yellow', 'shadcn-black', 'shadcn-gray', 'shadcn-gray-blue'] as const; +const EXPECTED_THEME_IDS = ['default', 'ocean', 'forest', 'sunset', 'zen', 'berry', 'high-contrast', 'industrial', 'monochrome', 'slate', 'ash', 'air', 'graphite', 'silver', 'solarized', 'factory', 'factory-mono', 'ayu', 'one-dark', 'nord', 'dracula', 'gruvbox', 'tokyo-night', 'catppuccin-mocha', 'github-dark', 'everforest', 'rose-pine', 'kanagawa', 'night-owl', 'palenight', 'monokai-pro', 'slime', 'brutalist', 'neon-city', 'parchment', 'medieval', 'terminal', 'glass', 'glass-silver', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia', 'cobalt', 'clay', 'moss', 'aurora', 'calm', 'dawn', 'sage', 'midnight', 'velvet', 'factory-dark', 'factory-light', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', 'shadcn-mono-red', 'shadcn-mono-blue', 'shadcn-mono-green', 'shadcn-mono-purple', 'shadcn-mono-pink', 'shadcn-mono-orange', 'shadcn-mono-yellow', 'shadcn-black', 'shadcn-gray', 'shadcn-gray-blue'] as const; function renderedThemeIds(listbox: HTMLElement) { return within(listbox).getAllByRole("option").map((option) => { @@ -59,7 +59,7 @@ describe("ThemeSelector", () => { expect(onShadcnCustomColorsChange).toHaveBeenCalledWith({}); }); - it("opens shared swatched options and selects a color theme", () => { + it("opens shared swatched options and selects Medieval from the Settings current-row host", () => { const { onColorThemeChange } = renderSelector("forest"); const trigger = screen.getByRole("button", { name: /current theme dark \/ forest/i }); @@ -67,12 +67,12 @@ describe("ThemeSelector", () => { const listbox = screen.getByRole("listbox", { name: "Color theme" }); expect(renderedThemeIds(listbox)).toEqual(EXPECTED_THEME_IDS); - fireEvent.change(screen.getByRole("searchbox", { name: /filter color themes/i }), { target: { value: "ocean" } }); - const oceanOption = within(listbox).getByRole("option", { name: "Ocean" }); - expect(oceanOption.querySelector(".theme-swatch-ocean")).toBeTruthy(); - fireEvent.click(oceanOption); + fireEvent.change(screen.getByRole("searchbox", { name: /filter color themes/i }), { target: { value: "medieval" } }); + const medievalOption = within(listbox).getByRole("option", { name: "Medieval" }); + expect(medievalOption.querySelector(".theme-swatch-medieval")).toBeTruthy(); + fireEvent.keyDown(medievalOption, { key: "Enter" }); - expect(onColorThemeChange).toHaveBeenCalledWith("ocean"); + expect(onColorThemeChange).toHaveBeenCalledWith("medieval"); expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); }); diff --git a/packages/dashboard/app/components/command-center/__tests__/CommandCenterControls.test.tsx b/packages/dashboard/app/components/command-center/__tests__/CommandCenterControls.test.tsx index 05717bd333..09ffa5165d 100644 --- a/packages/dashboard/app/components/command-center/__tests__/CommandCenterControls.test.tsx +++ b/packages/dashboard/app/components/command-center/__tests__/CommandCenterControls.test.tsx @@ -77,7 +77,7 @@ function cssRule(css: string, selector: string) { // FNXC:Theme 2026-07-16-14:30: FN-8146 pins the historical Settings-grid set, including restored shadcn-mono, so a removal from COLOR_THEMES cannot make the all-themes checks pass circularly. // FNXC:Theme 2026-08-16-05:07: FN-8730 (5931e10345) added the Midnight preset; the census gains "midnight" between sage and factory-dark to match COLOR_THEMES order. -const EXPECTED_THEME_IDS = ['default', 'ocean', 'forest', 'sunset', 'zen', 'berry', 'high-contrast', 'industrial', 'monochrome', 'slate', 'ash', 'air', 'graphite', 'silver', 'solarized', 'factory', 'factory-mono', 'ayu', 'one-dark', 'nord', 'dracula', 'gruvbox', 'tokyo-night', 'catppuccin-mocha', 'github-dark', 'everforest', 'rose-pine', 'kanagawa', 'night-owl', 'palenight', 'monokai-pro', 'slime', 'brutalist', 'neon-city', 'parchment', 'terminal', 'glass', 'glass-silver', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia', 'cobalt', 'clay', 'moss', 'aurora', 'calm', 'dawn', 'sage', 'midnight', 'factory-dark', 'factory-light', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', 'shadcn-mono-red', 'shadcn-mono-blue', 'shadcn-mono-green', 'shadcn-mono-purple', 'shadcn-mono-pink', 'shadcn-mono-orange', 'shadcn-mono-yellow', 'shadcn-black', 'shadcn-gray', 'shadcn-gray-blue'] as const; +const EXPECTED_THEME_IDS = ['default', 'ocean', 'forest', 'sunset', 'zen', 'berry', 'high-contrast', 'industrial', 'monochrome', 'slate', 'ash', 'air', 'graphite', 'silver', 'solarized', 'factory', 'factory-mono', 'ayu', 'one-dark', 'nord', 'dracula', 'gruvbox', 'tokyo-night', 'catppuccin-mocha', 'github-dark', 'everforest', 'rose-pine', 'kanagawa', 'night-owl', 'palenight', 'monokai-pro', 'slime', 'brutalist', 'neon-city', 'parchment', 'medieval', 'terminal', 'glass', 'glass-silver', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia', 'cobalt', 'clay', 'moss', 'aurora', 'calm', 'dawn', 'sage', 'midnight', 'velvet', 'factory-dark', 'factory-light', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', 'shadcn-mono-red', 'shadcn-mono-blue', 'shadcn-mono-green', 'shadcn-mono-purple', 'shadcn-mono-pink', 'shadcn-mono-orange', 'shadcn-mono-yellow', 'shadcn-black', 'shadcn-gray', 'shadcn-gray-blue'] as const; function renderedThemeIds(listbox: HTMLElement) { return within(listbox).getAllByRole("option").map((option) => { @@ -116,10 +116,12 @@ describe("CommandCenterControls concurrency markers", () => { fireEvent.click(trigger); const listbox = screen.getByRole("listbox", { name: "Color theme" }); expect(renderedThemeIds(listbox)).toEqual(EXPECTED_THEME_IDS); - fireEvent.change(screen.getByRole("searchbox", { name: /filter color themes/i }), { target: { value: "dawn" } }); - fireEvent.click(screen.getByRole("option", { name: "Dawn" })); + fireEvent.change(screen.getByRole("searchbox", { name: /filter color themes/i }), { target: { value: "medieval" } }); + const medievalOption = screen.getByRole("option", { name: "Medieval" }); + expect(medievalOption.querySelector(".theme-swatch-medieval")).toBeTruthy(); + fireEvent.click(medievalOption); - expect(onColorThemeChange).toHaveBeenCalledWith("dawn"); + expect(onColorThemeChange).toHaveBeenCalledWith("medieval"); expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); }); diff --git a/packages/dashboard/app/components/themeOptions.ts b/packages/dashboard/app/components/themeOptions.ts index 04007f9e74..ee3888c9b6 100644 --- a/packages/dashboard/app/components/themeOptions.ts +++ b/packages/dashboard/app/components/themeOptions.ts @@ -50,6 +50,7 @@ export const COLOR_THEMES: { value: ColorTheme; label: string; className: string { value: "brutalist", label: "Brutalist", className: "theme-swatch-brutalist" }, { value: "neon-city", label: "Neon City", className: "theme-swatch-neon-city" }, { value: "parchment", label: "Parchment", className: "theme-swatch-parchment" }, + { value: "medieval", label: "Medieval", className: "theme-swatch-medieval" }, { value: "terminal", label: "Terminal", className: "theme-swatch-terminal" }, { value: "glass", label: "Glass", className: "theme-swatch-glass" }, { value: "glass-silver", label: "Glass Silver", className: "theme-swatch-glass-silver" }, diff --git a/packages/dashboard/app/hooks/__tests__/useTheme.test.ts b/packages/dashboard/app/hooks/__tests__/useTheme.test.ts index 70c8377fc8..ab2ad974c0 100644 --- a/packages/dashboard/app/hooks/__tests__/useTheme.test.ts +++ b/packages/dashboard/app/hooks/__tests__/useTheme.test.ts @@ -210,6 +210,18 @@ describe("useTheme", () => { expect(document.documentElement.getAttribute("data-color-theme")).toBe("dawn"); }); + it("hydrates, caches, and applies Medieval from backend settings", async () => { + mockFetchGlobalSettings.mockResolvedValue({ colorTheme: "medieval" }); + + const { result } = renderHook(() => useTheme()); + + await waitFor(() => { + expect(result.current.colorTheme).toBe("medieval"); + }); + expect(localStorageMock[COLOR_THEME_STORAGE_KEY]).toBe("medieval"); + expect(document.documentElement.getAttribute("data-color-theme")).toBe("medieval"); + }); + it("hydrates dashboard font scale from backend on mount", async () => { mockFetchGlobalSettings.mockResolvedValue({ dashboardFontScalePct: 110 }); @@ -826,6 +838,22 @@ describe("useTheme", () => { expect(document.documentElement.getAttribute("data-color-theme")).toBe("shadcn-mono"); }); + it("preserves explicit Medieval color theme from localStorage and writes it through", () => { + localStorageMock[COLOR_THEME_STORAGE_KEY] = "medieval"; + + const { result } = renderHook(() => useTheme()); + + expect(result.current.colorTheme).toBe("medieval"); + expect(document.documentElement.getAttribute("data-color-theme")).toBe("medieval"); + + act(() => { + result.current.setColorTheme("medieval"); + }); + + expect(localStorageMock[COLOR_THEME_STORAGE_KEY]).toBe("medieval"); + expect(mockUpdateGlobalSettings).toHaveBeenCalledWith({ colorTheme: "medieval" }); + }); + it("preserves explicit Glass and Glass Silver color themes from localStorage", () => { localStorageMock[COLOR_THEME_STORAGE_KEY] = "glass-silver"; diff --git a/packages/dashboard/app/index.html b/packages/dashboard/app/index.html index ba9111711e..9eb9545d8a 100644 --- a/packages/dashboard/app/index.html +++ b/packages/dashboard/app/index.html @@ -164,10 +164,10 @@ mode = 'system'; } var colorTheme = localStorage.getItem('kb-dashboard-color-theme') || 'shadcn-ember'; - var validThemes = ['default', 'ocean', 'forest', 'sunset', 'zen', 'berry', 'high-contrast', 'industrial', 'monochrome', 'slate', 'ash', 'air', 'graphite', 'silver', 'solarized', 'factory', 'factory-mono', 'ayu', 'one-dark', 'nord', 'dracula', 'gruvbox', 'tokyo-night', 'catppuccin-mocha', 'github-dark', 'everforest', 'rose-pine', 'kanagawa', 'night-owl', 'palenight', 'monokai-pro', 'slime', 'brutalist', 'neon-city', 'parchment', 'terminal', 'glass', 'glass-silver', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia', 'cobalt', 'clay', 'moss', 'aurora', 'calm', 'dawn', 'sage', 'midnight', 'velvet', 'factory-dark', 'factory-light', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', 'shadcn-mono-red', 'shadcn-mono-blue', 'shadcn-mono-green', 'shadcn-mono-purple', 'shadcn-mono-pink', 'shadcn-mono-orange', 'shadcn-mono-yellow', 'shadcn-black', 'shadcn-gray', 'shadcn-gray-blue']; + var validThemes = ['default', 'ocean', 'forest', 'sunset', 'zen', 'berry', 'high-contrast', 'industrial', 'monochrome', 'slate', 'ash', 'air', 'graphite', 'silver', 'solarized', 'factory', 'factory-mono', 'ayu', 'one-dark', 'nord', 'dracula', 'gruvbox', 'tokyo-night', 'catppuccin-mocha', 'github-dark', 'everforest', 'rose-pine', 'kanagawa', 'night-owl', 'palenight', 'monokai-pro', 'slime', 'brutalist', 'neon-city', 'parchment', 'medieval', 'terminal', 'glass', 'glass-silver', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia', 'cobalt', 'clay', 'moss', 'aurora', 'calm', 'dawn', 'sage', 'midnight', 'velvet', 'factory-dark', 'factory-light', 'shadcn', 'shadcn-ember', 'shadcn-custom', 'shadcn-blue', 'shadcn-green', 'shadcn-red', 'shadcn-purple', 'shadcn-pink', 'shadcn-orange', 'shadcn-yellow', 'shadcn-mono', 'shadcn-mono-red', 'shadcn-mono-blue', 'shadcn-mono-green', 'shadcn-mono-purple', 'shadcn-mono-pink', 'shadcn-mono-orange', 'shadcn-mono-yellow', 'shadcn-black', 'shadcn-gray', 'shadcn-gray-blue']; // FNXC:DashboardTheming 2026-06-30-00:00: Unset startup theme is Shadcn Ember; explicit stored legacy ids such as "default" and "ocean" remain valid and must not be migrated. // FNXC:DashboardTheming 2026-07-16-14:30: FN-8146 restores Shadcn Mono as a distinct persisted choice, so bootstrap must retain its stored id instead of remapping it to Mono Red. - // FNXC:MidnightTheme 2026-08-03-02:05: Web first paint must accept persisted Midnight before React hydrates; invalid or unset values still fall back to Shadcn Ember. + // FNXC:DashboardTheming 2026-08-19-19:23: Web first paint must accept persisted Medieval before React hydrates; invalid or unset values still fall back to Shadcn Ember. if (!validThemes.includes(colorTheme)) { colorTheme = 'shadcn-ember'; } diff --git a/packages/dashboard/app/main.tsx b/packages/dashboard/app/main.tsx index c40723db0a..7a87d021c0 100644 --- a/packages/dashboard/app/main.tsx +++ b/packages/dashboard/app/main.tsx @@ -9,6 +9,7 @@ import { installSwUpdate } from "./swUpdate"; import { bootstrapShellHostContext } from "./shell-host"; import { registerBundledPluginViews } from "./plugins/registerBundledPluginViews"; import { i18nReady } from "./i18n"; +import "@fontsource/press-start-2p/400.css"; import "./styles.css"; // Install the bearer-token fetch wrapper before React mounts so every API diff --git a/packages/dashboard/app/public/theme-data.css b/packages/dashboard/app/public/theme-data.css index 0825ffd05f..6137f45652 100644 --- a/packages/dashboard/app/public/theme-data.css +++ b/packages/dashboard/app/public/theme-data.css @@ -34,6 +34,10 @@ --velvet-swatch-sample-2: #2b1d24; --velvet-swatch-sample-3: #c99a89; --velvet-swatch-sample-4: #b66b7d; + --medieval-swatch-sample-1: #21170f; + --medieval-swatch-sample-2: #392417; + --medieval-swatch-sample-3: #d5ad67; + --medieval-swatch-sample-4: #8db08b; --factory-dark-swatch-sample-1: #0d0f10; --factory-dark-swatch-sample-2: #1a1d1f; --factory-dark-swatch-sample-3: #e89532; @@ -69,6 +73,10 @@ --velvet-swatch-sample-2: #f0e2e0; --velvet-swatch-sample-3: #8a4a55; --velvet-swatch-sample-4: #a9636f; + --medieval-swatch-sample-1: #f4ead2; + --medieval-swatch-sample-2: #e1ceb0; + --medieval-swatch-sample-3: #765125; + --medieval-swatch-sample-4: #446b48; --factory-dark-swatch-sample-1: #eceef0; --factory-dark-swatch-sample-2: #d9dde0; --factory-dark-swatch-sample-3: #a95612; @@ -7445,6 +7453,84 @@ FNXC:DashboardTheming 2026-07-20-00:00: Aurora gives operators navy/teal/violet dark surfaces and misty light surfaces. Literal palette values stay in these canonical tokens, while globally defined swatch variables keep the unselected option accurate in either mode. */ /* +FNXC:DashboardTheming 2026-08-19-19:42: +Medieval keeps the dashboard clean and legible while applying a restrained aged-parchment canvas, wood-framed generic modals, and the bundled pixel display face. Press Start 2P remains primary, while explicit system fallbacks render any unsupported localized glyph rather than showing tofu; terminal typography stays on --font-mono and no runtime font request is made. +*/ +[data-color-theme="medieval"] { + --bg: #21170f; + --surface: #302116; + --card: #3b2a1c; + --card-hover: #4a3523; + --surface-hover: color-mix(in srgb, var(--surface) 90%, var(--text) 10%); + --border: #795735; + --text: #fff1ce; + --text-muted: #ddc79e; + --text-dim: #aa926c; + --todo: #d5ad67; + --in-progress: #79afaa; + --in-progress-rgb: 121, 175, 170; + --in-review: #c79ad4; + --triage: #e5ba6a; + --done: #8db08b; + --color-success: #9dca8c; + --color-warning: #e5ba6a; + --color-error: #ef8e75; + --color-info: #82b9c4; + --cta-bg: #80512c; + --cta-border: #d5ad67; + --cta-text: #fff8e5; + --cta-bg-hover: #9a6637; + --cta-border-hover: #ecd093; + --cta-glow: 0 0 var(--space-md) color-mix(in srgb, var(--cta-border) 30%, transparent); + --accent: #d5ad67; + --accent-text: #21170f; + --logo-accent: var(--accent); + --shadow-glow: 0 0 var(--space-lg) color-mix(in srgb, var(--accent) 28%, transparent); + --focus-ring: 0 0 0 var(--btn-border-width) color-mix(in srgb, var(--accent) 24%, transparent); + --focus-ring-strong: 0 0 0 var(--btn-border-width) color-mix(in srgb, var(--accent) 36%, transparent); + --font-primary: "Press Start 2P", ui-monospace, monospace; +} + +[data-color-theme="medieval"][data-theme="light"] { + --bg: #f4ead2; + --surface: #ead9b8; + --card: #fff8e5; + --card-hover: #f0dfc0; + --surface-hover: color-mix(in srgb, var(--surface) 92%, var(--text) 8%); + --border: #a77b4a; + --text: #302116; + --text-muted: #674e31; + --text-dim: #896f4d; + --todo: #8a612b; + --in-progress: #2e7473; + --in-progress-rgb: 46, 116, 115; + --in-review: #704c87; + --triage: #8a612b; + --done: #446b48; + --color-success: #3d7544; + --color-warning: #8a612b; + --color-error: #a94335; + --color-info: #2f6f89; + --cta-bg: #765125; + --cta-border: #61401e; + --cta-text: #fff8e5; + --cta-bg-hover: #5f3e1c; + --cta-border-hover: #4b3015; + --cta-glow: 0 0 var(--space-md) color-mix(in srgb, var(--cta-border) 24%, transparent); + --accent: #765125; + --accent-text: #fff8e5; + --logo-accent: var(--accent); + --shadow-glow: 0 0 var(--space-lg) color-mix(in srgb, var(--accent) 20%, transparent); + --focus-ring: 0 0 0 var(--btn-border-width) color-mix(in srgb, var(--accent) 18%, transparent); + --focus-ring-strong: 0 0 0 var(--btn-border-width) color-mix(in srgb, var(--accent) 28%, transparent); + --font-primary: "Press Start 2P", ui-monospace, monospace; +} + +[data-color-theme="medieval"] body { + background-color: var(--bg); + background-image: linear-gradient(color-mix(in srgb, var(--text) 3%, transparent) 0 0), repeating-linear-gradient(0deg, transparent 0, transparent var(--space-2xl), color-mix(in srgb, var(--border) 12%, transparent) calc(var(--space-2xl) + var(--btn-border-width))); +} +/* FNXC:MidnightTheme 2026-08-03-02:05: Midnight gives operators deep-navy surfaces with restrained violet/indigo accents in dark mode and a cool pale counterpart in light mode. These complete token blocks preserve distinct semantic statuses, AA-readable text and CTA pairs, and globally resolvable swatch previews for unselected selector options. */ diff --git a/packages/dashboard/app/styles.css b/packages/dashboard/app/styles.css index ba6d904099..e90ea4da94 100644 --- a/packages/dashboard/app/styles.css +++ b/packages/dashboard/app/styles.css @@ -1313,6 +1313,16 @@ Modal and dock pop-out surfaces should not darken or blur the app behind them. K display: flex; } +/* +FNXC:DashboardTheming 2026-08-19-19:27: +Medieval modal chrome uses only paint properties on the shared generic surface, preserving modal sizing, dismissal, focus, touch, and mobile behavior while providing the requested restrained wood frame. +*/ +[data-color-theme="medieval"] .modal { + border-color: color-mix(in srgb, var(--border) 82%, var(--accent) 18%); + box-shadow: inset 0 0 0 var(--btn-border-width) color-mix(in srgb, var(--card) 70%, var(--border) 30%), var(--shadow-lg); + background-image: linear-gradient(90deg, color-mix(in srgb, var(--border) 16%, transparent), transparent var(--space-2xl), color-mix(in srgb, var(--border) 12%, transparent)); +} + .modal { position: relative; background: var(--surface); @@ -3791,6 +3801,20 @@ Toast text must contrast its status background across every dashboard theme and display: none; } + /* + FNXC:DashboardTheming 2026-08-19-19:52: + Mobile's generic modal reset removes all borders, which would erase Medieval's shared wood frame at the + actual narrow viewport. Restore only tokenized border paint here; sizing, touch, dismissal, and the + fullscreen mobile modal contract remain owned by the generic rule above. + */ + [data-color-theme="medieval"] .modal:not(.confirm-dialog), + [data-color-theme="medieval"] .modal-lg, + [data-color-theme="medieval"] .modal-md, + [data-color-theme="medieval"] .gm-modal { + border: var(--btn-border-width) solid color-mix(in srgb, var(--border) 82%, var(--accent) 18%); + box-shadow: inset 0 0 0 var(--btn-border-width) color-mix(in srgb, var(--card) 70%, var(--border) 30%), var(--shadow-lg); + } + /* Settings modal: use the section picker as the only mobile navigation */ .settings-layout { flex-direction: column; diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index c7fab7fd0a..34d4e452ad 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -106,6 +106,7 @@ "@codemirror/view": "^6.36.4", "@earendil-works/pi-ai": "0.84.1", "@earendil-works/pi-coding-agent": "0.84.1", + "@fontsource/press-start-2p": "^5.3.0", "@fusion-plugin-examples/claude-runtime": "workspace:*", "@fusion-plugin-examples/cli-printing-press": "workspace:*", "@fusion-plugin-examples/compound-engineering": "workspace:*", diff --git a/packages/desktop/src/renderer/index.html b/packages/desktop/src/renderer/index.html index 8b8cd1a3d8..f1e40d485d 100644 --- a/packages/desktop/src/renderer/index.html +++ b/packages/desktop/src/renderer/index.html @@ -50,6 +50,7 @@ "brutalist", "neon-city", "parchment", + "medieval", "terminal", "glass", "glass-silver", @@ -107,7 +108,7 @@ ]; // FNXC:DashboardTheming 2026-06-30-00:00: Desktop pre-hydration mirrors the full core theme union before falling back to Shadcn Ember, so every explicit legacy id stays valid on native startup. - // FNXC:MidnightTheme 2026-08-03-02:05: Electron first paint must accept persisted Midnight before React hydrates; invalid or unset values still fall back to Shadcn Ember. + // FNXC:DashboardTheming 2026-08-19-19:23: Electron first paint must accept persisted Medieval before React hydrates; invalid or unset values still fall back to Shadcn Ember. if (!validThemes.includes(colorTheme)) { colorTheme = "shadcn-ember"; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39233b06fd..8c1c508a8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -275,6 +275,9 @@ importers: '@earendil-works/pi-coding-agent': specifier: 0.84.1 version: 0.84.1(@modelcontextprotocol/sdk@1.28.0(zod@3.25.76))(ws@8.21.1)(zod@3.25.76) + '@fontsource/press-start-2p': + specifier: ^5.3.0 + version: 5.3.0 '@fusion-plugin-examples/claude-runtime': specifier: workspace:* version: link:../../plugins/fusion-plugin-claude-runtime @@ -2346,6 +2349,9 @@ packages: '@noble/hashes': optional: true + '@fontsource/press-start-2p@5.3.0': + resolution: {integrity: sha512-gxWFxdeDglPhEYSwEeooomcaFK8kSQYLQwVxfnOJeYRj2sPEHXjpVtFgEaYfqzOjuAJGrIcJOipu2KY5YkbyuQ==} + '@google/genai@1.52.0': resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} engines: {node: '>=20.0.0'} @@ -9530,6 +9536,8 @@ snapshots: optionalDependencies: '@noble/hashes': 2.3.0 + '@fontsource/press-start-2p@5.3.0': {} + '@google/genai@1.52.0': dependencies: google-auth-library: 10.9.0 diff --git a/screenshots/fn-058-medieval-theme-desktop.png b/screenshots/fn-058-medieval-theme-desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..82cff4cbc5be8e0be47b9770071562a7dfb6c73d GIT binary patch literal 19898 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 literal 0 HcmV?d00001 diff --git a/screenshots/fn-058-medieval-theme-mobile.png b/screenshots/fn-058-medieval-theme-mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..ba9f6487ea01a72e4a3967e427d553d5c6e7f815 GIT binary patch literal 14996 zcmb_@c_37M+y4x+uIy_fOF}VYFqnh} zV+&&$X8g|Re(v{u?&rCm_xa;D|C~AJe9!rApX<6l*K(fd>8LX?axek_z@(|6Y5)K< zNC2Q^WuOPI)K)xr2mrE{nyM;B0cnfl+nKEBroCS~y6x@l_qDEH?MN+aZzgq^)I3Fh&ASyIT2eWGX^DZQ!Pb|)2)1)XWq%y49)$> zBDnofN4o3OoNB4dE_z}NFFVN5lA(p2qPsgbK&4n0}NinHeo_Zv&DgU8%DNz+Qh@U z=+S(BCij++Tx4f^)Q>|JmZ|C<XJ?0l^E#5!|Zt3!9%Mg`^|>2f<)b=^m~s| z1(!^8=B9cI3+gQ0W?fS5U0TZ;lNi@lYD=;DqG3*GE4o6;3!cEr^4J8oE)_f<3QkUT zhu)xta0ts7k=(M06cxLzHJ_%a%xQC?XywJ9Z^6zT+>z*j?igc`8lRIA7!qqnsFC+;HkkQ z3WvYUv@1VF51fyAU)Ik7@qVTv1}J;W>;#3=08i7F?d${&1M$6^ddFJ;Zs1UmO4+D0Tp#cZPscB}orF2;-myKCoy;O{VmS4#-wER&7)`jOH&@Sc+hr zR2JEQkF1(eeDl$1b^zcJhhop3I`MX=YLF{qJk=#*vnfqdJMsNccc-^|r=xc2=Gcd? zHln~o!FUd6X=XSXVUajf3|p2dkQy!!I%pfU4WFMRQVSbpNFA77{@DU? z76={fgO4XQqgpTJ-wYe__$D>z=V0xU9&*{Smq0XoG1{4Iqh9S)m%#=w7`E<~WFw#V zTK+(E#u5T)9*CX%g1dQ^-CIc%V96auvrE7d9*s^$5`$=XA#sQpDw+V+3=L<9yV{Tz zPPwl-O)YoEAR9d5rg~r&U(3LW>WP8tp?v%M9(9p+mB6*hvTN=H0D*tEV#0v zam%Zv4hsEJIJeU^oyi+Xi*-JsBN>jt3W%DdMHry0sA^!}h3d{+oi+2>PIe2eE49T< zz3=Zo6_T7mtDFF?#q_B74*6k*Q)u0oI2~Eu(lbDq8PI>4-JzFh*EQo7b7mDni+_-8^GYtKz5|HP4PNS z%z%d29N{pykixdDAM}m`=QGQ)&V$g5kD&tXKlTl^afWnmmg-}Ft{cqcl%NWaX#)UX ze6t_+Srhu(cuyNMk_5T1h}&(nP_c-iOc;o)WnbiLVr+?Y(zwFC#Q~Tj<(j{2$ z3|m3fEyp1pIV*B*#!yu+nUp7A^RwGqqQv?cZZ31fP2oy*WQJd7S(#DEa=ngHhMb$x z;;vyPH)`-MsFF4oM`GH}Vpn93${<`g$+AkXh}TA?8nnl~7GDNg;3JnLt7!mYTs}vH z?_*BW{y{G1?Vf%D9=}Jako8`18`qDS#hLd9PgQ-IXZC0*#Ee)qEBAAbv^>k;C+uRH z7lS5>9V6!hBZ8|ZKGerFu_ zyj7KKq&(vBUd16!g*G?1V?^g(60gy3y5`kaIThwcZ(uG}4%?9>IFBIW54>jRSB!eavy4#42Puh0!AN4EQFSwZD zq6J)!%$%(ERjOg%MUtW!+6l}`9iSatK zYLQkpf;f|kx%{|Mh2}AJ;}oNU_#5cs%TLUPvHfewvS%M=Q5J#g-S1T5DF=)LfJ$m^SC*n|_VPS4R5} z%;v2(o?Biva%?8O6<_YI)<#1&C$cf-O#$^b?wE~tl~b<_WR?9X2BDtZ>NQf@>rxAy z--vNLL|y%Xh6t}Sc~(p83!QwD&+|E1A8g$+ic&$?t3Q3e8qiV8N7}Ykl*?9>FNZ22 z0{hx4VcBNFYkn=}iBkQe9TQK9L7b~ka2$PexNSc4uhw9znmB@2t(*Oa7jEm;53o{= z(Qn&m{RvJIMi-P0GXP8=s-UH6wsMY>!y?$SCrJH}pBQ5rRbg;j=PsAnP9^l|XJpF3 zjY8`aJ9jQg<(8aFullmH)%LkEa7fA~X!0~!5ZBB!*Ok-z%k8D1ow5aX7^Ji9C1c~%M3D6V~( zGKD+eCG9QQbfRwWj46^{M;l#^+NGmWu8?W8k6VhsrD1A3L7g}uJb1_$RBf`m8)sn@ zEp4jmi`Q9VJE$F#`L*6=E1&;Y~=2z-TK=&a9V zMe%op;xElsZR~4*u0Fe3HmB0t58B*Cd_AHXZ?n@)+@j%i4J;EkC5^T2{ zUEyfilC{QJzgHd}_5fh1qCU*!{ENfja1a8z$up#P9C{OYV^S`ObR|gpBFy?ZIUxGd z41mBe2eg{VDXUbBXX3i~lyvGi0FJne7+6hg`iUPES0~D2e+Bj)dJxhb9L0yhj|^?5 zWD@jrjQZbmWnw~rF1CHc`T=+I0BCMQsnlu@G@Jhg@%qa;{qu7EpZ1XFW}&WFW6kZt zKkebSX3WKtCQ*|w$OM=`LX=Y$WqsogjFM`9lf2^tC_m0FQ!uD7+H4v)6dGg)4S;GQ zBB;odtvJv6Mo}Nc;QCW?ZRf+;jtN`!cZ>X+TlrZLX*hP7%!M%ak zX69s1ka;wQPloov-&9{dUD5$-wqbG_XE=B#B#9F>bisao=LF22o&{3GBl3zP!c1g#X-!Zg*Tt7(L3a~yKMzOzHT2~eWK8178f))`pW+1-<+3f?n{Y4L_q0-v_eWTXb z-9sevyDa%F+0r?t)8gWwZ&{~%55I~c$AV;%ZFYTio$a1)Un*$6Mt#d>X6i04hdkeH z3bk1dTdTmVBJ$P{_-KxZ2;WgoHps115w5KaruW2H^_Ulji#De_7ZBj)@1j@gH9OX6HjrFvoRv;dbzwaM)@+bbYiLn@%^TZ_gmc zo)*)De9nNhlReb}Jz~S9hGM%d-i=am*+rD332a%bUV4alYntaE+EfeWGU7_XDwYss1c9Df@j_BKa&W(lF+uI2U20$)c zP#5tB5J{VIMhE>->{hbUEiLeAd`&z966t>tf#`W`CvbWD%*`L_R+(`Mrw|cNUDUock(lub3~wTyyb;Mkd*>ts_z3b8kXnHr z2@8bkp@W)R(0)HTW3~!4!}| zEaXwlN`FF{asFamvhPGDmBo0^&_Qnt7IL&Lgl!fY?iuIWoEbR338UY(%`=-R`j&C6 z-2A2XTw&t#Al}s@3{0V-wxI>8+RI*VFUB}npgN5go|bml?X4> zpmj-Y?y~d=wf-Kzq_AE3wi>*mNm=iF$xR+gMYVmP(=;2)Jl?~W8`UbZJhkO#TCfdW z8U$B3IPgnEP;!3Ed$W_qQLSgWD788*COpO>MtfW9FjJmI^ZcaSTlr$)lY10m@}<+t} z?XALDJZxHOruHoAmwx^{1L2`iK6S~B)iM;tWl-+gS_nMeh7Mp4Gp4d8b)$J=-Zv%At<#Jb+uU{%r&Tf0L_;)WKi@f?sJ zT@5bhS{}!y6AAgNspBQ_dm^DogtP<&kIZ=tK5DmN9@^|;p@0i zBP8#D7CzsFCSpdbSW_p{d}=?lDuR#iO0bxQVt7sq6e3i<%=B@qALOx>pz7!InAT&$hQkDktGW)1U9YitD?i*i)8$&$8Ni@N&~a=b@S#h_O$( z-<=^d_Lg(BvU|N1D>hp#I)$MKiq4Q;tOF~Tmde&t>B-X(txlgFZp86i8S75M7_^km z!;>7z_47jZ+|j!M&w977sKa;~@3|5_7Kpp14&LHH#XZf*YFgP-@V`!8R?@i(Ih4%b z)AdfjMPFk?tie_%ix(Ez655?^lNB#oVs7jC!d4T8Xw_a~y(o`3Y3uo{Ym)1kFHa-+ zlOpKwT-$I@R?L4VE{vM2VJld3bUpz8hNF-llRERThFp{uo%yUcp@?6>u%$ui%1}hb z70HnGvKK+8%}s`$ka|Dj=l3Yv1*4Qz24hl}t^)M7il(9Ui^n9hxMbh0)tM?(X8Ts1 zh{Tb=%Y)N8VR~Jnb%wEe0-WPkCEO!M@E|+;Nff&(=+#kaOzCUaRJ1mk*ani9H2#p{ zLG-39114U@KOf&fzNvlR{}IPdmo9p%ItrB#-D0%1Loq2i;}tsH8964*5s(@*`NRcX zt6uhX`qvwp8g+!#CoR$ht)yMNn50b`e?JJ*0V)IiFWJpP@TDhBE(EkA_&5W)+g|}sCJ&}tz=sZg6?4gj z4Zcx4in7g@M0SNV%UzhC0v@n4Hg&KZ2EE^TVNt4Y7Sdh=vMdHQ2`bA9x^)j^S$ZD- zH_KWzum^GR(z);Jkn{9gQ*{dSG7axsb{X7jmbQ@8t2TKE3P;9EwOqA&DetMa?-+l} zJ$T`_W4wtAREzw$qF!nshtV?JlkaUo(>*BHv~36`Ywf$mt;^BcQ2mn3RzlNcZ?P&HsM4%p5kyQ@JJXi%~kUq9tPj{K< zffkTZU!%C!Sx8UbHm;~tH7?~iS@6{K=6c1M(3;Du(x;W%RJX&fJY`OPQXt;N$~8` z)S;x~O2-ZDHN3@Y;RiNPK#~*v6*pw*@%GLT0eU*91{h>9XbY!N_6=vG#y~c6^uj^z z(+YeD1L-G7k;GVo#U4t5(Fzubvehxrw1TtK-&!RD%#%-?N%7+i!wi>t(%tD{#}pzZ@I?uZ@Po3(6><; z*N{lMlhzBogtaeX)&^ZY;uciDn}8pZH(&B3rjjalD~DbkG|ORp)sBz-;_^rw zdA-7HCsiS^mIFl=hg*TMiK6jLB8QE&>@APIo)hS>#^zp^Q~BooOoKh${k?2`S=ep5 zM(TVSCD^A8t9`TUz7Rl1RnmTpg>0sHA#-~^X1)#dti3%vYzk?Wxh;Dv*QOnzF`w#s z-I>SpNWKB(excr0a5?$cF$7yj#hch%Y(;HTGycYg^b)W@=zizELm140Ee3YaNCJub z8+ecdl8jVLTf3Y8Nm13F$2o42(}lgmV!|yyr@8aTSgud#M%MY7taK|Rd;zNIUdD&X z5<fXoguo?l^u4$aFZyN#3bTfctNO{ zKm9u;^9c*xjNtVCd^sLgkw3;tOqJQV{YosNo$pCsW~LOar^4b)}kPuC3P+2 z#`yMY&x1tu_3+qyFx(inEDm_7{STI}bArKP272DoXya0Ym!Rx%GrsXQE|A@bG!6_L z-wnw*?ykFJX5ZY0)sk-P8sW13#bA^mGZJB;^oXj$~G}F z4m1-QvQ*p6zOmA8+)Znm&+I26l*SP;T=`m+=apn1XfK%Of@>~w&o$tVR(06PG&)u_ zzdZeGHuMJK4FH-isyUrha3IdVhn9eo`N59@XTyuQUPe8+@Y`PgUDkqcoue)JMKC%s zi9cVTYguo6>3%jC9f@SKDwM6Sn=wFltKlt0Zq-%ku(4TtoKTYReczxkbGU70^u-VV z+$9}Z8u#wD&676_s>`g%jY$yx_2MY35# zAz+FbZRmz5GU?|kpm@KL7|S}}(z?=p2#dO+EAXQ6ow`jE(}`x?--@8)?5lSC9@g zH=yDAMi*h$a`(mEwG8ZKjjMp zGG^l#7|9n^FEZh5UHPt^bi>x(bK;#EJRXGoRTzX{&4y~{cr5}C(*(6>Jg$0R z4#OA}MKF3ENt>EG-+UEx6B;{Wh1sVcxxQL|RcK{gc+@@pFnC)*=oT8OxoFkKC)&y~ ztrtHgs3DLa8hRAC4;(&J=kxfkfMAzh>>?bb0@M7gzz-dgLD9Jw7}dr;u$$(99JJl< zo;{A97ODy81)Svq6%_D#<)876d{ZXw>+!8N5#!U+BZoO`ABw16-ktMDRKpKwB!V|< zVF4RX2z)SXrA{Q1u58aA zE9=6ICaJ@~@f>fo*?1>c*(K%+>|h=uR_>t8?aeEMDCg7tF0zYn&WLwDg`a@L8^XY7{-OBq=kcu;E z6o-dpod+0fi;?l*9q{<~=RZzSvw0@Q9wNpbrO5^KN)$N-H!GZp^XKoKsM#g$_1*p$ zll%*Yhw^`sc~Jc&Fo$%NMUTP38!(SILrv??NuIbH+gSAZ{6ZZMAEAp< zjDN=iM&MVD7uh|=>J4mBnDYURhtVugs_Lx_ps;5rf+8CbPr zE?%VSX0B+yIPp%;V-5A>W>Y3O+8YzI0Bf}w(}hnUwhhT&zFDId#K%!b&dJR=t?{p$ z6#aylh7MO}`_)w)xrrXYAHtLxlKXCww|Y{nN!xLoQUv^}VTOljYH2jj=)DE?p@QLS zN^VM)CR*Rt94t4;z=I^SgU~T#{9S?jqhI};J64W5YxVyO8cpDfnZpn92UqMqIxguE z5;F?hSTrl-3DsW3a^Y)i;L}>NHlr~@>_`1}J!};8yng&-BEk5=UebDb|dA<$W zu%p#_Mc@W|34J^LokHk&nRBQ$aMt*NxKOg@OK#^B`7QCLsMF_U=X7k5@zdMWt#0Vu zCr!RwlVguQn9175T3iXOER-p)@>5DpR5WeqW0P!nPJYb1Q5@Asb3j18g5%~>OR+9~ zKS7RLolSuw=M}VNw=kwU`l*RJ18cgW7CB0bx3Y#2{CtFBTL~N87e^^yOC^YE+K&(- zgEggTZ~WXW^couFtCy}8lZP>0OP#XS`JB8DKAzY|%5k7w|9@ahogcuW4)zHu58jXp zMtWpC_4r?(BLyPk|3Qf;NE+qgOQtUi?L*B@#$;s+L`*Iu9YTYy5~?7@|7_?Wzuv*}d0Zalh)^>yak)1b`(_dI7-G6h-xk>;DUl!U~I;Ocgy^c+#-G$Sb|f6lx)T;DPFC+zfq{ zw=?rN=Fu?-NQlz5cOP7W?~Zoit^o|G>oCN}totqy?>t_9SIWxJ{B$b@CC%@5K>3XZ z?jk&G9p;T^c}AV5ihITMT`=@uJ?+5_JX9mq^06Ub-Z=)*KYGEu#!SD(31E>bYI*i) zzocUFnK5EAUE3oY=yN zpT^9|8aKiGrlSriOMJS-{zEF^JXe5O=(KYPo^Z}X9*7q-foggkL6+|wxP$tiM&JM* zzXc`u7`6Wu@IiJe2p)=pjp?8Pt)A4>Z5(`bzB6VlI0B4krhGXOQ&VfaH`d@*aWO{D z>eW45Vlj4gqr+(}sh6XGr>3#QYcV7_(P_`UlfRg3NuXuWp(@7gm2eeV@;5S;5M6EC z`O{esb7+0N;BD}H;DBI{{dQobb4ekBa8d!TwYL$JQfyk`RchlnREuz4_;9o%B)Qpd z@=8$3mtgn%(hB1i>bX4|GZhQi(UvbsZawdOGC5!KgnHjMT%f5R+H{aa>PA2Tn zYH~u{%%5)c$wXl1cdLo*Ckjcr>qA@-UwW!qv6~*YnR4{3=gOG%RKV>JOJKDT(_r`Y z@F}B=RLr;)V(d2wbF1AGjEr?B$`eC9c8)7Fxl)Eli4Nvv+t;~!D!JX3j3Y7w#N76V zNbfvq$eV=W>q?1k4Q@Od^|d&0J^RO&Vq+6#M0bplbY)*jXkHLj?k9FUe9{4Y(FFLS zUOO>7HH2jq(OTWSfOXp=%T|7kalpD`55@NpXpO#tL2bwdHPaA3k$%17v7t6RuftDK zvBB497mMRg@w>gSI_J)Of7L7(wj!YDU#nN{C#=Xkd;d$)oXe@`!NHEB8ls9lq+9nO z=)uH(jvOiBbk_x3GuT&MsD87j)ij$3OKvUm1%3O+=3=U3LLn|-O#g0!eR=hAqdiT0 z*ZvqJL6|;dg8)DH24h|I_J;(&99TC-UQJYNoh|1ZoMfe;p$LtTzzXX?T}5iqE_~>YBl^$I-?Ha!$^RRffSPiG^S#3K%9-xqiECdZS)93Ld2r zLTRN>_kLM)gL<8$SEKjXf$9gAg5^yBFnOt#16tF7xJTEf9^7+I2_%u1qnjH(Gdw!v zQGb=h(Kf5&f4Wty!2b%&)x&(6{|FGjbRX?3Sv+WcLktqIJ_K^Q)Kf9i;aL$gHHo*3 z)un4d*Qstcq{l&!TG;sJcVVO7ABBwuyM3tfx#%9_7&$~*Zr)6vY?CuN+hn<6fLPIB zxobp(ebXJYpAXeNH`o+~GUTSeuVhHORCML*r`J1+rC&3uUE(E%KgC`&E#%YIzM#_! z7CE|*8;8Vht;x1AnL3@b$mVpyNH`ev*k+#=wn*+k)yw7((S@i-TDrparORWH6MK*B zD|n_nZwD(>l@0nGolZ+I6G;=@sjJL%A;8Ng*micLRw(tG@0Z9rRW!k=u^V@)xE!(X zYMP81ie2(HHd39j1=a2zdkRr5=V6Oxn7B3B_qH)ZlA|RX9L>dvWQ#Jwk%65fbLz#4 zIi>yEw?|q0cTz<)>*kK5`+kahz1<2M8uD&qM!l7CbKgRICJM$3%ZH4As^WgP)B9;& zWifb^_6J6=WU-W7>g+H$uv)ZScxqIwDn+_pNs#3cpWuhkRmCOSwNkM6H)aqXO{>b0s)nv(%k1x3MfKduEC z^}Hp=Z4zZ&wpnYZpH+3FI?0VA40s(oozua~mARJpso$psyJj)5xV7u5G+6CzJs~4H zxB>8Hf-!JU!u^)+ujWYdAYn^_D>Bp0)AAxF7I=`a2G-L_8BIyz>J*fq8VDrfog2E9o@@0jxK)KR*JaLv zjWYHFs`Gjy0^{8M8O5)o&c%JaKNUvbHh0bvwKQUl-tZDeQD)juJ1(6xaq)h&aI0HY zJ~x7i#JG}5uk$)z-Q>U8#p>#Nw;DB}Y}1w2vJCbp5CH}prR2tUSh7qc~ex+T9{qRqi$l&7YtR_%>A`g#6kOXU60<6^3E2y{|XRK!2XA z71&Z0QG4E9Dty8L*8`*e9dfUPB&NXUeu>wn?gHH0YhdaHkbLYoyyk9&-IR@eD z@zW0=uUm*<7SG9pSdaCkgK}>*S@${2cDu+i@U+HSVypT$<<_UG;7Xm3NaxsBzj#dX zvokWce&B+F!wy`exo~Z`LpVl$Kb1W_N%1Yb-pl3=ewNRSGB` za3zcuS|3bLrS?D24QXZ4?3B)=$)v6kE`&bhj=*6}FjM{SmW7V?`-#>E+2`1S{nr1y zqWdq|3g?WE_E4nVU)$zw$X2`F@%U(t-_Hrv+Fdh}y18hpczG4vrve)(Ev`#EYQHz} z5I2O!^s*kdv&skr$i>wcwAVn$@K-G+|D10xO}am&kt~! zb}0VCS9-Cgh*7%)^-GI#^LKhc z<0xqC_en36M*v{10QkE<)aGJz9FRPh3BAsVl!8LjgxNg>MpY*e#N*t$7}uQN;uOlX zn%bWi8{_Eb{rFkXwBN}oEp{4IZX(IE#2eFs(8XK^vXTZW4r?h>EO$&XC zjNZP&bAu8i1U%dAtZI68Xuic1_c{~>-cK-db!K^Jup>@Y zrSjK@h_KLv=#hGs%TAg5R&L@&UgWs@VdU;)YJXtD+z__T zDtE5W%>kJ;PM8bYL4fRN#zmExY2&J=c3`d7ix ztGNYgbb;qm+-4w=i#-D-;68vEIsi1_fBh2%Z)F0&Z3J&ni>Uvu*`E)BnhLOExXM1w zqt6rUl3Qq_|7N2}f(2lCLl(N}n;Bex&!xPT1y$1d7yEQ}kADP+^&P;t^I!}X)4tij z;dfm;p_(eLs+40@53m0rOqak~Y5)jaS;RS`CO*-kGi0n`wpL=TD_)-tq+2{Z08D4D zH%VCd+`)YOco+$Fc)KpFjp9E9JT(Rv2^6MT@G4mdIr|ID7(>8}v9*tx_(SlEf^0Cw z2v9S}RCS;da{7Aned^Z?zS+!)&9!#T+gj8b>wRMa@KW|o_MbEU{fGCev=k|$)g>r5 zFgdZmGTkb=)1^hFMWkg1#vK8dV+ypcYm`zx24OIS%U)9&^hwVoc%l76RhmS~7Vlya zaN9%QAK5A2XP^%)duFGc^*yzpK^u;gp1*R&K%@0Le(MU8LZBP2-zzX(mzT47`}xdYZe5J zwBnGxny|PVz!igAi*;i-x^n&B@B99SgBAy%2Mq)uw$khP#}^uZgA;0dJx^T7M9Ul1 zKWqo^sW50nuPSur>&NY%YZW#33Kyp=3O)8rPHcD6Jh@m~@=|4YMoPgVlgDK-uEwAe9xO0!3d!eQ2E*>^ zB#Swp%`bC>Iy~bX8n)c`#b)Iq`~3=HgiaNL`|o><4q(XuB@y++`Uq`0%oLKT`oW9RU4hn!nur*GYe%Hs7}`)MER;GcbSJo_$NRk1?t{w-2;G5%;fu zRt`#os|~^ujHMg^s8QqJXMgiYwDynj{`V5PZyCWD-G4NX)X{%G;{L~Y#S-WMYVF&9 z9KEe4?H}U>9qs=(XzJ`hBJzhu`Tw%yTQQv;l&s@D{F^3u(T!Q)2Tg^WL%Jq^ejIWu zQxv}U@z=QVpiA>x(Baq14msFH(H=zC87|WT$F)e*FPHRsOu;Av9nk%^A1v6mg2@yf z_%j+nLu8KN!-zb0^4WSg@L>9dqLK@UpQkY{VhMhAhLI72jM9cs@Uh9XTJ*L61=Yha3v8{y-Ir%N^^u8bp})3Zxi+;gZAWP5KY z)z3ZKb1F~f^l)NNr8dEAEc(ini)5cZN#<%_%@o-)bZ>qFueI1(rEPuRXJoVhT|GVu zABV*wxEvbj(=laai~b?e+D+jmNep8CI?vA`%!qL6n2>`(ea`8|)imq=tDSvOBQ{9TLgKm|3s<{`aFR3E_?UOW{PdeO;UqLQnaTV+ndwvi zM=~=Jmt5a4>(v=!O{^Sqn$ID(f87!Bhc7Hq3WhLHC9jwQ(BT~z$7(3d+_EK z{^60ou;^@N!Z#P&S48(BzO+4Bl;kMGT~mkQ`(-mbw*4CUhjtvALJKylmf+PbdoZ{2 zX!kTX&Cz7^&$PvX5x<==@(b1^m)98UAy|C9-vTB+u-Ce@-crOWb^jYXOH2!zC!6%7 z$9ZJuMZKar>CHMtlO$ZkTtvVdERbK{|3^Ag=8tsdsC-bw_@*&u&%RfFx%4l|%z7P{ z2A7s1i;>F8-Ag&2%0witdbm30!qp`kSQ!7|XoG&mDaanr*Q1qkPnE-a0r01(rlVSV I-7ft90mVyR`v3p{ literal 0 HcmV?d00001