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:
parent
a2d6f09ab3
commit
a4811d1c04
|
|
@ -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
|
||||
|
|
@ -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?
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 |
|
|
@ -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"}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// ===================
|
||||
// © AngelaMos | 2026
|
||||
// index.ts
|
||||
// ===================
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// ===================
|
||||
// © AngelaMos | 2025
|
||||
// index.ts
|
||||
// ===================
|
||||
|
||||
export * from './hooks'
|
||||
export * from './types'
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// ===================
|
||||
// © AngelaMos | 2026
|
||||
// index.ts
|
||||
// ===================
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* ©AngelaMos | 2025
|
||||
* index.tsx
|
||||
*/
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
// ===================
|
||||
// © AngelaMos | 2025
|
||||
// index.ts
|
||||
// ===================
|
||||
|
||||
export * from './api.config'
|
||||
export * from './errors'
|
||||
export * from './query.config'
|
||||
|
|
@ -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,
|
||||
}),
|
||||
})
|
||||
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// ===================
|
||||
// © AngelaMos | 2026
|
||||
// shell.module.scss
|
||||
// ===================
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// ===================
|
||||
// © AngelaMos | 2026
|
||||
// toast.module.scss
|
||||
// ===================
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// ===================
|
||||
// © AngelaMos | 2026
|
||||
// index.ts
|
||||
// ===================
|
||||
|
||||
export * from './shell.ui.store'
|
||||
|
|
@ -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)
|
||||
|
|
@ -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>
|
||||
)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// ===================
|
||||
// © AngelaMos | 2026
|
||||
// dashboard.module.scss
|
||||
// ===================
|
||||
|
|
@ -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'
|
||||
|
|
@ -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'
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// ===================
|
||||
// © AngelaMos | 2026
|
||||
// landing.module.scss
|
||||
// ===================
|
||||
|
|
@ -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'
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// ===================
|
||||
// © AngelaMos | 2026
|
||||
// settings.module.scss
|
||||
// ===================
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
// ===================
|
||||
// © AngelaMos | 2025
|
||||
// _index.scss
|
||||
// ===================
|
||||
|
||||
@forward 'tokens';
|
||||
@forward 'fonts';
|
||||
@forward 'mixins';
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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%;
|
||||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue