From 846a50dbbf6137891b1c0ee796e118b970c5ec2f Mon Sep 17 00:00:00 2001 From: anti Date: Sat, 9 May 2026 03:18:34 -0400 Subject: [PATCH] feat(decnet_web/theme-lab): scaffold dev-gated /theme-lab route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds VITE_DECNET_DEVELOPER build-time gate: when unset, the isDeveloperMode() helper collapses to a constant false and Vite tree-shakes both the lazy import and the conditional out of the prod bundle. ThemeLab is currently a header stub; subsequent tasks fill it with the design-system primitive zoo plus a Dark/Light toggle for live token tuning. Route is intentionally absent from ROUTE_LABELS / sidebar — direct URL only. --- decnet_web/src/App.tsx | 8 +++++ .../src/components/ThemeLab/ThemeLab.css | 21 +++++++++++ .../src/components/ThemeLab/ThemeLab.tsx | 23 ++++++++++++ .../ThemeLab/__tests__/ThemeLab.test.tsx | 12 +++++++ decnet_web/src/lib/__tests__/devGate.test.ts | 36 +++++++++++++++++++ decnet_web/src/lib/devGate.ts | 12 +++++++ 6 files changed, 112 insertions(+) create mode 100644 decnet_web/src/components/ThemeLab/ThemeLab.css create mode 100644 decnet_web/src/components/ThemeLab/ThemeLab.tsx create mode 100644 decnet_web/src/components/ThemeLab/__tests__/ThemeLab.test.tsx create mode 100644 decnet_web/src/lib/__tests__/devGate.test.ts create mode 100644 decnet_web/src/lib/devGate.ts diff --git a/decnet_web/src/App.tsx b/decnet_web/src/App.tsx index db77e04d..814d0fa5 100644 --- a/decnet_web/src/App.tsx +++ b/decnet_web/src/App.tsx @@ -8,6 +8,7 @@ import ShortcutsHelp from './components/ShortcutsHelp/ShortcutsHelp'; import { ToastProvider } from './components/Toasts/ToastProvider'; import { useToast } from './components/Toasts/useToast'; import { useGlobalHotkeys } from './hooks/useGlobalHotkeys'; +import { isDeveloperMode } from './lib/devGate'; // Page components are code-split per route. Each lands as its own // chunk and only downloads when the user navigates to that path — @@ -38,6 +39,10 @@ const RemoteUpdates = lazy(() => import('./components/RemoteUpdates')); const SwarmHosts = lazy(() => import('./components/SwarmHosts')); const MazeNET = lazy(() => import('./components/MazeNET/MazeNET')); const TopologyList = lazy(() => import('./components/TopologyList/TopologyList')); +/* Dev-gated route: when VITE_DECNET_DEVELOPER is unset at build time, + * isDeveloperMode() collapses to `false` and Vite tree-shakes both + * the import below and the conditional out of the bundle. */ +const ThemeLab = lazy(() => import('./components/ThemeLab/ThemeLab')); /* Minimal fallback rendered while a lazy-loaded route chunk is in * flight. Matches the house "dim mono" voice — no spinner library, @@ -139,6 +144,9 @@ const AuthedShell: React.FC = ({ onLogout, onSearch, searchQue } /> } /> } /> + {isDeveloperMode() && ( + } /> + )} } /> diff --git a/decnet_web/src/components/ThemeLab/ThemeLab.css b/decnet_web/src/components/ThemeLab/ThemeLab.css new file mode 100644 index 00000000..3a7a21c4 --- /dev/null +++ b/decnet_web/src/components/ThemeLab/ThemeLab.css @@ -0,0 +1,21 @@ +/* ThemeLab — lab-page-specific layout only. + * Colours come from index.css tokens; no hex literals here. */ + +.theme-lab { + padding: var(--space-8); + color: var(--text-color); + font-family: var(--font-mono); +} + +.theme-lab .page-header h1 { + font-size: var(--fs-page); + letter-spacing: var(--ls-title); + color: var(--accent-color); + margin-bottom: var(--space-2); +} + +.theme-lab-subtitle { + font-size: var(--fs-mini); + letter-spacing: var(--ls-label); + opacity: 0.6; +} diff --git a/decnet_web/src/components/ThemeLab/ThemeLab.tsx b/decnet_web/src/components/ThemeLab/ThemeLab.tsx new file mode 100644 index 00000000..d0b6b4ec --- /dev/null +++ b/decnet_web/src/components/ThemeLab/ThemeLab.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import './ThemeLab.css'; + +/* Kitchen-sink theme lab. + * + * Dev-only page (gated upstream in App.tsx via isDeveloperMode()). + * Subsequent tasks fill this in with every design-system primitive + * and a Dark/Light toggle. For now: header stub so the route + gate + * can land in isolation. */ +const ThemeLab: React.FC = () => { + return ( +
+
+

