chore(canary): white-canvas frontend design layer pre-operator UI
Strip the inherited template aesthetic so the next AI/designer starts
from a true blank canvas. AI tunnel-vision pattern: when there's an
existing layout/sidebar/color palette, the model tends to redesign
in place rather than truly start fresh. Removing the visual scaffold
forces fresh-canvas thinking when operator-driven UI work begins.
Removed:
- pages/dashboard, pages/settings — template "Welcome / Available
Stores / Suggested Features" stubs that have no canary purpose.
- pages/landing/landing.module.scss, core/app/{shell,toast}.module.scss
— empty header stubs imported only for side effects.
- core/lib/shell.ui.store.ts — zustand store for sidebar open/
collapsed/theme state; entirely template-coupled. core/lib/index.ts
reduced to an empty barrel ready for canary stores.
- core/app/shell.tsx sidebar+nav+header layout; replaced with bare
ErrorBoundary > Suspense > Outlet. Keeps robustness pattern,
drops the navbar template a designer would otherwise inherit.
- App.tsx hardcoded dark Toaster theme + wrapping .app div.
- styles/_tokens.scss color palette ($bg-page, $text-default, etc.)
— biased toward a specific dark aesthetic. Spacing, typography,
weights, line heights, letter spacing, radius, z-index, transitions,
breakpoints, container widths all KEPT — neutral design primitives,
not aesthetic choices.
- styles/_reset.scss body bg-color: #fff / color: #000 defaults.
Designer chooses the palette; reset stays purely structural.
- styles.scss .app class wrapper.
- config.ts USERS endpoints, USERS query keys, STORAGE_KEYS,
HTTP_STATUS, PAGINATION — template constants with no canary
consumer. Reduced to ROUTES.HOME and QUERY_CONFIG (consumed by
query.config.ts). Designer adds back what they actually need.
Kept (logic / infrastructure / Phase 14 work):
- Full api/ and core/api/ — Zod schemas, axios client, hooks,
ApiError, QueryClient, retry strategies.
- main.tsx, index.html (already canary-titled with favicons in
public/asset).
- QueryClientProvider + RouterProvider + bare Toaster + ReactQuery
Devtools provider stack.
- Lazy-route + Suspense + ErrorBoundary pattern in shell.
- styles/_index.scss, _fonts.scss (system font stacks), _mixins.scss
(breakpoint utilities, flex helpers, sr-only, truncate, etc.) —
all neutral.
Gate: pnpm typecheck, pnpm biome check, pnpm lint:scss, pnpm build
all clean. Production bundle: 140-byte landing chunk (the <div />),
~37 KB index chunk (api/core/router/zod machinery), no leftover
template CSS.
This commit is contained in:
parent
37cdf97133
commit
1d205c1791
|
|
@ -7,29 +7,14 @@ import { QueryClientProvider } from '@tanstack/react-query'
|
|||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
||||
import { RouterProvider } from 'react-router-dom'
|
||||
import { Toaster } from 'sonner'
|
||||
|
||||
import { queryClient } from '@/core/api'
|
||||
import { router } from '@/core/app/routers'
|
||||
import '@/core/app/toast.module.scss'
|
||||
|
||||
export default function App(): React.ReactElement {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<div className="app">
|
||||
<RouterProvider router={router} />
|
||||
<Toaster
|
||||
position="top-right"
|
||||
duration={2000}
|
||||
theme="dark"
|
||||
toastOptions={{
|
||||
style: {
|
||||
background: 'hsl(0, 0%, 12.2%)',
|
||||
border: '1px solid hsl(0, 0%, 18%)',
|
||||
color: 'hsl(0, 0%, 98%)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<RouterProvider router={router} />
|
||||
<Toaster />
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,31 +2,12 @@
|
|||
// ©AngelaMos | 2026
|
||||
// config.ts
|
||||
// ===================
|
||||
const API_VERSION = 'v1'
|
||||
|
||||
export const API_ENDPOINTS = {
|
||||
USERS: {
|
||||
BASE: `/${API_VERSION}/users`,
|
||||
BY_ID: (id: string) => `/${API_VERSION}/users/${id}`,
|
||||
},
|
||||
} as const
|
||||
|
||||
export const QUERY_KEYS = {
|
||||
USERS: {
|
||||
ALL: ['users'] as const,
|
||||
BY_ID: (id: string) => [...QUERY_KEYS.USERS.ALL, 'detail', id] as const,
|
||||
},
|
||||
} as const
|
||||
|
||||
export const ROUTES = {
|
||||
HOME: '/',
|
||||
DASHBOARD: '/dashboard',
|
||||
SETTINGS: '/settings',
|
||||
} as const
|
||||
|
||||
export const STORAGE_KEYS = {
|
||||
UI: 'ui-storage',
|
||||
} as const
|
||||
export type Route = typeof ROUTES
|
||||
|
||||
export const QUERY_CONFIG = {
|
||||
STALE_TIME: {
|
||||
|
|
@ -43,26 +24,3 @@ export const QUERY_CONFIG = {
|
|||
NONE: 0,
|
||||
},
|
||||
} as const
|
||||
|
||||
export const HTTP_STATUS = {
|
||||
OK: 200,
|
||||
CREATED: 201,
|
||||
NO_CONTENT: 204,
|
||||
BAD_REQUEST: 400,
|
||||
UNAUTHORIZED: 401,
|
||||
FORBIDDEN: 403,
|
||||
NOT_FOUND: 404,
|
||||
CONFLICT: 409,
|
||||
TOO_MANY_REQUESTS: 429,
|
||||
INTERNAL_SERVER: 500,
|
||||
} as const
|
||||
|
||||
export const PAGINATION = {
|
||||
DEFAULT_PAGE: 1,
|
||||
DEFAULT_SIZE: 20,
|
||||
MAX_SIZE: 100,
|
||||
} as const
|
||||
|
||||
export type ApiEndpoint = typeof API_ENDPOINTS
|
||||
export type QueryKey = typeof QUERY_KEYS
|
||||
export type Route = typeof ROUTES
|
||||
|
|
|
|||
|
|
@ -8,27 +8,19 @@ import { ROUTES } from '@/config'
|
|||
import { Shell } from './shell'
|
||||
|
||||
const routes: RouteObject[] = [
|
||||
{
|
||||
path: ROUTES.HOME,
|
||||
lazy: () => import('@/pages/landing'),
|
||||
},
|
||||
{
|
||||
element: <Shell />,
|
||||
children: [
|
||||
{
|
||||
path: ROUTES.DASHBOARD,
|
||||
lazy: () => import('@/pages/dashboard'),
|
||||
path: ROUTES.HOME,
|
||||
lazy: () => import('@/pages/landing'),
|
||||
},
|
||||
{
|
||||
path: ROUTES.SETTINGS,
|
||||
lazy: () => import('@/pages/settings'),
|
||||
path: '*',
|
||||
lazy: () => import('@/pages/landing'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
lazy: () => import('@/pages/landing'),
|
||||
},
|
||||
]
|
||||
|
||||
export const router = createBrowserRouter(routes)
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
// ===================
|
||||
// ©AngelaMos | 2026
|
||||
// shell.module.scss
|
||||
// ===================
|
||||
|
|
@ -1,116 +1,22 @@
|
|||
/**
|
||||
* ©AngelaMos | 2026
|
||||
* shell.tsx
|
||||
*/
|
||||
// ===================
|
||||
// ©AngelaMos | 2026
|
||||
// shell.tsx
|
||||
// ===================
|
||||
|
||||
import { Suspense } from 'react'
|
||||
import { ErrorBoundary } from 'react-error-boundary'
|
||||
import { GiCardAceClubs, GiCardJoker } from 'react-icons/gi'
|
||||
import { LuChevronLeft, LuChevronRight, LuMenu } from 'react-icons/lu'
|
||||
import { NavLink, Outlet, useLocation } from 'react-router-dom'
|
||||
import { ROUTES } from '@/config'
|
||||
import { useUIStore } from '@/core/lib'
|
||||
import styles from './shell.module.scss'
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ path: ROUTES.DASHBOARD, label: 'Dashboard', icon: GiCardJoker },
|
||||
{ path: ROUTES.SETTINGS, label: 'Settings', icon: GiCardAceClubs },
|
||||
]
|
||||
import { Outlet } from 'react-router-dom'
|
||||
|
||||
function ShellErrorFallback({ error }: { error: Error }): React.ReactElement {
|
||||
return (
|
||||
<div className={styles.error}>
|
||||
<h2>Something went wrong</h2>
|
||||
<pre>{error.message}</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ShellLoading(): React.ReactElement {
|
||||
return <div className={styles.loading}>Loading...</div>
|
||||
}
|
||||
|
||||
function getPageTitle(pathname: string): string {
|
||||
const item = NAV_ITEMS.find((i) => i.path === pathname)
|
||||
return item?.label ?? 'Dashboard'
|
||||
return <pre>{error.message}</pre>
|
||||
}
|
||||
|
||||
export function Shell(): React.ReactElement {
|
||||
const location = useLocation()
|
||||
const { sidebarOpen, sidebarCollapsed, toggleSidebar, toggleSidebarCollapsed } =
|
||||
useUIStore()
|
||||
|
||||
const pageTitle = getPageTitle(location.pathname)
|
||||
|
||||
return (
|
||||
<div className={styles.shell}>
|
||||
<aside
|
||||
className={`${styles.sidebar} ${sidebarOpen ? styles.open : ''} ${sidebarCollapsed ? styles.collapsed : ''}`}
|
||||
>
|
||||
<div className={styles.sidebarHeader}>
|
||||
<span className={styles.logo}>NavBar Template</span>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.collapseBtn}
|
||||
onClick={toggleSidebarCollapsed}
|
||||
aria-label={sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
>
|
||||
{sidebarCollapsed ? <LuChevronRight /> : <LuChevronLeft />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className={styles.nav}>
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={({ isActive }) =>
|
||||
`${styles.navItem} ${isActive ? styles.active : ''}`
|
||||
}
|
||||
onClick={() => sidebarOpen && toggleSidebar()}
|
||||
>
|
||||
<item.icon className={styles.navIcon} />
|
||||
<span className={styles.navLabel}>{item.label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{sidebarOpen && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.overlay}
|
||||
onClick={toggleSidebar}
|
||||
onKeyDown={(e) => e.key === 'Escape' && toggleSidebar()}
|
||||
aria-label="Close sidebar"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`${styles.main} ${sidebarCollapsed ? styles.collapsed : ''}`}
|
||||
>
|
||||
<header className={styles.header}>
|
||||
<div className={styles.headerLeft}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.menuBtn}
|
||||
onClick={toggleSidebar}
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
<LuMenu />
|
||||
</button>
|
||||
<h1 className={styles.pageTitle}>{pageTitle}</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className={styles.content}>
|
||||
<ErrorBoundary FallbackComponent={ShellErrorFallback}>
|
||||
<Suspense fallback={<ShellLoading />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<ErrorBoundary FallbackComponent={ShellErrorFallback}>
|
||||
<Suspense fallback={null}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
// ===================
|
||||
// ©AngelaMos | 2026
|
||||
// toast.module.scss
|
||||
// ===================
|
||||
|
|
@ -2,5 +2,3 @@
|
|||
// ©AngelaMos | 2026
|
||||
// index.ts
|
||||
// ===================
|
||||
|
||||
export * from './shell.ui.store'
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
/**
|
||||
* ©AngelaMos | 2026
|
||||
* ui.store.ts
|
||||
*/
|
||||
|
||||
import { create } from 'zustand'
|
||||
import { devtools, persist } from 'zustand/middleware'
|
||||
|
||||
type Theme = 'light' | 'dark' | 'system'
|
||||
|
||||
interface UIState {
|
||||
theme: Theme
|
||||
sidebarOpen: boolean
|
||||
sidebarCollapsed: boolean
|
||||
setTheme: (theme: Theme) => void
|
||||
toggleSidebar: () => void
|
||||
setSidebarOpen: (open: boolean) => void
|
||||
toggleSidebarCollapsed: () => void
|
||||
}
|
||||
|
||||
export const useUIStore = create<UIState>()(
|
||||
devtools(
|
||||
persist(
|
||||
(set) => ({
|
||||
theme: 'dark',
|
||||
sidebarOpen: false,
|
||||
sidebarCollapsed: false,
|
||||
|
||||
setTheme: (theme) => set({ theme }, false, 'ui/setTheme'),
|
||||
|
||||
toggleSidebar: () =>
|
||||
set(
|
||||
(state) => ({ sidebarOpen: !state.sidebarOpen }),
|
||||
false,
|
||||
'ui/toggleSidebar'
|
||||
),
|
||||
|
||||
setSidebarOpen: (open) =>
|
||||
set({ sidebarOpen: open }, false, 'ui/setSidebarOpen'),
|
||||
|
||||
toggleSidebarCollapsed: () =>
|
||||
set(
|
||||
(state) => ({ sidebarCollapsed: !state.sidebarCollapsed }),
|
||||
false,
|
||||
'ui/toggleSidebarCollapsed'
|
||||
),
|
||||
}),
|
||||
{
|
||||
name: 'ui-storage',
|
||||
partialize: (state) => ({
|
||||
theme: state.theme,
|
||||
sidebarCollapsed: state.sidebarCollapsed,
|
||||
}),
|
||||
}
|
||||
),
|
||||
{ name: 'UIStore' }
|
||||
)
|
||||
)
|
||||
|
||||
export const useTheme = (): Theme => useUIStore((s) => s.theme)
|
||||
export const useSidebarOpen = (): boolean => useUIStore((s) => s.sidebarOpen)
|
||||
export const useSidebarCollapsed = (): boolean =>
|
||||
useUIStore((s) => s.sidebarCollapsed)
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
// ===================
|
||||
// ©AngelaMos | 2026
|
||||
// dashboard.module.scss
|
||||
// ===================
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
/**
|
||||
* ©AngelaMos | 2026
|
||||
* index.tsx
|
||||
*/
|
||||
|
||||
import styles from './dashboard.module.scss'
|
||||
|
||||
const AVAILABLE_STORES = [
|
||||
{
|
||||
name: 'useUIStore()',
|
||||
file: 'core/lib/shell.ui.store.ts',
|
||||
description: 'Theme, sidebar open/collapsed state',
|
||||
},
|
||||
]
|
||||
|
||||
const SUGGESTED_FEATURES = [
|
||||
'Stats and metrics',
|
||||
'Recent activity feed',
|
||||
'Quick actions',
|
||||
'Charts and analytics',
|
||||
'Notifications overview',
|
||||
'Task/project summary',
|
||||
]
|
||||
|
||||
export function Component(): React.ReactElement {
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<h1 className={styles.title}>Welcome</h1>
|
||||
<p className={styles.subtitle}>
|
||||
Template page — build your dashboard here
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Available Stores</h2>
|
||||
<div className={styles.grid}>
|
||||
{AVAILABLE_STORES.map((store) => (
|
||||
<div key={store.name} className={styles.card}>
|
||||
<code className={styles.hookName}>{store.name}</code>
|
||||
<p className={styles.description}>{store.description}</p>
|
||||
<span className={styles.file}>{store.file}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Suggested Features</h2>
|
||||
<ul className={styles.list}>
|
||||
{SUGGESTED_FEATURES.map((feature) => (
|
||||
<li key={feature}>{feature}</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Component.displayName = 'Dashboard'
|
||||
|
|
@ -3,10 +3,8 @@
|
|||
// index.tsx
|
||||
// ===================
|
||||
|
||||
import styles from './landing.module.scss'
|
||||
|
||||
export function Component(): React.ReactElement {
|
||||
return <div className={styles.page} />
|
||||
return <div />
|
||||
}
|
||||
|
||||
Component.displayName = 'Landing'
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
// ===================
|
||||
// ©AngelaMos | 2026
|
||||
// landing.module.scss
|
||||
// ===================
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
/**
|
||||
* ©AngelaMos | 2026
|
||||
* index.tsx
|
||||
*/
|
||||
|
||||
import styles from './settings.module.scss'
|
||||
|
||||
const AVAILABLE_STORES = [
|
||||
{
|
||||
name: 'useUIStore()',
|
||||
file: 'core/lib/shell.ui.store.ts',
|
||||
description: 'Theme, sidebar open/collapsed state',
|
||||
},
|
||||
]
|
||||
|
||||
export function Component(): React.ReactElement {
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<h1 className={styles.title}>Settings</h1>
|
||||
<p className={styles.subtitle}>
|
||||
Template page — available stores for building your settings UI
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Available Stores</h2>
|
||||
<div className={styles.grid}>
|
||||
{AVAILABLE_STORES.map((store) => (
|
||||
<div key={store.name} className={styles.card}>
|
||||
<code className={styles.hookName}>{store.name}</code>
|
||||
<p className={styles.description}>{store.description}</p>
|
||||
<div className={styles.meta}>
|
||||
<span className={styles.file}>{store.file}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Suggested Features</h2>
|
||||
<ul className={styles.list}>
|
||||
<li>Profile form</li>
|
||||
<li>Theme toggle (dark/light)</li>
|
||||
<li>Notification settings</li>
|
||||
<li>Application preferences</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Component.displayName = 'Settings'
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
// ===================
|
||||
// ©AngelaMos | 2026
|
||||
// settings.module.scss
|
||||
// ===================
|
||||
|
|
@ -12,12 +12,4 @@
|
|||
#root {
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,8 +38,6 @@ body {
|
|||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
overflow-x: hidden;
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
h1,
|
||||
|
|
|
|||
|
|
@ -71,44 +71,6 @@ $tracking-normal: 0;
|
|||
$tracking-wide: 0.025em;
|
||||
$tracking-wider: 0.05em;
|
||||
|
||||
// ============================================================================
|
||||
// COLORS
|
||||
// ============================================================================
|
||||
$white: hsl(0, 0%, 100%);
|
||||
$black: hsl(0, 0%, 0%);
|
||||
|
||||
// Auth
|
||||
$bg-page: hsl(0, 0%, 10.5%);
|
||||
$bg-card: hsl(0, 0%, 6.2%);
|
||||
|
||||
// Home/landing
|
||||
$bg-landing: hsl(0, 0%, 10.8%);
|
||||
|
||||
$bg-default: hsl(0, 0%, 7.1%);
|
||||
$bg-alternative: hsl(0, 0%, 5.9%);
|
||||
$bg-surface-75: hsl(0, 0%, 9%);
|
||||
$bg-surface-100: hsl(0, 0%, 12.2%);
|
||||
$bg-surface-200: hsl(0, 0%, 14.1%);
|
||||
$bg-surface-300: hsl(0, 0%, 16.1%);
|
||||
$bg-control: hsl(0, 0%, 10%);
|
||||
$bg-selection: hsl(0, 0%, 19.2%);
|
||||
$bg-overlay: hsl(0, 0%, 14.1%);
|
||||
$bg-overlay-hover: hsl(0, 0%, 18%);
|
||||
|
||||
$border-muted: hsl(0, 0%, 11.1%);
|
||||
$border-default: hsl(0, 0%, 18%);
|
||||
$border-strong: hsl(0, 0%, 22.4%);
|
||||
$border-stronger: hsl(0, 0%, 27.1%);
|
||||
$border-control: hsl(0, 0%, 22.4%);
|
||||
|
||||
$text-default: hsl(0, 0%, 98%);
|
||||
$text-light: hsl(0, 0%, 70.6%);
|
||||
$text-lighter: hsl(0, 0%, 53.7%);
|
||||
$text-muted: hsl(0, 0%, 30.2%);
|
||||
|
||||
$error-default: hsl(0, 72%, 51%);
|
||||
$error-light: hsl(0, 72%, 65%);
|
||||
|
||||
// ============================================================================
|
||||
// BORDER RADIUS
|
||||
// ============================================================================
|
||||
|
|
|
|||
Loading…
Reference in New Issue