feat(canary): import operator's React + Vite frontend template

Frontend stack (pre-rebranded by operator before Phase 0):
- React 19 + react-router-dom v7
- TypeScript 5.9 with project references
- Vite 7 (rolldown-vite — Rust port)
- TanStack Query v5 + axios for data
- Zustand v5 for client state
- Zod v4 for runtime validation
- SCSS (Sass), not Tailwind — operator stylistic choice
- Biome 2 (lint + format), Stylelint for SCSS
- react-error-boundary, sonner (toasts)
- react-icons

Source layout (operator-authored skeleton):
- src/App.tsx, main.tsx, config.ts, styles.scss
- src/api/{hooks,types,index.ts}        TanStack Query hooks
- src/components/                        UI primitives (operator-designed later)
- src/core/{api,app,lib}                 axios client, routers, utilities
- src/pages/                             page-level containers
- src/styles/                            SCSS partials

Per implementation plan §16.3, Phase 14 stops at API client only —
operator owns visual/component design from there.

Pre-commit hooks fixed:
- site.webmanifest: missing trailing newline
- index.html: trailing whitespace stripped
- .stylelintignore + stylelint.config.js: chmod -x (no shebang, not executable)

node_modules/ is gitignored.
This commit is contained in:
CarterPerez-dev 2026-05-10 05:28:09 -04:00
parent a2d6f09ab3
commit a4811d1c04
48 changed files with 4281 additions and 0 deletions

View File

@ -0,0 +1,15 @@
node_modules
build
dist
.git
.gitignore
*.md
.env*
.vscode
.idea
*.log
npm-debug.log*
pnpm-debug.log*
.DS_Store
coverage
.nyc_output

View File

@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
.vite
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1,22 @@
# ©AngelaMos | 2025
# .stylelintignore
# Dependencies
node_modules/
# Production builds
dist/
build/
out/
# JS/TS files
**/*.js
**/*.jsx
**/*.ts
**/*.tsx
# Generated files
*.min.css
# Error system styles - ignore from linting
src/core/app/_toastStyles.scss

View File

@ -0,0 +1,94 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.json"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 82,
"lineEnding": "lf"
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"jsxQuoteStyle": "double",
"semicolons": "asNeeded",
"trailingCommas": "es5",
"arrowParentheses": "always"
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"complexity": {
"noExcessiveCognitiveComplexity": {
"level": "error",
"options": { "maxAllowedComplexity": 25 }
},
"noForEach": "off",
"useLiteralKeys": "off"
},
"correctness": {
"noUnusedVariables": "error",
"noUnusedImports": "error",
"useExhaustiveDependencies": "warn",
"useHookAtTopLevel": "error",
"noUndeclaredVariables": "error"
},
"style": {
"useImportType": "error",
"useConst": "error",
"useTemplate": "error",
"useSelfClosingElements": "error",
"useFragmentSyntax": "error",
"noNonNullAssertion": "error",
"useConsistentArrayType": {
"level": "error",
"options": { "syntax": "shorthand" }
},
"useNamingConvention": "off"
},
"suspicious": {
"noExplicitAny": "error",
"noDebugger": "error",
"noConsole": "warn",
"noArrayIndexKey": "warn",
"noAssignInExpressions": "error",
"noDoubleEquals": "error",
"noRedeclare": "error",
"noVar": "error"
},
"security": {
"noDangerouslySetInnerHtml": "error"
},
"a11y": {
"useAltText": "error",
"useAnchorContent": "error",
"useKeyWithClickEvents": "error",
"noStaticElementInteractions": "error",
"useButtonType": "error",
"useValidAnchor": "error"
}
}
},
"overrides": [
{
"includes": ["src/main.tsx"],
"linter": {
"rules": {
"style": {
"noNonNullAssertion": "off"
}
}
}
}
]
}

View File