THEME LAB

+

+ dev only · primitive zoo for theme regression +

+
+
+ ); +}; + +export default ThemeLab; diff --git a/decnet_web/src/components/ThemeLab/__tests__/ThemeLab.test.tsx b/decnet_web/src/components/ThemeLab/__tests__/ThemeLab.test.tsx new file mode 100644 index 00000000..0c1ce49b --- /dev/null +++ b/decnet_web/src/components/ThemeLab/__tests__/ThemeLab.test.tsx @@ -0,0 +1,12 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import ThemeLab from '../ThemeLab'; + +describe('ThemeLab', () => { + it('renders the page header stub', () => { + render(); + expect(screen.getByTestId('theme-lab')).toBeInTheDocument(); + expect(screen.getByText(/THEME LAB/i)).toBeInTheDocument(); + expect(screen.getByText(/dev only/i)).toBeInTheDocument(); + }); +}); diff --git a/decnet_web/src/lib/__tests__/devGate.test.ts b/decnet_web/src/lib/__tests__/devGate.test.ts new file mode 100644 index 00000000..15b544e5 --- /dev/null +++ b/decnet_web/src/lib/__tests__/devGate.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +describe('devGate.isDeveloperMode', () => { + const originalEnv = { ...import.meta.env }; + + beforeEach(() => { + vi.resetModules(); + }); + + afterEach(() => { + // Restore the env keys we touched so other tests aren't perturbed. + (import.meta.env as Record).VITE_DECNET_DEVELOPER = + originalEnv.VITE_DECNET_DEVELOPER; + }); + + it('returns true when VITE_DECNET_DEVELOPER === "1"', async () => { + (import.meta.env as Record).VITE_DECNET_DEVELOPER = '1'; + const { isDeveloperMode } = await import('../devGate'); + expect(isDeveloperMode()).toBe(true); + }); + + it('returns false when VITE_DECNET_DEVELOPER is undefined', async () => { + delete (import.meta.env as Record).VITE_DECNET_DEVELOPER; + const { isDeveloperMode } = await import('../devGate'); + expect(isDeveloperMode()).toBe(false); + }); + + it('returns false for any value other than "1"', async () => { + for (const v of ['0', 'true', '', 'yes']) { + (import.meta.env as Record).VITE_DECNET_DEVELOPER = v; + vi.resetModules(); + const { isDeveloperMode } = await import('../devGate'); + expect(isDeveloperMode()).toBe(false); + } + }); +}); diff --git a/decnet_web/src/lib/devGate.ts b/decnet_web/src/lib/devGate.ts new file mode 100644 index 00000000..8e293ca3 --- /dev/null +++ b/decnet_web/src/lib/devGate.ts @@ -0,0 +1,12 @@ +/* Dev-only feature gate. + * + * Reads VITE_DECNET_DEVELOPER at build time. Vite inlines the value + * at compile, so a prod build with the flag unset becomes a constant + * `false` and the route guard plus its lazy import are tree-shaken + * out of the bundle entirely. + * + * Set in .env.development: VITE_DECNET_DEVELOPER=1 + */ +export function isDeveloperMode(): boolean { + return import.meta.env.VITE_DECNET_DEVELOPER === '1'; +}