create-freakpunch 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -3
- package/bin/create-freakpunch.mjs +227 -0
- package/files/README.md +19 -0
- package/files/app.config.ts +50 -0
- package/files/locales.ts +10 -0
- package/files/registry.ts +17 -0
- package/package.json +23 -6
- package/template/.dockerignore +17 -0
- package/template/.oxlintrc.json +9 -0
- package/template/AGENTS.md +6 -0
- package/template/BACKEND.md +108 -0
- package/template/CLAUDE.md +297 -0
- package/template/Dockerfile +19 -0
- package/template/_gitignore +26 -0
- package/template/components.json +27 -0
- package/template/design/COMPONENTS.md +145 -0
- package/template/design/DESIGN.md +212 -0
- package/template/design/PATTERNS.md +110 -0
- package/template/index.html +25 -0
- package/template/nginx.conf +32 -0
- package/template/package-lock.json +6484 -0
- package/template/package.json +51 -0
- package/template/public/favicon.svg +1 -0
- package/template/public/robots.txt +2 -0
- package/template/src/app.config.ts +50 -0
- package/template/src/app.roles.ts +24 -0
- package/template/src/components/ui/alert-dialog.tsx +181 -0
- package/template/src/components/ui/avatar.tsx +116 -0
- package/template/src/components/ui/badge.tsx +57 -0
- package/template/src/components/ui/breadcrumb.tsx +130 -0
- package/template/src/components/ui/button.tsx +58 -0
- package/template/src/components/ui/calendar.tsx +277 -0
- package/template/src/components/ui/card.tsx +99 -0
- package/template/src/components/ui/chart.tsx +370 -0
- package/template/src/components/ui/checkbox.tsx +28 -0
- package/template/src/components/ui/combobox.tsx +329 -0
- package/template/src/components/ui/command.tsx +191 -0
- package/template/src/components/ui/dialog.tsx +162 -0
- package/template/src/components/ui/dropdown-menu.tsx +271 -0
- package/template/src/components/ui/empty.tsx +103 -0
- package/template/src/components/ui/field.tsx +236 -0
- package/template/src/components/ui/input-group.tsx +263 -0
- package/template/src/components/ui/input.tsx +45 -0
- package/template/src/components/ui/kbd.tsx +31 -0
- package/template/src/components/ui/label.tsx +17 -0
- package/template/src/components/ui/pagination.tsx +134 -0
- package/template/src/components/ui/popover.tsx +87 -0
- package/template/src/components/ui/radio-group.tsx +35 -0
- package/template/src/components/ui/select.tsx +204 -0
- package/template/src/components/ui/separator.tsx +25 -0
- package/template/src/components/ui/sheet.tsx +139 -0
- package/template/src/components/ui/sidebar.tsx +741 -0
- package/template/src/components/ui/skeleton.tsx +13 -0
- package/template/src/components/ui/sonner.tsx +78 -0
- package/template/src/components/ui/spinner.tsx +42 -0
- package/template/src/components/ui/switch.tsx +29 -0
- package/template/src/components/ui/table.tsx +121 -0
- package/template/src/components/ui/tabs.tsx +191 -0
- package/template/src/components/ui/textarea.tsx +17 -0
- package/template/src/components/ui/toggle-group.tsx +94 -0
- package/template/src/components/ui/toggle.tsx +48 -0
- package/template/src/components/ui/tooltip.tsx +91 -0
- package/template/src/core/api/demo.ts +8 -0
- package/template/src/core/api/http.ts +76 -0
- package/template/src/core/api/mock.ts +88 -0
- package/template/src/core/api/rest.ts +67 -0
- package/template/src/core/api/types.ts +21 -0
- package/template/src/core/audit/audit.service.ts +94 -0
- package/template/src/core/audit/hooks.ts +19 -0
- package/template/src/core/auth/auth-store.ts +52 -0
- package/template/src/core/auth/auth.service.ts +86 -0
- package/template/src/core/auth/can.tsx +39 -0
- package/template/src/core/auth/dev-role-switcher.tsx +64 -0
- package/template/src/core/auth/local-credentials.ts +46 -0
- package/template/src/core/auth/permissions.ts +115 -0
- package/template/src/core/auth/require-auth.tsx +16 -0
- package/template/src/core/auth/require-permission.tsx +24 -0
- package/template/src/core/auth/roles.ts +9 -0
- package/template/src/core/auth/types.ts +20 -0
- package/template/src/core/auth/use-auth.ts +19 -0
- package/template/src/core/company/company.service.ts +94 -0
- package/template/src/core/company/hooks.ts +40 -0
- package/template/src/core/company/password-policy.ts +40 -0
- package/template/src/core/company/types.ts +46 -0
- package/template/src/core/company/use-app-name.ts +7 -0
- package/template/src/core/components/button-link.tsx +14 -0
- package/template/src/core/components/confirm-dialog.tsx +116 -0
- package/template/src/core/components/data-table/data-table.tsx +297 -0
- package/template/src/core/components/data-table/features.ts +24 -0
- package/template/src/core/components/data-table/table-header-cell.tsx +13 -0
- package/template/src/core/components/data-table/toolbar.tsx +100 -0
- package/template/src/core/components/data-table/use-list-url-state.ts +108 -0
- package/template/src/core/components/date-picker.tsx +71 -0
- package/template/src/core/components/file-attachments.tsx +84 -0
- package/template/src/core/components/form-error.tsx +9 -0
- package/template/src/core/components/form-page.tsx +93 -0
- package/template/src/core/components/image-upload.tsx +86 -0
- package/template/src/core/components/page.tsx +75 -0
- package/template/src/core/components/phone-input.tsx +58 -0
- package/template/src/core/components/search-select.tsx +57 -0
- package/template/src/core/components/segmented.tsx +41 -0
- package/template/src/core/components/states.tsx +97 -0
- package/template/src/core/components/status-badge.tsx +26 -0
- package/template/src/core/components/user-avatar.tsx +23 -0
- package/template/src/core/config/runtime.ts +16 -0
- package/template/src/core/config/types.ts +37 -0
- package/template/src/core/i18n/i18n.ts +69 -0
- package/template/src/core/i18n/i18next.d.ts +12 -0
- package/template/src/core/i18n/keys.ts +37 -0
- package/template/src/core/i18n/language-switcher.tsx +45 -0
- package/template/src/core/i18n/types.ts +17 -0
- package/template/src/core/i18n/use-format.ts +31 -0
- package/template/src/core/layout/app-shell.tsx +50 -0
- package/template/src/core/layout/app-sidebar.tsx +285 -0
- package/template/src/core/layout/command-menu.tsx +117 -0
- package/template/src/core/layout/top-bar.tsx +100 -0
- package/template/src/core/lib/csv.ts +27 -0
- package/template/src/core/lib/format.ts +87 -0
- package/template/src/core/lib/phone.ts +50 -0
- package/template/src/core/lib/storage.ts +27 -0
- package/template/src/core/modules/lazy-page.ts +9 -0
- package/template/src/core/modules/navigation.ts +39 -0
- package/template/src/core/modules/types.ts +55 -0
- package/template/src/core/notifications/hooks.ts +84 -0
- package/template/src/core/notifications/notification-center.tsx +251 -0
- package/template/src/core/notifications/notifications.service.ts +132 -0
- package/template/src/core/notifications/types.ts +70 -0
- package/template/src/core/preferences/preferences-store.ts +70 -0
- package/template/src/core/setup/setup-gate.tsx +20 -0
- package/template/src/core/setup/setup.service.ts +101 -0
- package/template/src/core/theme/brand-context.tsx +53 -0
- package/template/src/core/theme/brand.ts +29 -0
- package/template/src/core/theme/theme-context.tsx +62 -0
- package/template/src/core/theme/theme-options.ts +8 -0
- package/template/src/core/theme/theme-toggle.tsx +45 -0
- package/template/src/core/users/hooks.ts +51 -0
- package/template/src/core/users/types.ts +37 -0
- package/template/src/core/users/users.service.ts +167 -0
- package/template/src/hooks/use-mobile.ts +19 -0
- package/template/src/index.css +321 -0
- package/template/src/lib/utils.ts +1 -0
- package/template/src/locales/en/account.ts +180 -0
- package/template/src/locales/en/auth.ts +18 -0
- package/template/src/locales/en/common.ts +151 -0
- package/template/src/locales/en/company.ts +380 -0
- package/template/src/locales/en/dashboard.ts +46 -0
- package/template/src/locales/en/errors.ts +18 -0
- package/template/src/locales/en/notifications.ts +43 -0
- package/template/src/locales/en/settings.ts +25 -0
- package/template/src/locales/en/setup.ts +45 -0
- package/template/src/locales/index.ts +58 -0
- package/template/src/locales/tr/account.ts +184 -0
- package/template/src/locales/tr/auth.ts +22 -0
- package/template/src/locales/tr/common.ts +154 -0
- package/template/src/locales/tr/company.ts +384 -0
- package/template/src/locales/tr/dashboard.ts +50 -0
- package/template/src/locales/tr/errors.ts +22 -0
- package/template/src/locales/tr/notifications.ts +47 -0
- package/template/src/locales/tr/settings.ts +29 -0
- package/template/src/locales/tr/setup.ts +49 -0
- package/template/src/main.tsx +16 -0
- package/template/src/modules/locales.ts +10 -0
- package/template/src/modules/registry.ts +17 -0
- package/template/src/pages/dashboard/dashboard-page.tsx +55 -0
- package/template/src/pages/dashboard/quick-actions.tsx +37 -0
- package/template/src/pages/login-page.tsx +152 -0
- package/template/src/pages/no-access-page.tsx +11 -0
- package/template/src/pages/not-found-page.tsx +61 -0
- package/template/src/pages/settings/account/account.service.ts +128 -0
- package/template/src/pages/settings/account/components/mock-qr.tsx +50 -0
- package/template/src/pages/settings/account/components/password-strength.tsx +38 -0
- package/template/src/pages/settings/account/components/two-factor-dialog.tsx +140 -0
- package/template/src/pages/settings/account/hooks.ts +73 -0
- package/template/src/pages/settings/account/notifications-page.tsx +103 -0
- package/template/src/pages/settings/account/preferences-page.tsx +157 -0
- package/template/src/pages/settings/account/profile-page.tsx +242 -0
- package/template/src/pages/settings/account/security-page.tsx +364 -0
- package/template/src/pages/settings/account/types.ts +37 -0
- package/template/src/pages/settings/company/audit-log-page.tsx +126 -0
- package/template/src/pages/settings/company/branding-page.tsx +278 -0
- package/template/src/pages/settings/company/components/invite-dialog.tsx +106 -0
- package/template/src/pages/settings/company/components/read-only-notice.tsx +13 -0
- package/template/src/pages/settings/company/danger-zone-page.tsx +127 -0
- package/template/src/pages/settings/company/general-page.tsx +234 -0
- package/template/src/pages/settings/company/integrations/api-keys-panel.tsx +170 -0
- package/template/src/pages/settings/company/integrations/hooks.ts +37 -0
- package/template/src/pages/settings/company/integrations/integrations-page.tsx +57 -0
- package/template/src/pages/settings/company/integrations/integrations.service.ts +153 -0
- package/template/src/pages/settings/company/integrations/settings-forms.tsx +299 -0
- package/template/src/pages/settings/company/integrations/webhooks-panel.tsx +179 -0
- package/template/src/pages/settings/company/modules-page.tsx +65 -0
- package/template/src/pages/settings/company/roles-page.tsx +116 -0
- package/template/src/pages/settings/company/security-policies-page.tsx +164 -0
- package/template/src/pages/settings/company/users-page.tsx +262 -0
- package/template/src/pages/settings/settings-layout.tsx +57 -0
- package/template/src/pages/settings/settings-nav.ts +69 -0
- package/template/src/pages/settings/settings-routes.tsx +49 -0
- package/template/src/pages/setup-page.tsx +387 -0
- package/template/src/providers.tsx +68 -0
- package/template/src/router.tsx +59 -0
- package/template/tools/check-design.mjs +73 -0
- package/template/tools/new-module.mjs +90 -0
- package/template/tools/templates/module/api/__id__.mock.ts.tpl +17 -0
- package/template/tools/templates/module/api/__id__.service.ts.tpl +51 -0
- package/template/tools/templates/module/hooks.ts.tpl +57 -0
- package/template/tools/templates/module/index.ts.tpl +44 -0
- package/template/tools/templates/module/locales/en.ts.tpl +51 -0
- package/template/tools/templates/module/locales/tr.ts.tpl +55 -0
- package/template/tools/templates/module/pages/__id__-detail-page.tsx.tpl +103 -0
- package/template/tools/templates/module/pages/__id__-form-page.tsx.tpl +121 -0
- package/template/tools/templates/module/pages/__id__-list-page.tsx.tpl +234 -0
- package/template/tools/templates/module/types.ts.tpl +32 -0
- package/template/tsconfig.app.json +32 -0
- package/template/tsconfig.json +12 -0
- package/template/tsconfig.node.json +23 -0
- package/template/vite.config.ts +47 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# freakpunch admin panel
|
|
2
|
+
|
|
3
|
+
A reusable admin panel, installed into a project's `fp-admin/` folder with `npm create freakpunch@latest`.
|
|
4
|
+
New features are **modules** that look and behave exactly like the rest of the panel. Never invent a new
|
|
5
|
+
look, layout or component style: reuse what exists and follow `design/`.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
A new panel is deliberately empty: no modules, no sample data, and optional built-in pages switched off in
|
|
9
|
+
`src/app.config.ts` → `features`. On first open, a setup wizard (`/setup`) creates the company and the Owner.
|
|
10
|
+
Until a backend is connected, data stays in the browser: see `BACKEND.md`.
|
|
11
|
+
|
|
12
|
+
## Rules for AI agents
|
|
13
|
+
|
|
14
|
+
1. Read this file and `design/` before changing anything.
|
|
15
|
+
2. New feature = new module: `npm run new-module -- <id> --en "Label" --tr "Etiket"`, then replace its sample
|
|
16
|
+
fields. The generated list, detail and form pages are the reference for new pages.
|
|
17
|
+
3. **Only change project files:** `src/modules/**`, `src/app.config.ts`, `src/app.roles.ts`, `public/**`.
|
|
18
|
+
Everything else (`src/core`, `src/pages`, `src/components/ui`, `src/locales`, `src/index.css`, `design/`,
|
|
19
|
+
`tools/`) belongs to freakpunch and is replaced by updates. If something is missing there, say so to the user
|
|
20
|
+
instead of editing or copying it.
|
|
21
|
+
4. Every string in both EN and TR. Tokens only. Shared components first (`design/COMPONENTS.md`).
|
|
22
|
+
5. Gate every route and action with a permission; add project-specific ones in `src/app.roles.ts`.
|
|
23
|
+
6. Never add sample or placeholder data to a panel unless the user asks for it.
|
|
24
|
+
7. `npm run check` must pass before you finish.
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm run dev # Vite dev server
|
|
30
|
+
npm run build # tsc -b && vite build (must pass with no errors)
|
|
31
|
+
npm run lint # oxlint (must pass with no errors or warnings)
|
|
32
|
+
npm run check # design rules + lint + build: run this before finishing any change
|
|
33
|
+
npm run new-module -- orders --en "Orders" --tr "Siparişler" [--group content]
|
|
34
|
+
npm run fp:update # installed copies: refresh freakpunch's files (add -- --dry-run to preview)
|
|
35
|
+
docker build -t freakpunch . && docker run --rm -p 8080:80 freakpunch
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Stack
|
|
39
|
+
|
|
40
|
+
Vite · React 19 · TypeScript (strict) · Tailwind CSS v4 · React Router v8 (data router, `react-router`) ·
|
|
41
|
+
TanStack Query v5 · TanStack Table **v9** (`useTable` + `tableFeatures`, not the v8 `useReactTable` API) ·
|
|
42
|
+
React Hook Form + Zod v4 · Recharts (through SortUI `chart`) · Sonner toasts.
|
|
43
|
+
|
|
44
|
+
UI components: **SortUI first** (shadcn-compatible, built on Base UI). **Component source order:** SortUI
|
|
45
|
+
first; if SortUI doesn't have it, the official shadcn/ui component (`npx shadcn@latest add <name>`);
|
|
46
|
+
always adapted to our tokens, never two versions of the same component. No other component libraries.
|
|
47
|
+
- Install: `npx shadcn@latest add @sortui/<name> -y` (component list: https://sortui.dev/docs/components).
|
|
48
|
+
- Files land in `src/components/ui/` and are vendored: avoid editing them; style via `className`
|
|
49
|
+
or tokens. Lint ignores them. The one exception: hard-coded screen-reader/default strings are
|
|
50
|
+
translated in place with `useTranslation()` (dialog, sheet, sidebar, spinner, breadcrumb,
|
|
51
|
+
pagination; keys in `common:a11y`, `common:actions.close`, `common:shell.*`). Re-adding one of those
|
|
52
|
+
with the CLI overwrites the file: reapply the translation.
|
|
53
|
+
- The CLI writes `import { cn } from "cn"`. Rewrite it to `@/lib/utils` after adding:
|
|
54
|
+
`sed -i 's|from "cn"|from "@/lib/utils"|' src/components/ui/*.tsx`
|
|
55
|
+
- Base UI uses `render` instead of `asChild`. A Button rendering a link needs `nativeButton={false}`;
|
|
56
|
+
use `ButtonLink` (`src/core/components/button-link.tsx`) for router links.
|
|
57
|
+
- Theme: we use our own `ThemeProvider` (`src/core/theme/theme-context.tsx`), not `next-themes`.
|
|
58
|
+
`next-themes` stays installed only because the vendored `sonner.tsx` imports it.
|
|
59
|
+
|
|
60
|
+
## Folder structure
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
src/
|
|
64
|
+
app.config.ts ← per-project config: name, brand, currency, workspaces, nav groups, modules
|
|
65
|
+
index.css ← THE design-token source (colors, radius, shadows, layout spacing)
|
|
66
|
+
main.tsx providers.tsx router.tsx
|
|
67
|
+
locales/ ← translations: {en,tr}/<namespace>.ts + index.ts (registers every namespace)
|
|
68
|
+
components/ui/ ← SortUI components (vendored, CLI-managed)
|
|
69
|
+
core/ ← identical in every project
|
|
70
|
+
api/ http.ts (fetch client, ApiError, bearer/cookie auth), rest.ts (createRestService,
|
|
71
|
+
toPaginated), mock.ts (delay, paginate, createMockCollection, createMockDocument),
|
|
72
|
+
types.ts (ListParams, Paginated)
|
|
73
|
+
auth/ auth-store (session in localStorage), auth.service (mock, swappable), use-auth,
|
|
74
|
+
require-auth (→ /login?redirect=…), roles, permissions (role → permission map),
|
|
75
|
+
can (Can, useCan, usePermissions), require-permission (route guard), dev-role-switcher
|
|
76
|
+
users/ user model, mock team (one demo user per role), users.service, hooks
|
|
77
|
+
setup/ first-run wizard service (local or POST /setup) + SetupGate
|
|
78
|
+
company/ company settings (general, branding, security policies), password-policy, hooks
|
|
79
|
+
audit/ audit.service (record + list), hooks
|
|
80
|
+
notifications/ event catalog + types, notifications.service (feed + settings, mock, per user),
|
|
81
|
+
hooks (polling, optimistic mark-read), notification-center (top-bar bell + panel)
|
|
82
|
+
preferences/ preferences-store (time zone, date format, sidebar default)
|
|
83
|
+
i18n/ i18n init + language detection, keys (I18nKey, useAnyT, vmsg), use-format,
|
|
84
|
+
language-switcher, types (Messages<T>), i18next.d.ts (typed keys)
|
|
85
|
+
components/ page (Page, Panel), states (Loading/Empty/ErrorState), form-page (FormLayout,
|
|
86
|
+
FormSection), form-error, confirm-dialog (useConfirm, typed confirm), status-badge,
|
|
87
|
+
segmented, button-link, user-avatar, image-upload, phone-input, date-picker,
|
|
88
|
+
search-select, file-attachments,
|
|
89
|
+
data-table/ (DataTable, TableHeaderCell, features, toolbar, use-list-url-state)
|
|
90
|
+
layout/ app-shell (canvas + inset panel), app-sidebar, top-bar (breadcrumbs), command-menu (⌘K)
|
|
91
|
+
config/ types (AppConfig, FeatureId, AuthMode), runtime (isFeatureOn, authMode, showLanding)
|
|
92
|
+
modules/ types (AppModule, NavItem…), navigation (nav built from config + modules), lazy-page
|
|
93
|
+
theme/ theme-context (light/dark/system), brand (presets), brand-context, theme-toggle
|
|
94
|
+
lib/ storage (namespaced localStorage), format (createFormatters, initials), phone, csv
|
|
95
|
+
modules/
|
|
96
|
+
registry.ts ← every available module; app.config.ts decides which are on
|
|
97
|
+
locales.ts ← registers each module's translations
|
|
98
|
+
orders/ ← what `npm run new-module` generates
|
|
99
|
+
index.ts module definition: nav, routes, quick actions (labels = i18n keys, permissions)
|
|
100
|
+
locales/ en.ts + tr.ts (namespace = module id)
|
|
101
|
+
types.ts entity types + Zod form schema
|
|
102
|
+
api/ orders.service.ts (interface + mock impl), orders.mock.ts (seed)
|
|
103
|
+
hooks.ts TanStack Query keys + hooks
|
|
104
|
+
components/ pages/
|
|
105
|
+
pages/ core pages: setup, login, dashboard/, not-found, no-access
|
|
106
|
+
settings/ settings-routes, settings-nav, settings-layout (left sub-nav),
|
|
107
|
+
account/ (profile, security, notifications, preferences),
|
|
108
|
+
company/ (general, branding, users, roles, security policies, modules,
|
|
109
|
+
integrations/, audit log, danger zone)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Architecture rules
|
|
113
|
+
|
|
114
|
+
- **Screens never call fetch or mocks directly.** Data flows: page → module `hooks.ts` (TanStack Query)
|
|
115
|
+
→ `*.service.ts` (typed interface) → mock today / `http` tomorrow.
|
|
116
|
+
- **Swapping to a real API** = implement the service interface with `http` from `@/core/api/http` and
|
|
117
|
+
change the one `export const xService = …` line. Map backend responses into `Paginated<T>` /
|
|
118
|
+
entity types inside the service so screens don't change. Set `VITE_API_URL` for the base URL.
|
|
119
|
+
- **Real JWT auth** = reimplement `AuthService` in `core/auth/auth.service.ts` (call the login
|
|
120
|
+
endpoint, then `authStore.set(session)`). `http` already sends `Authorization: Bearer <token>` and
|
|
121
|
+
clears the session on 401. `RequireAuth` and the screens stay as they are.
|
|
122
|
+
- Mock services use `createMockCollection(name, seed)` so data persists in localStorage
|
|
123
|
+
(`fp:mock:<name>`) and survives reloads. Clear site data to reset.
|
|
124
|
+
- Lists keep state in the URL via `useListUrlState` (`?page=&size=&sort=field.dir&q=&<filter>=`) and
|
|
125
|
+
use **server-side** pagination/sorting (`manualPagination`, `manualSorting`): the service does the
|
|
126
|
+
work, as a backend would.
|
|
127
|
+
- Keep TanStack Table `columns` stable (`useMemo` with stable deps, `mutateAsync` instead of the
|
|
128
|
+
mutation object). Build columns with `createColumnHelper<DataTableFeatures, Row>()`.
|
|
129
|
+
- Forms: React Hook Form + `zodResolver`, SortUI `Field`/`FieldLabel` + `FormFieldError` (translates
|
|
130
|
+
`vmsg` keys), default `onSubmit` validation mode (`onTouched` races with autofocus+submit and drops
|
|
131
|
+
errors). Use `useWatch`, not `watch` (React Compiler lint). Settings forms use
|
|
132
|
+
`FormLayout saveBar="dirty"` (save bar only when dirty).
|
|
133
|
+
- Feedback: `toast` from `sonner`; destructive confirmations via `useConfirm()`; query errors via
|
|
134
|
+
`ErrorState` (404 `ApiError` renders "Not found" with `notFoundAction`).
|
|
135
|
+
- Every page wraps content in `<Page title description actions>` (sets the document title).
|
|
136
|
+
Breadcrumbs come from route `handle: { crumb: 'ns:key' }`; access from `handle.permission`.
|
|
137
|
+
- Toasts appear bottom-center everywhere (`providers.tsx`).
|
|
138
|
+
- Mock services record audit entries (`auditService.record`) when company settings, users or
|
|
139
|
+
integrations change; a real backend writes the audit log server-side.
|
|
140
|
+
- Accessibility: real links for navigation (`Link`/`ButtonLink`), `aria-label` on icon buttons,
|
|
141
|
+
visible focus rings (brand-colored `--ring`), keyboard paths for everything (⌘K, ⌘B sidebar).
|
|
142
|
+
|
|
143
|
+
## Design system
|
|
144
|
+
|
|
145
|
+
**`design/` is the official design system: read it before building any screen.**
|
|
146
|
+
`design/DESIGN.md` (principles, layout, type, spacing, radius, shadows, color tokens, accent/semantic
|
|
147
|
+
rules), `design/COMPONENTS.md` (which SortUI + shared component for what), `design/PATTERNS.md`
|
|
148
|
+
(list/form/detail/settings page patterns, do's and don'ts).
|
|
149
|
+
- Every new screen must follow `design/` and reuse existing tokens and shared components.
|
|
150
|
+
- Never introduce new colors, radii, shadows or fonts outside the token system in `src/index.css`.
|
|
151
|
+
If something is truly missing, add a token there (light + dark) and document it in `design/DESIGN.md`.
|
|
152
|
+
- Other components may be added following the component source order (SortUI → official shadcn/ui);
|
|
153
|
+
no other component libraries.
|
|
154
|
+
|
|
155
|
+
Minimal, Apple/Revolut-like. No glassmorphism (overlays dim, never blur).
|
|
156
|
+
- **Tokens only.** All colors, radius, shadows and layout spacing are CSS variables in `src/index.css`
|
|
157
|
+
(`:root` + `.dark`), mapped to Tailwind in `@theme inline`. Never hard-code colors in components;
|
|
158
|
+
use `bg-card`, `text-muted-foreground`, `border`, `bg-fill-1/2`, `bg-raised`, `bg-canvas`,
|
|
159
|
+
`text-success/warning/danger/info`, `shadow-(--shadow-card)` etc.
|
|
160
|
+
- Palette: black/white/warm grays + **one brand accent** (`--brand`, default indigo). `--primary`,
|
|
161
|
+
`--ring`, sidebar active icon and chart series 1 all derive from it. Semantic colors only where they
|
|
162
|
+
carry meaning (status dots/badges, deltas).
|
|
163
|
+
- Type: **Inter is the only font.** Small uppercase labels use the `label-caps` utility; aligned numbers
|
|
164
|
+
use `tabular-nums`. There is no monospace font (`font-mono` is disabled).
|
|
165
|
+
- 1px hairline borders, very soft shadows (`--shadow-card`, `--shadow-raised`, `--shadow-panel`),
|
|
166
|
+
12px radius for cards/controls (`rounded-xl`), 16px for the floating panel (`--radius-panel`).
|
|
167
|
+
- Layout: warm off-white canvas (`--canvas`) with the sidebar directly on it; content in a floating
|
|
168
|
+
white panel (`--background`) inset by `--panel-inset` (matches SortUI inset sidebar `m-2`).
|
|
169
|
+
The active nav item is a raised bordered pill (`bg-raised` + ring + `--shadow-raised`).
|
|
170
|
+
|
|
171
|
+
## Roles & permissions
|
|
172
|
+
|
|
173
|
+
Four built-in roles (`core/auth/roles.ts`), plus project-defined ones from `src/app.roles.ts`: **owner** (everything; exactly one; can't be removed or
|
|
174
|
+
demoted, only transfer ownership), **admin** (everything except the danger zone; can't edit the Owner),
|
|
175
|
+
**editor** (create/edit/delete in modules, no company settings), **viewer** (read-only).
|
|
176
|
+
- `core/auth/permissions.ts` is the single role → permission map. Permission strings are
|
|
177
|
+
`<resource>.<action>` (`orders.delete`, `settings.company.edit`, `users.manage`,
|
|
178
|
+
`company.transfer`) and are meant to be reused 1:1 as Spring Security authorities. Every registered
|
|
179
|
+
module automatically gets `<id>.view|create|edit|delete`.
|
|
180
|
+
- UI: `useCan('x')` / `usePermissions().can` / `<Can permission="x">` (hide, or render-prop to
|
|
181
|
+
disable). Nav items, quick actions and settings nav take `permission`. Routes take
|
|
182
|
+
`handle.permission`; `RequirePermissions` in the shell redirects to `/app/no-access`.
|
|
183
|
+
Owner rules for user management: `canManageUser(actor, target)`.
|
|
184
|
+
- **UI checks are not security.** The backend must enforce every permission (the mock services
|
|
185
|
+
enforce the Owner rules the same way a backend should).
|
|
186
|
+
- In `npm run dev` the top
|
|
187
|
+
bar has a role switcher (`DevRoleSwitcher`, code-split behind `import.meta.env.DEV`, absent from
|
|
188
|
+
production builds).
|
|
189
|
+
|
|
190
|
+
## Settings
|
|
191
|
+
|
|
192
|
+
`/app/settings` has a left sub-navigation inside the panel (horizontal scroller on mobile).
|
|
193
|
+
"My account" (profile, security, notifications, preferences) is for everyone; the "Company" group needs
|
|
194
|
+
`settings.company.view` (Owner/Admin) and the danger zone `company.delete` (Owner). Add a page: a
|
|
195
|
+
route in `settings-routes.tsx` (crumb + permission) and an item in `settings-nav.ts` (same permission).
|
|
196
|
+
Company forms are read-only (`FormLayout readOnly`) without `settings.company.edit`.
|
|
197
|
+
|
|
198
|
+
## Internationalization (EN + TR)
|
|
199
|
+
|
|
200
|
+
react-i18next. English is the default and fallback; Turkish is a full second language.
|
|
201
|
+
**Every feature ships with both EN and TR strings in the same change. No user-facing text in
|
|
202
|
+
components**: page titles, labels, `aria-label`, `placeholder`, toasts, empty states, validation.
|
|
203
|
+
- Files: `src/locales/{en,tr}/<namespace>.ts` (common, auth, landing, errors, dashboard, settings,
|
|
204
|
+
account, company, notifications), registered in `src/locales/index.ts`, and module-owned
|
|
205
|
+
`src/modules/<module>/locales/{en,tr}.ts`, registered in `src/modules/locales.ts`.
|
|
206
|
+
- Locale files are TS objects. `tr` files are typed `Messages<typeof en>`, so a missing/extra Turkish
|
|
207
|
+
key fails `npm run build`. Keys are typed (`src/core/i18n/i18next.d.ts`): `t('typo')` doesn't compile.
|
|
208
|
+
- Add a string: add the key to `en/<ns>.ts`, then the same key to `tr/<ns>.ts` (correct Turkish
|
|
209
|
+
characters: ç ğ ı İ ö ş ü). Use `{{name}}` interpolation, `_one`/`_other` plurals with `count`,
|
|
210
|
+
`<b>…</b>` + `<Trans components>` for inline markup. Never concatenate translated fragments.
|
|
211
|
+
- Add a namespace: create `en/<ns>.ts` + `tr/<ns>.ts` and register both in `src/locales/index.ts`
|
|
212
|
+
(modules: `locales/` and `src/modules/locales.ts`), use `useTranslation(['<ns>', 'common'])` and `t('common:actions.save')`
|
|
213
|
+
for shared strings.
|
|
214
|
+
- Config-like data (nav labels, crumbs, quick actions, nav groups, workspace plans) stores
|
|
215
|
+
`I18nKey` values (`'orders:nav'`) and is translated with `useAnyT()`.
|
|
216
|
+
- Zod messages are keys: `vmsg('validation.minChars', { min: 2 })`; render errors with
|
|
217
|
+
`FormFieldError` (translates at render time).
|
|
218
|
+
- Formatting: `const fmt = useFormat()` → `fmt.currency/number/date/dateTime/relative/…` (Intl with the
|
|
219
|
+
active locale, user timezone/date format, company currency). Pass `fmt` and `t` as `useMemo` deps
|
|
220
|
+
of table columns.
|
|
221
|
+
- Language: `LanguageSwitcher` (landing, login, top bar) and Settings → Preferences. Stored in
|
|
222
|
+
`localStorage` `fp:lang`; first visit uses TR if the browser language is Turkish, else EN.
|
|
223
|
+
`<html lang>` follows the active language.
|
|
224
|
+
- Vendored SortUI screen-reader strings (dialog/sheet "Close", mobile sidebar title, "Toggle sidebar",
|
|
225
|
+
spinner "Loading", pagination) are translated in the vendored files (see Stack). Our dialogs still use
|
|
226
|
+
`showCloseButton={false}` + a footer Cancel.
|
|
227
|
+
|
|
228
|
+
## Notifications
|
|
229
|
+
|
|
230
|
+
Top-bar bell (`core/notifications/notification-center.tsx`): unread badge (brand color, hidden at 0,
|
|
231
|
+
"9+" above 9) and a panel (popover on desktop, bottom sheet on mobile) with All / Unread tabs, Today /
|
|
232
|
+
Earlier groups, mark one / all as read, and a link to Settings → Notifications.
|
|
233
|
+
- One event catalog (`core/notifications/types.ts` → `notificationEvents`) drives both the settings
|
|
234
|
+
toggles and the feed. Every notification has an `event`; events with in-app off are not returned.
|
|
235
|
+
Add an event there + its label in `account:notifications.events` (EN + TR).
|
|
236
|
+
- The API sends `template` + `params`, never rendered text; the client renders
|
|
237
|
+
`notifications:templates.<template>.title|body` in the active language (roles, money and numbers are
|
|
238
|
+
localized at render time). Add a template to `notificationTemplates` and both locale files.
|
|
239
|
+
- Data: `notificationsService` (per signed-in user; the mock seeds a different feed per role and
|
|
240
|
+
occasionally adds a new notification) → `useNotifications()` polls every 30 s. For a real backend
|
|
241
|
+
implement the interface with `http` (`GET /me/notifications`, `POST …/{id}/read`, `POST …/read-all`,
|
|
242
|
+
`GET|PUT /me/notification-settings`) and optionally `subscribe()` for SSE/WebSocket push; the hooks
|
|
243
|
+
already invalidate on pushed events and keep polling as a fallback.
|
|
244
|
+
|
|
245
|
+
## How to add a module
|
|
246
|
+
|
|
247
|
+
`npm run new-module -- orders --en "Orders" --tr "Siparişler"` creates `src/modules/orders` (list with search,
|
|
248
|
+
filters, bulk delete and CSV export; create/edit form; mock data; REST switch; EN + TR) and registers it in
|
|
249
|
+
`registry.ts`, `locales.ts` and `app.config.ts`. Replace the sample fields. It leaves the `// fp:*` marker
|
|
250
|
+
comments in those files in place; keep them. What it sets up, if you need to do it by hand:
|
|
251
|
+
|
|
252
|
+
1. Module folder with types, service, hooks, pages and `locales/`.
|
|
253
|
+
2. `<name>/locales/en.ts` + `tr.ts` registered in `src/modules/locales.ts` as namespace `<name>`.
|
|
254
|
+
3. In `<name>/index.ts` export an `AppModule` (labels/crumbs are translation keys):
|
|
255
|
+
```ts
|
|
256
|
+
export const ordersModule: AppModule = {
|
|
257
|
+
id: 'orders',
|
|
258
|
+
nav: [{ label: 'orders:nav', path: '/app/orders', icon: ShoppingCart, group: 'catalog',
|
|
259
|
+
useBadge: usePendingCount, permission: 'orders.view' }],
|
|
260
|
+
quickActions: [{ label: 'orders:quickActions.new', path: '/app/orders/new', icon: Plus, permission: 'orders.create' }],
|
|
261
|
+
routes: [{ path: 'orders', handle: { crumb: 'orders:crumbs.list', permission: 'orders.view' }, children: [
|
|
262
|
+
{ index: true, lazy: lazyPage(() => import('./pages/orders-list-page')) },
|
|
263
|
+
{ path: 'new', handle: { crumb: 'orders:crumbs.new', permission: 'orders.create' }, lazy: … },
|
|
264
|
+
] }],
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
Routes are relative to `/app` and render inside the shell; pages are default exports, lazy-loaded.
|
|
268
|
+
Gate buttons with `<Can permission="orders.delete">`.
|
|
269
|
+
4. Register it in `src/modules/registry.ts` (`orders: ordersModule`); this also creates the
|
|
270
|
+
`orders.view|create|edit|delete` permissions and their row in the roles matrix.
|
|
271
|
+
5. Enable it in `src/app.config.ts` → `modules: { orders: true }`. Nav groups come from
|
|
272
|
+
`appConfig.navGroups` (`group` must match an id); empty groups are hidden.
|
|
273
|
+
6. The sidebar, ⌘K menu, dashboard quick actions, routes and Settings → Modules pick it up automatically.
|
|
274
|
+
|
|
275
|
+
Extras: `dashboardWidgets: [Component]` on the module adds cards to the dashboard. Extra permissions
|
|
276
|
+
(`orders.refund`) and extra roles (`hr`) go in `src/app.roles.ts`, with their EN + TR labels.
|
|
277
|
+
|
|
278
|
+
## How to change the brand color
|
|
279
|
+
|
|
280
|
+
- Per project: set `brand` in `src/app.config.ts` to a preset key from `src/core/theme/brand.ts`
|
|
281
|
+
(`indigo`, `blue`, `violet`, `emerald`, `orange`, `rose`, `graphite`) or a custom
|
|
282
|
+
`{ color: 'oklch(…)', foreground: 'oklch(1 0 0)' }`. Also update the `--brand` default in
|
|
283
|
+
`src/index.css` to the same value so the first paint matches.
|
|
284
|
+
- Add presets in `brandPresets` (`brand.ts`) and a label in `company:branding.presets` (EN + TR); they appear in Settings → Company → Branding.
|
|
285
|
+
- Settings → Company → Branding previews the brand live (`previewBrand`) and saves it for the company (cached in localStorage `fp:brand` for first paint).
|
|
286
|
+
|
|
287
|
+
## Deployment & indexing
|
|
288
|
+
|
|
289
|
+
Backend connection, auth modes and serving under `/admin` (`VITE_BASE_PATH`) are in `BACKEND.md`.
|
|
290
|
+
|
|
291
|
+
- `Dockerfile`: node:24-alpine build → nginx:alpine on port 80 with `nginx.conf`.
|
|
292
|
+
- `nginx.conf`: SPA fallback (`try_files … /index.html`) so deep links work on refresh; immutable cache
|
|
293
|
+
for `/assets/`, `no-cache` for `index.html`; `X-Robots-Tag: noindex, nofollow` on every response
|
|
294
|
+
(repeated inside locations that set their own headers).
|
|
295
|
+
- Search engines must not index the site: keep the `robots` meta tag in `index.html`,
|
|
296
|
+
`public/robots.txt` (`Disallow: /`) and the nginx header.
|
|
297
|
+
- `assets/` holds design reference screenshots (not part of the build).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
|
|
3
|
+
# ---- Build stage ----
|
|
4
|
+
FROM node:24-alpine AS build
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
|
|
7
|
+
COPY package.json package-lock.json ./
|
|
8
|
+
RUN npm ci
|
|
9
|
+
|
|
10
|
+
COPY . .
|
|
11
|
+
RUN npm run build
|
|
12
|
+
|
|
13
|
+
# ---- Serve stage ----
|
|
14
|
+
FROM nginx:alpine
|
|
15
|
+
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
16
|
+
COPY --from=build /app/dist /usr/share/nginx/html
|
|
17
|
+
|
|
18
|
+
EXPOSE 80
|
|
19
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Logs
|
|
2
|
+
logs
|
|
3
|
+
*.log
|
|
4
|
+
npm-debug.log*
|
|
5
|
+
yarn-debug.log*
|
|
6
|
+
yarn-error.log*
|
|
7
|
+
pnpm-debug.log*
|
|
8
|
+
lerna-debug.log*
|
|
9
|
+
|
|
10
|
+
node_modules
|
|
11
|
+
dist
|
|
12
|
+
packages/create-freakpunch/template
|
|
13
|
+
*.tgz
|
|
14
|
+
dist-ssr
|
|
15
|
+
*.local
|
|
16
|
+
|
|
17
|
+
# Editor directories and files
|
|
18
|
+
.vscode/*
|
|
19
|
+
!.vscode/extensions.json
|
|
20
|
+
.idea
|
|
21
|
+
.DS_Store
|
|
22
|
+
*.suo
|
|
23
|
+
*.ntvs*
|
|
24
|
+
*.njsproj
|
|
25
|
+
*.sln
|
|
26
|
+
*.sw?
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "base-nova",
|
|
4
|
+
"rsc": false,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/index.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"iconLibrary": "lucide",
|
|
14
|
+
"rtl": false,
|
|
15
|
+
"aliases": {
|
|
16
|
+
"components": "@/components",
|
|
17
|
+
"utils": "@/lib/utils",
|
|
18
|
+
"ui": "@/components/ui",
|
|
19
|
+
"lib": "@/lib",
|
|
20
|
+
"hooks": "@/hooks"
|
|
21
|
+
},
|
|
22
|
+
"menuColor": "default",
|
|
23
|
+
"menuAccent": "subtle",
|
|
24
|
+
"registries": {
|
|
25
|
+
"@sortui": "https://sortui.dev/r/{name}.json"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Components
|
|
2
|
+
|
|
3
|
+
## Component source order
|
|
4
|
+
|
|
5
|
+
1. **SortUI** first (`npx shadcn@latest add @sortui/<name> -y`).
|
|
6
|
+
2. If SortUI doesn't have it, the **official shadcn/ui** component (`npx shadcn@latest add <name>`).
|
|
7
|
+
3. Either way it is adapted to our tokens (no raw colors, radii, shadows or fonts) and there is
|
|
8
|
+
**never more than one version of the same component**: don't add a shadcn/ui component SortUI
|
|
9
|
+
already provides, and don't keep two variants side by side.
|
|
10
|
+
|
|
11
|
+
Two layers: **SortUI** primitives (vendored in `src/components/ui/`, CLI-managed, never edited) and our
|
|
12
|
+
**shared components** (`src/core/components/`, `src/core/layout/`, …) that compose them into the
|
|
13
|
+
design language. Screens use shared components first and SortUI primitives second. No other UI
|
|
14
|
+
library is allowed.
|
|
15
|
+
|
|
16
|
+
## SortUI components we use
|
|
17
|
+
|
|
18
|
+
Add more with `npx shadcn@latest add @sortui/<name> -y`, then rewrite the `cn` import (see CLAUDE.md).
|
|
19
|
+
|
|
20
|
+
| Component | Use it for | Notes |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| `button` | every action | One `default` (brand) button per view = the primary action. Others: `outline` (secondary), `ghost` (tertiary / toolbar / icon buttons), `destructive` (delete, irreversible), `link`. Sizes: `default` in pages, `sm` in dense toolbars and cards, `lg`/`xl` on login/landing, `icon-sm` for row/toolbar icons. Use `data-icon="inline-start"` on leading icons. |
|
|
23
|
+
| `input`, `textarea`, `input-group` | text entry | `InputGroup` for prefixes (currency symbol, search icon, country code). |
|
|
24
|
+
| `field` | every form control | `Field` + `FieldLabel` + control + `FieldDescription` + `FormFieldError`. `data-invalid` + `aria-invalid` when invalid. `orientation="horizontal"` for switch/checkbox rows. |
|
|
25
|
+
| `select` | single choice from a list | Always pass `items` so the trigger shows the label. Filters use `FilterSelect`. |
|
|
26
|
+
| `calendar`, `combobox` | via `DatePicker` / `SearchSelect` | Don't use them raw. |
|
|
27
|
+
| `checkbox`, `switch`, `radio-group` | booleans / small exclusive choices | Switch = takes effect (setting); checkbox = part of a form or selection. Radio cards (`FieldLabel` wrapping `Field`) for visual choices like theme. |
|
|
28
|
+
| `toggle-group` | via `Segmented` only | Don't use raw toggle groups for view switches. |
|
|
29
|
+
| `tabs` | switching between sibling views of one object | Not for top-level navigation (use routes). |
|
|
30
|
+
| `dropdown-menu` | row actions, account menu, theme/language menus | Destructive items last, separated, `variant="destructive"`. |
|
|
31
|
+
| `dialog` | short focused tasks (invite user, create API key, 2FA setup) | Title + description + body + footer (cancel left of primary). |
|
|
32
|
+
| `alert-dialog` | via `useConfirm()` only | Confirmations, including typed confirmations. |
|
|
33
|
+
| `sheet` | mobile sidebar (managed by `sidebar`), mobile notification panel (`side="bottom"`) | Use where a desktop popover would be too small on a phone. |
|
|
34
|
+
| `popover` | non-modal panels anchored to a trigger (notification panel) | `p-0` + own header/body/footer for list panels. |
|
|
35
|
+
| `sidebar` | app shell only | `variant="inset"`, `collapsible="icon"`. |
|
|
36
|
+
| `table` | via `DataTable` | Use raw `Table` only for small static tables inside a card (e.g. login history, roles matrix), always with `TableHeaderCell` for headers. |
|
|
37
|
+
| `badge` | counts, role labels, "Current", "New" | Status uses `StatusBadge`, not colored badges. Role uses `variant="secondary"`. |
|
|
38
|
+
| `avatar` | people | `rounded-lg` square-ish avatars in lists and the sidebar; initials fallback on `bg-fill-2`. |
|
|
39
|
+
| `breadcrumb` | top bar only | Driven by route `handle.crumb`. |
|
|
40
|
+
| `command` | ⌘K menu only | |
|
|
41
|
+
| `empty` | via `EmptyState` / `ErrorState` | |
|
|
42
|
+
| `skeleton`, `spinner` | loading | Skeletons for known layouts (tables, KPI cards); `LoadingState` spinner for whole pages. |
|
|
43
|
+
| `tooltip` | icon-only buttons in collapsed sidebar, truncated text | Never put essential information only in a tooltip. |
|
|
44
|
+
| `kbd` | keyboard shortcuts | |
|
|
45
|
+
| `separator` | vertical separators in toolbars | Use `divide-y`/`border` for horizontal lines. |
|
|
46
|
+
| `chart` | all charts (Recharts inside `ChartContainer`) | Colors via `var(--chart-n)`. |
|
|
47
|
+
| `sonner` | toasts via `toast()` | Mounted once in `providers.tsx`, bottom-center. |
|
|
48
|
+
|
|
49
|
+
## Shared components
|
|
50
|
+
|
|
51
|
+
### Page frame — `core/components/page.tsx`
|
|
52
|
+
|
|
53
|
+
- **`<Page title description actions leading documentTitle>`**: every routed screen's root. Renders the
|
|
54
|
+
h1, description, right-aligned actions (primary action last) and sets `document.title`.
|
|
55
|
+
`title`/`description` are translated strings.
|
|
56
|
+
- **`<Panel label actions>`**: bordered card with a `label-caps` header row. Use for dashboard cards,
|
|
57
|
+
detail summaries and any titled group of content.
|
|
58
|
+
|
|
59
|
+
### Forms — `core/components/form-page.tsx`, `core/components/form-error.tsx`
|
|
60
|
+
|
|
61
|
+
- **`<FormLayout onSubmit submitLabel isSubmitting isDirty onCancel extraActions saveBar>`**: a form
|
|
62
|
+
card with sections divided by hairlines and a sticky action bar. `saveBar="dirty"` hides the bar
|
|
63
|
+
until the form has unsaved changes (settings pages); default `"always"` (create/edit pages).
|
|
64
|
+
- **`<FormSection title description>`**: two-column section (label column + fields) inside a
|
|
65
|
+
`FormLayout` or any `divide-y` card.
|
|
66
|
+
- **`<FormFieldError error={errors.x} />`**: translated replacement for SortUI `FieldError`. Zod
|
|
67
|
+
messages are translation keys (`vmsg('validation.required')`), translated at render time so they
|
|
68
|
+
switch language instantly.
|
|
69
|
+
|
|
70
|
+
### Lists — `core/components/data-table/`
|
|
71
|
+
|
|
72
|
+
- **`<DataTable>`**: server-driven table (TanStack Table v9) with toolbar slot, skeleton rows,
|
|
73
|
+
error/empty states, footer counter, page size and paging. Columns from
|
|
74
|
+
`createColumnHelper<DataTableFeatures, Row>()`, memoized.
|
|
75
|
+
- **`useListUrlState`**: keeps page/size/sort/search/filters in the URL.
|
|
76
|
+
- **`selection` prop**: row checkboxes and a bar with bulk actions. Bulk deletes go through `useConfirm`.
|
|
77
|
+
- **`downloadCsv`** (`core/lib/csv.ts`): CSV export with Excel-safe encoding and escaped formulas.
|
|
78
|
+
- **`TableSearch`** (debounced), **`FilterSelect`** ("All …" clears), **`ClearFiltersButton`**.
|
|
79
|
+
- **`TableHeaderCell`**: the shared header cell style. `DataTable` uses it; small static tables must
|
|
80
|
+
too (never copy the header classes by hand).
|
|
81
|
+
|
|
82
|
+
### States — `core/components/states.tsx`
|
|
83
|
+
|
|
84
|
+
- **`LoadingState`**: centered spinner + label for whole-page loading.
|
|
85
|
+
- **`EmptyState`**: icon tile + title + description + optional action. Two flavours per list:
|
|
86
|
+
"nothing yet" (with a create action) and "no matches" (with clear filters).
|
|
87
|
+
- **`ErrorState`**: query errors with retry; a 404 `ApiError` renders "Not found" with
|
|
88
|
+
`notFoundAction`.
|
|
89
|
+
|
|
90
|
+
### Feedback
|
|
91
|
+
|
|
92
|
+
- **`useConfirm()`** (`core/components/confirm-dialog.tsx`): promise-based confirm.
|
|
93
|
+
`destructive: true` for irreversible actions, `typeToConfirm: companyName` to require typing a value
|
|
94
|
+
(danger zone).
|
|
95
|
+
- **`toast`** from `sonner`: success after a save, error with the message on failure,
|
|
96
|
+
`toast.promise` for row actions. Messages are translated.
|
|
97
|
+
|
|
98
|
+
### Status and small pieces
|
|
99
|
+
|
|
100
|
+
- **`StatusBadge tone`** / **`StatusDot tone`**: neutral outline pill + semantic dot.
|
|
101
|
+
- **`Segmented`**: iOS-style segmented control for 2–4 short options (date ranges, role picker).
|
|
102
|
+
- **`ButtonLink`**: a `Button` that is a router link.
|
|
103
|
+
- **`LanguageSwitcher`** (`core/i18n/language-switcher.tsx`) and **`ThemeToggle`**
|
|
104
|
+
(`core/theme/theme-toggle.tsx`): ghost icon-button menus, used together in headers.
|
|
105
|
+
- **`Can`** / **`useCan`** (`core/auth/can.tsx`): permission-gated UI (hide or disable).
|
|
106
|
+
- **`UserAvatar`** (`core/components/user-avatar.tsx`): avatar image or initials.
|
|
107
|
+
- **`ImageUpload`** (`core/components/image-upload.tsx`): file picker with preview tile, used for
|
|
108
|
+
avatars, logos and favicons.
|
|
109
|
+
- **`DatePicker`** (`core/components/date-picker.tsx`): date-only field, value is `YYYY-MM-DD`.
|
|
110
|
+
- **`SearchSelect`** (`core/components/search-select.tsx`): single choice from a long list with type-to-search.
|
|
111
|
+
Short lists use `select`.
|
|
112
|
+
- **`FileAttachments`** (`core/components/file-attachments.tsx`): pick and list files; the service uploads
|
|
113
|
+
them with `http.post(path, formData)`.
|
|
114
|
+
- **`PhoneInput`** (`core/components/phone-input.tsx`): country calling code (localized names) +
|
|
115
|
+
national number; validate with `isValidPhone`, store with `toE164` (`core/lib/phone.ts`).
|
|
116
|
+
- **`ReadOnlyNotice`** (`pages/settings/company/components`): muted bar above company forms the
|
|
117
|
+
current role can view but not edit.
|
|
118
|
+
- **`DevRoleSwitcher`** (`core/auth/dev-role-switcher.tsx`): development-only top-bar control.
|
|
119
|
+
|
|
120
|
+
### Layout — `core/layout/`
|
|
121
|
+
|
|
122
|
+
`AppShell` (canvas + inset panel, auth + permission guard), `AppSidebar` (workspace switcher, search,
|
|
123
|
+
nav groups, user card with profile + sign out), `TopBar` (sidebar toggle, breadcrumbs, dev role
|
|
124
|
+
switcher, language, clock, notifications, theme), `CommandMenu` (⌘K).
|
|
125
|
+
|
|
126
|
+
### Notifications — `core/notifications/notification-center.tsx`
|
|
127
|
+
|
|
128
|
+
**`NotificationBell`**: ghost icon button with a brand-colored unread badge; opens the
|
|
129
|
+
notification panel in a `popover` (desktop) or bottom `sheet` (mobile). Items: type icon tile
|
|
130
|
+
(semantic color + translated type for screen readers), title, body, relative time, unread dot;
|
|
131
|
+
items with a link are real links. Settings has its own sub-navigation (`pages/settings/settings-layout.tsx`).
|
|
132
|
+
|
|
133
|
+
## Usage rules
|
|
134
|
+
|
|
135
|
+
1. Reach for a shared component before composing primitives. If you compose the same primitives twice,
|
|
136
|
+
promote them to `src/core/components/`.
|
|
137
|
+
2. Never edit `src/components/ui/*`. Style through `className` with tokens, or with unlayered
|
|
138
|
+
attribute selectors in `index.css` (see the overlay rule). Only exception: translating hard-coded
|
|
139
|
+
screen-reader strings with `useTranslation()` (see CLAUDE.md).
|
|
140
|
+
3. Never pass raw colors, shadows or radii in `className` (`bg-[#…]`, `shadow-[…]`, `rounded-[…]`).
|
|
141
|
+
4. Every visible string, `aria-label`, `placeholder` and `title` comes from `t()`.
|
|
142
|
+
5. Icon-only buttons: `variant="ghost" size="icon-sm"` + translated `aria-label`.
|
|
143
|
+
6. Destructive actions always go through `useConfirm({ destructive: true })`.
|
|
144
|
+
7. Permission-dependent controls use `Can`/`useCan`: hide what the user can never do; disable (with a
|
|
145
|
+
reason) what they can't do in this context, e.g. editing the Owner.
|