@ -0,0 +1,44 @@
<!doctype html>
<html lang="en">
<head>
<!--
Canary Token Generator
Author(s): © AngelaMos, CarterPerez-dev
-->
<meta charset="UTF-8" />
<link
rel="icon"
type="image/x-icon"
href="/asset/favicon.ico"
/>
<link
rel="apple-touch-icon"
type="image/png"
href="/asset/apple-touch-icon.png"
/>
<link
rel="manifest"
href="/asset/site.webmanifest"
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Canary Token Generator</title>
<meta
name="description"
content="*Cracked*"
/>
<meta
name="author"
content=" ©AngelaMos | 2026 | CarterPerez-dev"
/>
</head>
<body>
<div id="root"></div>
<script
type="module"
src="/src/main.tsx"
></script>
</body>
</html>

View File

@ -0,0 +1,50 @@
{
"name": "canary-token-generator",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"lint": "biome check .",
"lint:fix": "biome check --write .",
"format": "biome format --write .",
"typecheck": "tsc -b",
"lint:scss": "stylelint '**/*.scss'",
"lint:scss:fix": "stylelint '**/*.scss' --fix"
},
"dependencies": {
"@tanstack/react-query": "^5.90.12",
"axios": "^1.13.0",
"react": "^19.2.1",
"react-dom": "^19.2.0",
"react-error-boundary": "^6.0.0",
"react-icon": "^1.0.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.1.1",
"sonner": "^2.0.7",
"zod": "^4.1.13",
"zustand": "^5.0.9"
},
"devDependencies": {
"@biomejs/biome": "^2.3.8",
"@tanstack/react-query-devtools": "^5.91.1",
"@types/node": "^24.10.2",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"sass": "^1.95.0",
"stylelint": "^16.26.1",
"stylelint-config-prettier-scss": "^1.0.0",
"stylelint-config-standard-scss": "^16.0.0",
"typescript": "~5.9.3",
"vite": "npm:rolldown-vite@7.2.5",
"vite-tsconfig-paths": "^5.1.0"
},
"pnpm": {
"overrides": {
"vite": "npm:rolldown-vite@7.2.5"
}
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 823 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@ -0,0 +1,36 @@
// ===========================
// ©AngelaMos | 2026
// App.tsx
// ===========================
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>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}

View File

@ -0,0 +1,4 @@
// ===================
// © AngelaMos | 2026
// index.ts
// ===================

View File

@ -0,0 +1,7 @@
// ===================
// © AngelaMos | 2025
// index.ts
// ===================
export * from './hooks'
export * from './types'

View File

@ -0,0 +1,4 @@
// ===================
// © AngelaMos | 2026
// index.ts
// ===================

View File

@ -0,0 +1,4 @@
/**
* ©AngelaMos | 2025
* index.tsx
*/

View File

@ -0,0 +1,68 @@
// ===================
// © 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 const QUERY_CONFIG = {
STALE_TIME: {
USER: 1000 * 60 * 5,
STATIC: Infinity,
FREQUENT: 1000 * 30,
},
GC_TIME: {
DEFAULT: 1000 * 60 * 30,
LONG: 1000 * 60 * 60,
},
RETRY: {
DEFAULT: 3,
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

View File

@ -0,0 +1,17 @@
// ===================
// © AngelaMos | 2026
// api.config.ts
// ===================
import axios, { type AxiosInstance } from 'axios'
const getBaseURL = (): string => {
return import.meta.env.VITE_API_URL ?? '/api'
}
export const apiClient: AxiosInstance = axios.create({
baseURL: getBaseURL(),
timeout: 15000,
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
})

View File

@ -0,0 +1,114 @@
/**
* ©AngelaMos | 2025
* errors.ts
*/
import type { AxiosError } from 'axios'
export const ApiErrorCode = {
NETWORK_ERROR: 'NETWORK_ERROR',
VALIDATION_ERROR: 'VALIDATION_ERROR',
AUTHENTICATION_ERROR: 'AUTHENTICATION_ERROR',
AUTHORIZATION_ERROR: 'AUTHORIZATION_ERROR',
NOT_FOUND: 'NOT_FOUND',
CONFLICT: 'CONFLICT',
RATE_LIMITED: 'RATE_LIMITED',
SERVER_ERROR: 'SERVER_ERROR',
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
} as const
export type ApiErrorCode = (typeof ApiErrorCode)[keyof typeof ApiErrorCode]
export class ApiError extends Error {
readonly code: ApiErrorCode
readonly statusCode: number
readonly details?: Record<string, string[]>
constructor(
message: string,
code: ApiErrorCode,
statusCode: number,
details?: Record<string, string[]>
) {
super(message)
this.name = 'ApiError'
this.code = code
this.statusCode = statusCode
this.details = details
}
getUserMessage(): string {
const messages: Record<ApiErrorCode, string> = {
[ApiErrorCode.NETWORK_ERROR]:
'Unable to connect. Please check your internet connection.',
[ApiErrorCode.VALIDATION_ERROR]: 'Please check your input and try again.',
[ApiErrorCode.AUTHENTICATION_ERROR]:
'Your session has expired. Please log in again.',
[ApiErrorCode.AUTHORIZATION_ERROR]:
'You do not have permission to perform this action.',
[ApiErrorCode.NOT_FOUND]: 'The requested resource was not found.',
[ApiErrorCode.CONFLICT]:
'This operation conflicts with an existing resource.',
[ApiErrorCode.RATE_LIMITED]:
'Too many requests. Please wait a moment and try again.',
[ApiErrorCode.SERVER_ERROR]:
'Something went wrong on our end. Please try again later.',
[ApiErrorCode.UNKNOWN_ERROR]:
'An unexpected error occurred. Please try again.',
}
return messages[this.code]
}
}
interface ApiErrorResponse {
detail?: string | { msg: string; type: string }[]
message?: string
}
export function transformAxiosError(error: AxiosError<unknown>): ApiError {
if (!error.response) {
return new ApiError('Network error', ApiErrorCode.NETWORK_ERROR, 0)
}
const { status } = error.response
const data = error.response.data as ApiErrorResponse | undefined
let message = 'An error occurred'
let details: Record<string, string[]> | undefined
if (data?.detail) {
if (typeof data.detail === 'string') {
message = data.detail
} else if (Array.isArray(data.detail)) {
details = { validation: [] }
data.detail.forEach((err) => {
details?.validation.push(err.msg)
})
message = 'Validation error'
}
} else if (data?.message) {
message = data.message
}
const codeMap: Record<number, ApiErrorCode> = {
400: ApiErrorCode.VALIDATION_ERROR,
401: ApiErrorCode.AUTHENTICATION_ERROR,
403: ApiErrorCode.AUTHORIZATION_ERROR,
404: ApiErrorCode.NOT_FOUND,
409: ApiErrorCode.CONFLICT,
429: ApiErrorCode.RATE_LIMITED,
500: ApiErrorCode.SERVER_ERROR,
502: ApiErrorCode.SERVER_ERROR,
503: ApiErrorCode.SERVER_ERROR,
504: ApiErrorCode.SERVER_ERROR,
}
const code = codeMap[status] || ApiErrorCode.UNKNOWN_ERROR
return new ApiError(message, code, status, details)
}
declare module '@tanstack/react-query' {
interface Register {
defaultError: ApiError
}
}

View File

@ -0,0 +1,8 @@
// ===================
// © AngelaMos | 2025
// index.ts
// ===================
export * from './api.config'
export * from './errors'
export * from './query.config'

View File

@ -0,0 +1,105 @@
// ===================
// © AngelaMos | 2025
// query.config.ts
// ===================
import { MutationCache, QueryCache, QueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { QUERY_CONFIG } from '@/config'
import { ApiError, ApiErrorCode } from './errors'
const NO_RETRY_ERROR_CODES: readonly ApiErrorCode[] = [
ApiErrorCode.AUTHENTICATION_ERROR,
ApiErrorCode.AUTHORIZATION_ERROR,
ApiErrorCode.NOT_FOUND,
ApiErrorCode.VALIDATION_ERROR,
] as const
const shouldRetryQuery = (failureCount: number, error: Error): boolean => {
if (error instanceof ApiError) {
if (NO_RETRY_ERROR_CODES.includes(error.code)) {
return false
}
}
return failureCount < QUERY_CONFIG.RETRY.DEFAULT
}
const calculateRetryDelay = (attemptIndex: number): number => {
const baseDelay = 1000
const maxDelay = 30000
return Math.min(baseDelay * 2 ** attemptIndex, maxDelay)
}
const handleQueryCacheError = (
error: Error,
query: { state: { data: unknown } }
): void => {
if (query.state.data !== undefined) {
const message =
error instanceof ApiError
? error.getUserMessage()
: 'Background update failed'
toast.error(message)
}
}
const handleMutationCacheError = (
error: Error,
_variables: unknown,
_context: unknown,
mutation: { options: { onError?: unknown } }
): void => {
if (mutation.options.onError === undefined) {
const message =
error instanceof ApiError ? error.getUserMessage() : 'Operation failed'
toast.error(message)
}
}
export const QUERY_STRATEGIES = {
standard: {
staleTime: QUERY_CONFIG.STALE_TIME.USER,
gcTime: QUERY_CONFIG.GC_TIME.DEFAULT,
},
frequent: {
staleTime: QUERY_CONFIG.STALE_TIME.FREQUENT,
gcTime: QUERY_CONFIG.GC_TIME.DEFAULT,
refetchInterval: QUERY_CONFIG.STALE_TIME.FREQUENT,
},
static: {
staleTime: QUERY_CONFIG.STALE_TIME.STATIC,
gcTime: QUERY_CONFIG.GC_TIME.LONG,
refetchOnMount: false,
refetchOnWindowFocus: false,
},
auth: {
staleTime: QUERY_CONFIG.STALE_TIME.USER,
gcTime: QUERY_CONFIG.GC_TIME.DEFAULT,
retry: QUERY_CONFIG.RETRY.NONE,
},
} as const
export type QueryStrategy = keyof typeof QUERY_STRATEGIES
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: QUERY_CONFIG.STALE_TIME.USER,
gcTime: QUERY_CONFIG.GC_TIME.DEFAULT,
retry: shouldRetryQuery,
retryDelay: calculateRetryDelay,
refetchOnWindowFocus: true,
refetchOnMount: true,
refetchOnReconnect: true,
},
mutations: {
retry: QUERY_CONFIG.RETRY.NONE,
},
},
queryCache: new QueryCache({
onError: handleQueryCacheError,
}),
mutationCache: new MutationCache({
onError: handleMutationCacheError,
}),
})

View File

@ -0,0 +1,34 @@
// ===================
// © AngelaMos | 2026
// routers.tsx
// ===================
import { createBrowserRouter, type RouteObject } from 'react-router-dom'
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.SETTINGS,
lazy: () => import('@/pages/settings'),
},
],
},
{
path: '*',
lazy: () => import('@/pages/landing'),
},
]
export const router = createBrowserRouter(routes)

View File

@ -0,0 +1,4 @@
// ===================
// © AngelaMos | 2026
// shell.module.scss
// ===================

View File

@ -0,0 +1,116 @@
/**
* ©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 },
]
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'
}
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>
)
}

View File

@ -0,0 +1,4 @@
// ===================
// © AngelaMos | 2026
// toast.module.scss
// ===================

View File

@ -0,0 +1,6 @@
// ===================
// © AngelaMos | 2026
// index.ts
// ===================
export * from './shell.ui.store'

View File

@ -0,0 +1,63 @@
/**
* ©AngelaMos | 2025
* 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)

View File

@ -0,0 +1,15 @@
// ===========================
// ©AngelaMos | 2025
// main.tsx
// ===========================
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
import './styles.scss'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
)

View File

@ -0,0 +1,4 @@
// ===================
// © AngelaMos | 2026
// dashboard.module.scss
// ===================

View File

@ -0,0 +1,62 @@
/**
* ©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'

View File

@ -0,0 +1,12 @@
// ===================
// © AngelaMos | 2026
// index.tsx
// ===================
import styles from './landing.module.scss'
export function Component(): React.ReactElement {
return <div className={styles.page} />
}
Component.displayName = 'Landing'

View File

@ -0,0 +1,4 @@
// ===================
// © AngelaMos | 2026
// landing.module.scss
// ===================

View File

@ -0,0 +1,56 @@
/**
* ©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'

View File

@ -0,0 +1,4 @@
// ===================
// © AngelaMos | 2026
// settings.module.scss
// ===================

View File

@ -0,0 +1,23 @@
// ===================
// © AngelaMos | 2026
// styles.scss
// ===================
@forward 'styles/tokens';
@forward 'styles/fonts';
@forward 'styles/mixins';
@use 'styles/reset';
#root {
min-height: 100vh;
min-height: 100dvh;
display: flex;
flex-direction: column;
}
.app {
flex: 1;
display: flex;
flex-direction: column;
}

View File

@ -0,0 +1,12 @@
// ===================
// © AngelaMos | 2025
// _fonts.scss
// ===================
@use 'tokens' as *;
$font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto,
'Helvetica Neue', Arial, sans-serif;
$font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas,
'Liberation Mono', monospace;

View File

@ -0,0 +1,8 @@
// ===================
// © AngelaMos | 2025
// _index.scss
// ===================
@forward 'tokens';
@forward 'fonts';
@forward 'mixins';

View File

@ -0,0 +1,120 @@
// ===================
// © AngelaMos | 2025
// _mixins.scss
// ===================
@use 'sass:map';
@use 'sass:list';
@use 'tokens' as *;
$breakpoints: (
'xs': $breakpoint-xs,
'sm': $breakpoint-sm,
'md': $breakpoint-md,
'lg': $breakpoint-lg,
'xl': $breakpoint-xl,
'2xl': $breakpoint-2xl,
);
@mixin breakpoint-up($size) {
@media (min-width: map.get($breakpoints, $size)) {
@content;
}
}
@mixin breakpoint-down($size) {
@media (width < map.get($breakpoints, $size)) {
@content;
}
}
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
@mixin flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
@mixin flex-column {
display: flex;
flex-direction: column;
}
@mixin flex-column-center {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
@mixin sr-only {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
@mixin truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@mixin line-clamp($lines: 2) {
display: -webkit-box;
-webkit-line-clamp: #{$lines};
-webkit-box-orient: vertical;
overflow: hidden;
}
@mixin transition-fast {
transition-property: background-color, border-color, color, opacity;
transition-duration: $duration-fast;
transition-timing-function: $ease-out;
}
@mixin transition-normal {
transition-property: background-color, border-color, color, opacity;
transition-duration: $duration-normal;
transition-timing-function: $ease-out;
}
@mixin absolute-fill {
position: absolute;
inset: 0;
}
@mixin absolute-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
@mixin hover {
@media (hover: hover) and (pointer: fine) {
&:hover {
@content;
}
}
}
@mixin hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}

View File

@ -0,0 +1,105 @@
// ===================
// © AngelaMos | 2026
// _reset.scss
// ===================
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
padding: 0;
-webkit-tap-highlight-color: transparent;
}
html {
font-size: 16px;
-moz-text-size-adjust: none;
-webkit-text-size-adjust: none;
text-size-adjust: none;
overflow-x: hidden;
}
@media (prefers-reduced-motion: no-preference) {
html {
interpolate-size: allow-keywords;
scroll-behavior: smooth;
}
}
body {
min-height: 100vh;
min-height: 100dvh;
line-height: 1.5;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
background-color: #fff;
color: #000;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.2;
text-wrap: balance;
overflow-wrap: break-word;
}
p {
text-wrap: pretty;
overflow-wrap: break-word;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
height: auto;
}
input,
button,
textarea,
select {
font: inherit;
color: inherit;
}
button {
background: none;
border: none;
cursor: pointer;
text-align: inherit;
font-family: inherit;
}
[hidden] {
display: none !important;
}
[disabled] {
cursor: not-allowed;
opacity: 0.5;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

View File

@ -0,0 +1,168 @@
// ===================
// © AngelaMos | 2025
// _tokens.scss
// ===================
// ============================================================================
// SPACING (8px base system)
// ============================================================================
$space-0: 0;
$space-px: 1px;
$space-0-5: 0.125rem;
$space-1: 0.25rem;
$space-1-5: 0.375rem;
$space-2: 0.5rem;
$space-2-5: 0.625rem;
$space-3: 0.75rem;
$space-3-5: 0.875rem;
$space-4: 1rem;
$space-5: 1.25rem;
$space-6: 1.5rem;
$space-7: 1.75rem;
$space-8: 2rem;
$space-9: 2.25rem;
$space-10: 2.5rem;
$space-11: 2.75rem;
$space-12: 3rem;
$space-14: 3.5rem;
$space-16: 4rem;
$space-20: 5rem;
$space-24: 6rem;
$space-28: 7rem;
$space-32: 8rem;
// ============================================================================
// TYPOGRAPHY SCALE
// ============================================================================
$font-size-3xs: 0.625rem;
$font-size-2xs: 0.6875rem;
$font-size-xs: 0.75rem;
$font-size-sm: 0.875rem;
$font-size-base: 1rem;
$font-size-lg: 1.125rem;
$font-size-xl: 1.25rem;
$font-size-2xl: 1.5rem;
$font-size-3xl: 1.875rem;
$font-size-4xl: 2.25rem;
$font-size-5xl: 3rem;
// ============================================================================
// FONT WEIGHTS
// ============================================================================
$font-weight-regular: 400;
$font-weight-medium: 500;
$font-weight-semibold: 600;
// ============================================================================
// LINE HEIGHTS
// ============================================================================
$line-height-none: 1;
$line-height-tight: 1.2;
$line-height-snug: 1.375;
$line-height-normal: 1.5;
$line-height-relaxed: 1.625;
// ============================================================================
// LETTER SPACING
// ============================================================================
$tracking-tighter: -0.05em;
$tracking-tight: -0.025em;
$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
// ============================================================================
$radius-none: 0;
$radius-xs: 2px;
$radius-sm: 4px;
$radius-md: 6px;
$radius-lg: 8px;
$radius-xl: 12px;
$radius-full: 9999px;
// ============================================================================
// Z-INDEX SCALE
// ============================================================================
$z-hide: -1;
$z-base: 0;
$z-dropdown: 100;
$z-sticky: 200;
$z-fixed: 300;
$z-overlay: 400;
$z-modal: 500;
$z-popover: 600;
$z-tooltip: 700;
$z-toast: 800;
$z-max: 9999;
// ============================================================================
// TRANSITIONS
// ============================================================================
$duration-instant: 0ms;
$duration-fast: 100ms;
$duration-normal: 150ms;
$duration-slow: 200ms;
$ease-out: cubic-bezier(0, 0, 0.2, 1);
$ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
// ============================================================================
// BREAKPOINTS
// ============================================================================
$breakpoint-xs: 360px;
$breakpoint-sm: 480px;
$breakpoint-md: 768px;
$breakpoint-lg: 1024px;
$breakpoint-xl: 1280px;
$breakpoint-2xl: 1536px;
// ============================================================================
// CONTAINER WIDTHS
// ============================================================================
$container-xs: 20rem;
$container-sm: 24rem;
$container-md: 28rem;
$container-lg: 32rem;
$container-xl: 36rem;
$container-2xl: 42rem;
$container-full: 100%;

View File

@ -0,0 +1,107 @@
// ©AngelaMos | 2025
// stylelint.config.js
/** @type {import('stylelint').Config} */
export default {
extends: ['stylelint-config-standard-scss', 'stylelint-config-prettier-scss'],
rules: {
'block-no-empty': true,
'declaration-no-important': true,
'color-no-invalid-hex': true,
'property-no-unknown': true,
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['global'],
},
],
'selector-class-pattern': [
'^[a-z]([a-z0-9-]+)?(__[a-z0-9]([a-z0-9-]+)?)?(--[a-z0-9]([a-z0-9-]+)?)?$|^[a-z][a-zA-Z0-9]*$',
{
message:
'Selector should be in BEM format (e.g., .block__element--modifier) or CSS Modules camelCase (e.g., .testButton)',
},
],
'value-keyword-case': [
'lower',
{
camelCaseSvgKeywords: true,
ignoreKeywords: [
'BlinkMacSystemFont',
'SFMono-Regular',
'Menlo',
'Monaco',
'Consolas',
'Roboto',
'Arial',
'Helvetica',
'Times',
'Georgia',
'Verdana',
'Tahoma',
'Trebuchet',
'Impact',
'Comic',
],
},
],
'property-no-vendor-prefix': [
true,
{
ignoreProperties: ['text-size-adjust', 'appearance', 'backdrop-filter'],
},
],
'value-no-vendor-prefix': true,
'selector-no-vendor-prefix': true,
'property-no-deprecated': [
true,
{
ignoreProperties: ['clip'],
},
],
'container-name-pattern': null,
'layer-name-pattern': null,
'scss/at-rule-no-unknown': true,
'scss/declaration-nested-properties-no-divided-groups': true,
'scss/dollar-variable-no-missing-interpolation': true,
'scss/dollar-variable-empty-line-before': null,
'declaration-empty-line-before': null,
'custom-property-empty-line-before': null,
'no-descending-specificity': null,
'media-feature-name-no-unknown': [
true,
{
ignoreMediaFeatureNames: ['map'],
},
],
'color-function-notation': null,
'hue-degree-notation': null,
},
ignoreFiles: [
'node_modules/**',
'dist/**',
'build/**',
'**/*.js',
'**/*.ts',
'**/*.tsx',
],
overrides: [
{
files: ['**/styles/_reset.scss', '**/styles/_fonts.scss'],
rules: {
'declaration-no-important': null,
'scss/comment-no-empty': null,
},
},
],
}

View File

@ -0,0 +1,31 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}

View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,71 @@
/**
* ©AngelaMos | 2025
* vite.config.ts
*/
import path from 'node:path'
import react from '@vitejs/plugin-react'
import { defineConfig, loadEnv } from 'vite'
import tsconfigPaths from 'vite-tsconfig-paths'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, path.resolve(__dirname, '..'), '')
const isDev = mode === 'development'
return {
plugins: [react(), tsconfigPaths()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
css: {
preprocessorOptions: {
scss: {},
},
},
server: {
port: 5173,
host: '0.0.0.0',
proxy: {
'/api': {
target: env.VITE_API_TARGET || 'http://localhost:8000',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api/, ''),
},
},
},
build: {
target: 'esnext',
cssTarget: 'chrome100',
sourcemap: isDev ? true : 'hidden',
minify: 'oxc',
rollupOptions: {
output: {
manualChunks(id: string): string | undefined {
if (id.includes('node_modules')) {
if (id.includes('react-dom') || id.includes('react-router')) {
return 'vendor-react'
}
if (id.includes('@tanstack/react-query')) {
return 'vendor-query'
}
if (id.includes('zustand')) {
return 'vendor-state'
}
}
return undefined
},
},
},
},
preview: {
port: 4173,
},
}
})