feat(decnet_web/theme-lab): scaffold dev-gated /theme-lab route
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 <Route> 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.
This commit is contained in:
@@ -8,6 +8,7 @@ import ShortcutsHelp from './components/ShortcutsHelp/ShortcutsHelp';
|
|||||||
import { ToastProvider } from './components/Toasts/ToastProvider';
|
import { ToastProvider } from './components/Toasts/ToastProvider';
|
||||||
import { useToast } from './components/Toasts/useToast';
|
import { useToast } from './components/Toasts/useToast';
|
||||||
import { useGlobalHotkeys } from './hooks/useGlobalHotkeys';
|
import { useGlobalHotkeys } from './hooks/useGlobalHotkeys';
|
||||||
|
import { isDeveloperMode } from './lib/devGate';
|
||||||
|
|
||||||
// Page components are code-split per route. Each lands as its own
|
// Page components are code-split per route. Each lands as its own
|
||||||
// chunk and only downloads when the user navigates to that path —
|
// 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 SwarmHosts = lazy(() => import('./components/SwarmHosts'));
|
||||||
const MazeNET = lazy(() => import('./components/MazeNET/MazeNET'));
|
const MazeNET = lazy(() => import('./components/MazeNET/MazeNET'));
|
||||||
const TopologyList = lazy(() => import('./components/TopologyList/TopologyList'));
|
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 <Route> out of the bundle. */
|
||||||
|
const ThemeLab = lazy(() => import('./components/ThemeLab/ThemeLab'));
|
||||||
|
|
||||||
/* Minimal fallback rendered while a lazy-loaded route chunk is in
|
/* Minimal fallback rendered while a lazy-loaded route chunk is in
|
||||||
* flight. Matches the house "dim mono" voice — no spinner library,
|
* flight. Matches the house "dim mono" voice — no spinner library,
|
||||||
@@ -139,6 +144,9 @@ const AuthedShell: React.FC<AuthedShellProps> = ({ onLogout, onSearch, searchQue
|
|||||||
<Route path="/swarm-updates" element={<RemoteUpdates />} />
|
<Route path="/swarm-updates" element={<RemoteUpdates />} />
|
||||||
<Route path="/swarm/hosts" element={<SwarmHosts />} />
|
<Route path="/swarm/hosts" element={<SwarmHosts />} />
|
||||||
<Route path="/swarm/enroll" element={<Navigate to="/swarm/hosts" replace />} />
|
<Route path="/swarm/enroll" element={<Navigate to="/swarm/hosts" replace />} />
|
||||||
|
{isDeveloperMode() && (
|
||||||
|
<Route path="/theme-lab" element={<ThemeLab />} />
|
||||||
|
)}
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
21
decnet_web/src/components/ThemeLab/ThemeLab.css
Normal file
21
decnet_web/src/components/ThemeLab/ThemeLab.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
23
decnet_web/src/components/ThemeLab/ThemeLab.tsx
Normal file
23
decnet_web/src/components/ThemeLab/ThemeLab.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="theme-lab" data-testid="theme-lab">
|
||||||
|
<header className="page-header">
|
||||||
|
<h1>THEME LAB</h1>
|
||||||
|
<p className="theme-lab-subtitle">
|
||||||
|
dev only · primitive zoo for theme regression
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeLab;
|
||||||
@@ -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(<ThemeLab />);
|
||||||
|
expect(screen.getByTestId('theme-lab')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/THEME LAB/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/dev only/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
36
decnet_web/src/lib/__tests__/devGate.test.ts
Normal file
36
decnet_web/src/lib/__tests__/devGate.test.ts
Normal file
@@ -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<string, unknown>).VITE_DECNET_DEVELOPER =
|
||||||
|
originalEnv.VITE_DECNET_DEVELOPER;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when VITE_DECNET_DEVELOPER === "1"', async () => {
|
||||||
|
(import.meta.env as Record<string, unknown>).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<string, unknown>).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<string, unknown>).VITE_DECNET_DEVELOPER = v;
|
||||||
|
vi.resetModules();
|
||||||
|
const { isDeveloperMode } = await import('../devGate');
|
||||||
|
expect(isDeveloperMode()).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
12
decnet_web/src/lib/devGate.ts
Normal file
12
decnet_web/src/lib/devGate.ts
Normal file
@@ -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';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user