create-freakpunch 0.0.0-stage → 0.1.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.
Files changed (220) hide show
  1. package/README.md +22 -2
  2. package/bin/create-freakpunch.mjs +227 -0
  3. package/files/README.md +15 -0
  4. package/files/app.config.ts +58 -0
  5. package/package.json +21 -4
  6. package/template/.dockerignore +17 -0
  7. package/template/.oxlintrc.json +9 -0
  8. package/template/AGENTS.md +6 -0
  9. package/template/BACKEND.md +86 -0
  10. package/template/CLAUDE.md +292 -0
  11. package/template/Dockerfile +19 -0
  12. package/template/_gitignore +26 -0
  13. package/template/components.json +27 -0
  14. package/template/design/COMPONENTS.md +145 -0
  15. package/template/design/DESIGN.md +212 -0
  16. package/template/design/PATTERNS.md +110 -0
  17. package/template/index.html +25 -0
  18. package/template/nginx.conf +32 -0
  19. package/template/package-lock.json +6484 -0
  20. package/template/package.json +51 -0
  21. package/template/public/favicon.svg +1 -0
  22. package/template/public/robots.txt +2 -0
  23. package/template/src/app.config.ts +61 -0
  24. package/template/src/app.roles.ts +24 -0
  25. package/template/src/components/ui/alert-dialog.tsx +181 -0
  26. package/template/src/components/ui/avatar.tsx +116 -0
  27. package/template/src/components/ui/badge.tsx +57 -0
  28. package/template/src/components/ui/breadcrumb.tsx +130 -0
  29. package/template/src/components/ui/button.tsx +58 -0
  30. package/template/src/components/ui/calendar.tsx +277 -0
  31. package/template/src/components/ui/card.tsx +99 -0
  32. package/template/src/components/ui/chart.tsx +370 -0
  33. package/template/src/components/ui/checkbox.tsx +28 -0
  34. package/template/src/components/ui/combobox.tsx +329 -0
  35. package/template/src/components/ui/command.tsx +191 -0
  36. package/template/src/components/ui/dialog.tsx +162 -0
  37. package/template/src/components/ui/dropdown-menu.tsx +271 -0
  38. package/template/src/components/ui/empty.tsx +103 -0
  39. package/template/src/components/ui/field.tsx +236 -0
  40. package/template/src/components/ui/input-group.tsx +263 -0
  41. package/template/src/components/ui/input.tsx +45 -0
  42. package/template/src/components/ui/kbd.tsx +31 -0
  43. package/template/src/components/ui/label.tsx +17 -0
  44. package/template/src/components/ui/pagination.tsx +134 -0
  45. package/template/src/components/ui/popover.tsx +87 -0
  46. package/template/src/components/ui/radio-group.tsx +35 -0
  47. package/template/src/components/ui/select.tsx +204 -0
  48. package/template/src/components/ui/separator.tsx +25 -0
  49. package/template/src/components/ui/sheet.tsx +139 -0
  50. package/template/src/components/ui/sidebar.tsx +741 -0
  51. package/template/src/components/ui/skeleton.tsx +13 -0
  52. package/template/src/components/ui/sonner.tsx +78 -0
  53. package/template/src/components/ui/spinner.tsx +42 -0
  54. package/template/src/components/ui/switch.tsx +29 -0
  55. package/template/src/components/ui/table.tsx +121 -0
  56. package/template/src/components/ui/tabs.tsx +191 -0
  57. package/template/src/components/ui/textarea.tsx +17 -0
  58. package/template/src/components/ui/toggle-group.tsx +94 -0
  59. package/template/src/components/ui/toggle.tsx +48 -0
  60. package/template/src/components/ui/tooltip.tsx +91 -0
  61. package/template/src/core/api/http.ts +76 -0
  62. package/template/src/core/api/mock.ts +88 -0
  63. package/template/src/core/api/rest.ts +67 -0
  64. package/template/src/core/api/types.ts +21 -0
  65. package/template/src/core/audit/audit.service.ts +125 -0
  66. package/template/src/core/audit/hooks.ts +19 -0
  67. package/template/src/core/auth/auth-store.ts +52 -0
  68. package/template/src/core/auth/auth.service.ts +75 -0
  69. package/template/src/core/auth/can.tsx +39 -0
  70. package/template/src/core/auth/dev-role-switcher.tsx +64 -0
  71. package/template/src/core/auth/permissions.ts +115 -0
  72. package/template/src/core/auth/require-auth.tsx +16 -0
  73. package/template/src/core/auth/require-permission.tsx +24 -0
  74. package/template/src/core/auth/roles.ts +9 -0
  75. package/template/src/core/auth/types.ts +20 -0
  76. package/template/src/core/auth/use-auth.ts +19 -0
  77. package/template/src/core/company/company.service.ts +92 -0
  78. package/template/src/core/company/hooks.ts +40 -0
  79. package/template/src/core/company/password-policy.ts +40 -0
  80. package/template/src/core/company/types.ts +46 -0
  81. package/template/src/core/components/button-link.tsx +14 -0
  82. package/template/src/core/components/confirm-dialog.tsx +116 -0
  83. package/template/src/core/components/data-table/data-table.tsx +297 -0
  84. package/template/src/core/components/data-table/features.ts +24 -0
  85. package/template/src/core/components/data-table/table-header-cell.tsx +13 -0
  86. package/template/src/core/components/data-table/toolbar.tsx +100 -0
  87. package/template/src/core/components/data-table/use-list-url-state.ts +108 -0
  88. package/template/src/core/components/date-picker.tsx +71 -0
  89. package/template/src/core/components/file-attachments.tsx +84 -0
  90. package/template/src/core/components/form-error.tsx +9 -0
  91. package/template/src/core/components/form-page.tsx +93 -0
  92. package/template/src/core/components/image-upload.tsx +86 -0
  93. package/template/src/core/components/page.tsx +74 -0
  94. package/template/src/core/components/phone-input.tsx +58 -0
  95. package/template/src/core/components/search-select.tsx +57 -0
  96. package/template/src/core/components/segmented.tsx +41 -0
  97. package/template/src/core/components/states.tsx +97 -0
  98. package/template/src/core/components/status-badge.tsx +26 -0
  99. package/template/src/core/components/user-avatar.tsx +23 -0
  100. package/template/src/core/config/runtime.ts +14 -0
  101. package/template/src/core/config/types.ts +36 -0
  102. package/template/src/core/i18n/i18n.ts +69 -0
  103. package/template/src/core/i18n/i18next.d.ts +12 -0
  104. package/template/src/core/i18n/keys.ts +37 -0
  105. package/template/src/core/i18n/language-switcher.tsx +45 -0
  106. package/template/src/core/i18n/types.ts +17 -0
  107. package/template/src/core/i18n/use-format.ts +31 -0
  108. package/template/src/core/layout/app-shell.tsx +50 -0
  109. package/template/src/core/layout/app-sidebar.tsx +268 -0
  110. package/template/src/core/layout/command-menu.tsx +117 -0
  111. package/template/src/core/layout/top-bar.tsx +100 -0
  112. package/template/src/core/lib/csv.ts +27 -0
  113. package/template/src/core/lib/format.ts +87 -0
  114. package/template/src/core/lib/phone.ts +50 -0
  115. package/template/src/core/lib/storage.ts +27 -0
  116. package/template/src/core/modules/lazy-page.ts +9 -0
  117. package/template/src/core/modules/navigation.ts +39 -0
  118. package/template/src/core/modules/types.ts +55 -0
  119. package/template/src/core/notifications/hooks.ts +84 -0
  120. package/template/src/core/notifications/notification-center.tsx +251 -0
  121. package/template/src/core/notifications/notifications.service.ts +237 -0
  122. package/template/src/core/notifications/types.ts +65 -0
  123. package/template/src/core/preferences/preferences-store.ts +70 -0
  124. package/template/src/core/theme/brand-context.tsx +53 -0
  125. package/template/src/core/theme/brand.ts +29 -0
  126. package/template/src/core/theme/theme-context.tsx +62 -0
  127. package/template/src/core/theme/theme-options.ts +8 -0
  128. package/template/src/core/theme/theme-toggle.tsx +45 -0
  129. package/template/src/core/users/hooks.ts +51 -0
  130. package/template/src/core/users/types.ts +37 -0
  131. package/template/src/core/users/users.mock.ts +24 -0
  132. package/template/src/core/users/users.service.ts +167 -0
  133. package/template/src/hooks/use-mobile.ts +19 -0
  134. package/template/src/index.css +321 -0
  135. package/template/src/lib/utils.ts +1 -0
  136. package/template/src/locales/en/account.ts +180 -0
  137. package/template/src/locales/en/auth.ts +17 -0
  138. package/template/src/locales/en/common.ts +150 -0
  139. package/template/src/locales/en/company.ts +378 -0
  140. package/template/src/locales/en/dashboard.ts +41 -0
  141. package/template/src/locales/en/errors.ts +18 -0
  142. package/template/src/locales/en/landing.ts +15 -0
  143. package/template/src/locales/en/notifications.ts +43 -0
  144. package/template/src/locales/en/settings.ts +25 -0
  145. package/template/src/locales/index.ts +58 -0
  146. package/template/src/locales/tr/account.ts +184 -0
  147. package/template/src/locales/tr/auth.ts +21 -0
  148. package/template/src/locales/tr/common.ts +153 -0
  149. package/template/src/locales/tr/company.ts +382 -0
  150. package/template/src/locales/tr/dashboard.ts +45 -0
  151. package/template/src/locales/tr/errors.ts +22 -0
  152. package/template/src/locales/tr/landing.ts +19 -0
  153. package/template/src/locales/tr/notifications.ts +47 -0
  154. package/template/src/locales/tr/settings.ts +29 -0
  155. package/template/src/main.tsx +16 -0
  156. package/template/src/modules/locales.ts +14 -0
  157. package/template/src/modules/products/api/products.mock.ts +46 -0
  158. package/template/src/modules/products/api/products.service.ts +84 -0
  159. package/template/src/modules/products/components/product-status.tsx +21 -0
  160. package/template/src/modules/products/hooks.ts +73 -0
  161. package/template/src/modules/products/index.ts +50 -0
  162. package/template/src/modules/products/locales/en.ts +78 -0
  163. package/template/src/modules/products/locales/tr.ts +82 -0
  164. package/template/src/modules/products/pages/product-detail-page.tsx +108 -0
  165. package/template/src/modules/products/pages/product-form-page.tsx +211 -0
  166. package/template/src/modules/products/pages/products-list-page.tsx +286 -0
  167. package/template/src/modules/products/types.ts +61 -0
  168. package/template/src/modules/registry.ts +19 -0
  169. package/template/src/pages/dashboard/dashboard-page.tsx +270 -0
  170. package/template/src/pages/dashboard/dashboard.service.ts +92 -0
  171. package/template/src/pages/landing-page.tsx +79 -0
  172. package/template/src/pages/login-page.tsx +140 -0
  173. package/template/src/pages/no-access-page.tsx +11 -0
  174. package/template/src/pages/not-found-page.tsx +62 -0
  175. package/template/src/pages/settings/account/account.service.ts +147 -0
  176. package/template/src/pages/settings/account/components/mock-qr.tsx +50 -0
  177. package/template/src/pages/settings/account/components/password-strength.tsx +38 -0
  178. package/template/src/pages/settings/account/components/two-factor-dialog.tsx +140 -0
  179. package/template/src/pages/settings/account/hooks.ts +73 -0
  180. package/template/src/pages/settings/account/notifications-page.tsx +103 -0
  181. package/template/src/pages/settings/account/preferences-page.tsx +157 -0
  182. package/template/src/pages/settings/account/profile-page.tsx +242 -0
  183. package/template/src/pages/settings/account/security-page.tsx +364 -0
  184. package/template/src/pages/settings/account/types.ts +37 -0
  185. package/template/src/pages/settings/company/audit-log-page.tsx +126 -0
  186. package/template/src/pages/settings/company/branding-page.tsx +278 -0
  187. package/template/src/pages/settings/company/components/invite-dialog.tsx +106 -0
  188. package/template/src/pages/settings/company/components/read-only-notice.tsx +13 -0
  189. package/template/src/pages/settings/company/danger-zone-page.tsx +127 -0
  190. package/template/src/pages/settings/company/general-page.tsx +232 -0
  191. package/template/src/pages/settings/company/integrations/api-keys-panel.tsx +170 -0
  192. package/template/src/pages/settings/company/integrations/hooks.ts +37 -0
  193. package/template/src/pages/settings/company/integrations/integrations-page.tsx +57 -0
  194. package/template/src/pages/settings/company/integrations/integrations.service.ts +173 -0
  195. package/template/src/pages/settings/company/integrations/settings-forms.tsx +299 -0
  196. package/template/src/pages/settings/company/integrations/webhooks-panel.tsx +179 -0
  197. package/template/src/pages/settings/company/modules-page.tsx +57 -0
  198. package/template/src/pages/settings/company/roles-page.tsx +115 -0
  199. package/template/src/pages/settings/company/security-policies-page.tsx +164 -0
  200. package/template/src/pages/settings/company/users-page.tsx +262 -0
  201. package/template/src/pages/settings/settings-layout.tsx +57 -0
  202. package/template/src/pages/settings/settings-nav.ts +69 -0
  203. package/template/src/pages/settings/settings-routes.tsx +49 -0
  204. package/template/src/providers.tsx +68 -0
  205. package/template/src/router.tsx +54 -0
  206. package/template/tools/check-design.mjs +73 -0
  207. package/template/tools/new-module.mjs +90 -0
  208. package/template/tools/templates/module/api/__id__.mock.ts.tpl +17 -0
  209. package/template/tools/templates/module/api/__id__.service.ts.tpl +51 -0
  210. package/template/tools/templates/module/hooks.ts.tpl +57 -0
  211. package/template/tools/templates/module/index.ts.tpl +37 -0
  212. package/template/tools/templates/module/locales/en.ts.tpl +45 -0
  213. package/template/tools/templates/module/locales/tr.ts.tpl +49 -0
  214. package/template/tools/templates/module/pages/__id__-form-page.tsx.tpl +121 -0
  215. package/template/tools/templates/module/pages/__id__-list-page.tsx.tpl +237 -0
  216. package/template/tools/templates/module/types.ts.tpl +32 -0
  217. package/template/tsconfig.app.json +32 -0
  218. package/template/tsconfig.json +12 -0
  219. package/template/tsconfig.node.json +23 -0
  220. package/template/vite.config.ts +44 -0
@@ -0,0 +1,292 @@
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
+ Services return mock data until a backend is connected: see `BACKEND.md`.
9
+
10
+ ## Rules for AI agents
11
+
12
+ 1. Read this file and `design/` before changing anything.
13
+ 2. New feature = new module: `npm run new-module -- <id> --en "Label" --tr "Etiket"`, then replace its sample
14
+ fields. Use `src/modules/products` as the reference for detail pages and richer forms.
15
+ 3. **Only change project files:** `src/modules/**`, `src/app.config.ts`, `src/app.roles.ts`, `public/**`.
16
+ Everything else (`src/core`, `src/pages`, `src/components/ui`, `src/locales`, `src/index.css`, `design/`,
17
+ `tools/`) belongs to freakpunch and is replaced by updates. If something is missing there, say so to the user
18
+ instead of editing or copying it.
19
+ 4. Every string in both EN and TR. Tokens only. Shared components first (`design/COMPONENTS.md`).
20
+ 5. Gate every route and action with a permission; add project-specific ones in `src/app.roles.ts`.
21
+ 6. `npm run check` must pass before you finish.
22
+
23
+ ## Commands
24
+
25
+ ```bash
26
+ npm run dev # Vite dev server
27
+ npm run build # tsc -b && vite build (must pass with no errors)
28
+ npm run lint # oxlint (must pass with no errors or warnings)
29
+ npm run check # design rules + lint + build: run this before finishing any change
30
+ npm run new-module -- assets --en "Assets" --tr "Zimmet" [--group content]
31
+ npm run fp:update # installed copies: refresh freakpunch's files (add -- --dry-run to preview)
32
+ docker build -t freakpunch . && docker run --rm -p 8080:80 freakpunch
33
+ ```
34
+
35
+ ## Stack
36
+
37
+ Vite · React 19 · TypeScript (strict) · Tailwind CSS v4 · React Router v8 (data router, `react-router`) ·
38
+ TanStack Query v5 · TanStack Table **v9** (`useTable` + `tableFeatures`, not the v8 `useReactTable` API) ·
39
+ React Hook Form + Zod v4 · Recharts (through SortUI `chart`) · Sonner toasts.
40
+
41
+ UI components: **SortUI first** (shadcn-compatible, built on Base UI). **Component source order:** SortUI
42
+ first; if SortUI doesn't have it, the official shadcn/ui component (`npx shadcn@latest add <name>`);
43
+ always adapted to our tokens, never two versions of the same component. No other component libraries.
44
+ - Install: `npx shadcn@latest add @sortui/<name> -y` (component list: https://sortui.dev/docs/components).
45
+ - Files land in `src/components/ui/` and are vendored: avoid editing them; style via `className`
46
+ or tokens. Lint ignores them. The one exception: hard-coded screen-reader/default strings are
47
+ translated in place with `useTranslation()` (dialog, sheet, sidebar, spinner, breadcrumb,
48
+ pagination; keys in `common:a11y`, `common:actions.close`, `common:shell.*`). Re-adding one of those
49
+ with the CLI overwrites the file: reapply the translation.
50
+ - The CLI writes `import { cn } from "cn"`. Rewrite it to `@/lib/utils` after adding:
51
+ `sed -i 's|from "cn"|from "@/lib/utils"|' src/components/ui/*.tsx`
52
+ - Base UI uses `render` instead of `asChild`. A Button rendering a link needs `nativeButton={false}`;
53
+ use `ButtonLink` (`src/core/components/button-link.tsx`) for router links.
54
+ - Theme: we use our own `ThemeProvider` (`src/core/theme/theme-context.tsx`), not `next-themes`.
55
+ `next-themes` stays installed only because the vendored `sonner.tsx` imports it.
56
+
57
+ ## Folder structure
58
+
59
+ ```
60
+ src/
61
+ app.config.ts ← per-project config: name, brand, currency, workspaces, nav groups, modules
62
+ index.css ← THE design-token source (colors, radius, shadows, layout spacing)
63
+ main.tsx providers.tsx router.tsx
64
+ locales/ ← translations: {en,tr}/<namespace>.ts + index.ts (registers every namespace)
65
+ components/ui/ ← SortUI components (vendored, CLI-managed)
66
+ core/ ← identical in every project
67
+ api/ http.ts (fetch client, ApiError, bearer/cookie auth), rest.ts (createRestService,
68
+ toPaginated), mock.ts (delay, paginate, createMockCollection, createMockDocument),
69
+ types.ts (ListParams, Paginated)
70
+ auth/ auth-store (session in localStorage), auth.service (mock, swappable), use-auth,
71
+ require-auth (→ /login?redirect=…), roles, permissions (role → permission map),
72
+ can (Can, useCan, usePermissions), require-permission (route guard), dev-role-switcher
73
+ users/ user model, mock team (one demo user per role), users.service, hooks
74
+ company/ company settings (general, branding, security policies), password-policy, hooks
75
+ audit/ audit.service (record + list), hooks
76
+ notifications/ event catalog + types, notifications.service (feed + settings, mock, per user),
77
+ hooks (polling, optimistic mark-read), notification-center (top-bar bell + panel)
78
+ preferences/ preferences-store (time zone, date format, sidebar default)
79
+ i18n/ i18n init + language detection, keys (I18nKey, useAnyT, vmsg), use-format,
80
+ language-switcher, types (Messages<T>), i18next.d.ts (typed keys)
81
+ components/ page (Page, Panel), states (Loading/Empty/ErrorState), form-page (FormLayout,
82
+ FormSection), form-error, confirm-dialog (useConfirm, typed confirm), status-badge,
83
+ segmented, button-link, user-avatar, image-upload, phone-input, date-picker,
84
+ search-select, file-attachments,
85
+ data-table/ (DataTable, TableHeaderCell, features, toolbar, use-list-url-state)
86
+ layout/ app-shell (canvas + inset panel), app-sidebar, top-bar (breadcrumbs), command-menu (⌘K)
87
+ config/ types (AppConfig, FeatureId, AuthMode), runtime (isFeatureOn, authMode, showLanding)
88
+ modules/ types (AppModule, NavItem…), navigation (nav built from config + modules), lazy-page
89
+ theme/ theme-context (light/dark/system), brand (presets), brand-context, theme-toggle
90
+ lib/ storage (namespaced localStorage), format (createFormatters, initials), phone, csv
91
+ modules/
92
+ registry.ts ← every available module; app.config.ts decides which are on
93
+ products/ ← example module (copy this to start a new one)
94
+ index.ts module definition: nav, routes, quick actions (labels = i18n keys, permissions)
95
+ locales/ en.ts + tr.ts (namespace = module id)
96
+ types.ts entity types + Zod form schema
97
+ api/ products.service.ts (interface + mock impl), products.mock.ts (seed)
98
+ hooks.ts TanStack Query keys + hooks
99
+ components/ pages/
100
+ pages/ core pages: landing, login, dashboard/, not-found, no-access
101
+ settings/ settings-routes, settings-nav, settings-layout (left sub-nav),
102
+ account/ (profile, security, notifications, preferences),
103
+ company/ (general, branding, users, roles, security policies, modules,
104
+ integrations/, audit log, danger zone)
105
+ ```
106
+
107
+ ## Architecture rules
108
+
109
+ - **Screens never call fetch or mocks directly.** Data flows: page → module `hooks.ts` (TanStack Query)
110
+ → `*.service.ts` (typed interface) → mock today / `http` tomorrow.
111
+ - **Swapping to a real API** = implement the service interface with `http` from `@/core/api/http` and
112
+ change the one `export const xService = …` line. Map backend responses into `Paginated<T>` /
113
+ entity types inside the service so screens don't change. Set `VITE_API_URL` for the base URL.
114
+ - **Real JWT auth** = reimplement `AuthService` in `core/auth/auth.service.ts` (call the login
115
+ endpoint, then `authStore.set(session)`). `http` already sends `Authorization: Bearer <token>` and
116
+ clears the session on 401. `RequireAuth` and the screens stay as they are.
117
+ - Mock services use `createMockCollection(name, seed)` so data persists in localStorage
118
+ (`fp:mock:<name>`) and survives reloads. Clear site data to reset.
119
+ - Lists keep state in the URL via `useListUrlState` (`?page=&size=&sort=field.dir&q=&<filter>=`) and
120
+ use **server-side** pagination/sorting (`manualPagination`, `manualSorting`): the service does the
121
+ work, as a backend would.
122
+ - Keep TanStack Table `columns` stable (`useMemo` with stable deps, `mutateAsync` instead of the
123
+ mutation object). Build columns with `createColumnHelper<DataTableFeatures, Row>()`.
124
+ - Forms: React Hook Form + `zodResolver`, SortUI `Field`/`FieldLabel` + `FormFieldError` (translates
125
+ `vmsg` keys), default `onSubmit` validation mode (`onTouched` races with autofocus+submit and drops
126
+ errors). Use `useWatch`, not `watch` (React Compiler lint). Settings forms use
127
+ `FormLayout saveBar="dirty"` (save bar only when dirty).
128
+ - Feedback: `toast` from `sonner`; destructive confirmations via `useConfirm()`; query errors via
129
+ `ErrorState` (404 `ApiError` renders "Not found" with `notFoundAction`).
130
+ - Every page wraps content in `<Page title description actions>` (sets the document title).
131
+ Breadcrumbs come from route `handle: { crumb: 'ns:key' }`; access from `handle.permission`.
132
+ - Toasts appear bottom-center everywhere (`providers.tsx`).
133
+ - Mock services record audit entries (`auditService.record`) when company settings, users or
134
+ integrations change; a real backend writes the audit log server-side.
135
+ - Accessibility: real links for navigation (`Link`/`ButtonLink`), `aria-label` on icon buttons,
136
+ visible focus rings (brand-colored `--ring`), keyboard paths for everything (⌘K, ⌘B sidebar).
137
+
138
+ ## Design system
139
+
140
+ **`design/` is the official design system: read it before building any screen.**
141
+ `design/DESIGN.md` (principles, layout, type, spacing, radius, shadows, color tokens, accent/semantic
142
+ rules), `design/COMPONENTS.md` (which SortUI + shared component for what), `design/PATTERNS.md`
143
+ (list/form/detail/settings page patterns, do's and don'ts).
144
+ - Every new screen must follow `design/` and reuse existing tokens and shared components.
145
+ - Never introduce new colors, radii, shadows or fonts outside the token system in `src/index.css`.
146
+ If something is truly missing, add a token there (light + dark) and document it in `design/DESIGN.md`.
147
+ - Other components may be added following the component source order (SortUI → official shadcn/ui);
148
+ no other component libraries.
149
+
150
+ Minimal, Apple/Revolut-like. No glassmorphism (overlays dim, never blur).
151
+ - **Tokens only.** All colors, radius, shadows and layout spacing are CSS variables in `src/index.css`
152
+ (`:root` + `.dark`), mapped to Tailwind in `@theme inline`. Never hard-code colors in components;
153
+ use `bg-card`, `text-muted-foreground`, `border`, `bg-fill-1/2`, `bg-raised`, `bg-canvas`,
154
+ `text-success/warning/danger/info`, `shadow-(--shadow-card)` etc.
155
+ - Palette: black/white/warm grays + **one brand accent** (`--brand`, default indigo). `--primary`,
156
+ `--ring`, sidebar active icon and chart series 1 all derive from it. Semantic colors only where they
157
+ carry meaning (status dots/badges, deltas).
158
+ - Type: **Inter is the only font.** Small uppercase labels use the `label-caps` utility; aligned numbers
159
+ use `tabular-nums`. There is no monospace font (`font-mono` is disabled).
160
+ - 1px hairline borders, very soft shadows (`--shadow-card`, `--shadow-raised`, `--shadow-panel`),
161
+ 12px radius for cards/controls (`rounded-xl`), 16px for the floating panel (`--radius-panel`).
162
+ - Layout: warm off-white canvas (`--canvas`) with the sidebar directly on it; content in a floating
163
+ white panel (`--background`) inset by `--panel-inset` (matches SortUI inset sidebar `m-2`).
164
+ The active nav item is a raised bordered pill (`bg-raised` + ring + `--shadow-raised`).
165
+
166
+ ## Roles & permissions
167
+
168
+ 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
169
+ demoted, only transfer ownership), **admin** (everything except the danger zone; can't edit the Owner),
170
+ **editor** (create/edit/delete in modules, no company settings), **viewer** (read-only).
171
+ - `core/auth/permissions.ts` is the single role → permission map. Permission strings are
172
+ `<resource>.<action>` (`products.delete`, `settings.company.edit`, `users.manage`,
173
+ `company.transfer`) and are meant to be reused 1:1 as Spring Security authorities. Every registered
174
+ module automatically gets `<id>.view|create|edit|delete`.
175
+ - UI: `useCan('x')` / `usePermissions().can` / `<Can permission="x">` (hide, or render-prop to
176
+ disable). Nav items, quick actions and settings nav take `permission`. Routes take
177
+ `handle.permission`; `RequirePermissions` in the shell redirects to `/app/no-access`.
178
+ Owner rules for user management: `canManageUser(actor, target)`.
179
+ - **UI checks are not security.** The backend must enforce every permission (the mock services
180
+ enforce the Owner rules the same way a backend should).
181
+ - Demo users (any password): owner@, admin@, editor@, viewer@freakpunch.site. In `npm run dev` the top
182
+ bar has a role switcher (`DevRoleSwitcher`, code-split behind `import.meta.env.DEV`, absent from
183
+ production builds).
184
+
185
+ ## Settings
186
+
187
+ `/app/settings` has a left sub-navigation inside the panel (horizontal scroller on mobile).
188
+ "My account" (profile, security, notifications, preferences) is for everyone; the "Company" group needs
189
+ `settings.company.view` (Owner/Admin) and the danger zone `company.delete` (Owner). Add a page: a
190
+ route in `settings-routes.tsx` (crumb + permission) and an item in `settings-nav.ts` (same permission).
191
+ Company forms are read-only (`FormLayout readOnly`) without `settings.company.edit`.
192
+
193
+ ## Internationalization (EN + TR)
194
+
195
+ react-i18next. English is the default and fallback; Turkish is a full second language.
196
+ **Every feature ships with both EN and TR strings in the same change. No user-facing text in
197
+ components**: page titles, labels, `aria-label`, `placeholder`, toasts, empty states, validation.
198
+ - Files: `src/locales/{en,tr}/<namespace>.ts` (common, auth, landing, errors, dashboard, settings,
199
+ account, company, notifications), registered in `src/locales/index.ts`, and module-owned
200
+ `src/modules/<module>/locales/{en,tr}.ts`, registered in `src/modules/locales.ts`.
201
+ - Locale files are TS objects. `tr` files are typed `Messages<typeof en>`, so a missing/extra Turkish
202
+ key fails `npm run build`. Keys are typed (`src/core/i18n/i18next.d.ts`): `t('typo')` doesn't compile.
203
+ - Add a string: add the key to `en/<ns>.ts`, then the same key to `tr/<ns>.ts` (correct Turkish
204
+ characters: ç ğ ı İ ö ş ü). Use `{{name}}` interpolation, `_one`/`_other` plurals with `count`,
205
+ `<b>…</b>` + `<Trans components>` for inline markup. Never concatenate translated fragments.
206
+ - Add a namespace: create `en/<ns>.ts` + `tr/<ns>.ts` and register both in `src/locales/index.ts`
207
+ (modules: `locales/` and `src/modules/locales.ts`), use `useTranslation(['<ns>', 'common'])` and `t('common:actions.save')`
208
+ for shared strings.
209
+ - Config-like data (nav labels, crumbs, quick actions, nav groups, workspace plans) stores
210
+ `I18nKey` values (`'products:nav'`) and is translated with `useAnyT()`.
211
+ - Zod messages are keys: `vmsg('validation.minChars', { min: 2 })`; render errors with
212
+ `FormFieldError` (translates at render time).
213
+ - Formatting: `const fmt = useFormat()` → `fmt.currency/number/date/dateTime/relative/…` (Intl with the
214
+ active locale, user timezone/date format, company currency). Pass `fmt` and `t` as `useMemo` deps
215
+ of table columns.
216
+ - Language: `LanguageSwitcher` (landing, login, top bar) and Settings → Preferences. Stored in
217
+ `localStorage` `fp:lang`; first visit uses TR if the browser language is Turkish, else EN.
218
+ `<html lang>` follows the active language.
219
+ - Vendored SortUI screen-reader strings (dialog/sheet "Close", mobile sidebar title, "Toggle sidebar",
220
+ spinner "Loading", pagination) are translated in the vendored files (see Stack). Our dialogs still use
221
+ `showCloseButton={false}` + a footer Cancel.
222
+
223
+ ## Notifications
224
+
225
+ Top-bar bell (`core/notifications/notification-center.tsx`): unread badge (brand color, hidden at 0,
226
+ "9+" above 9) and a panel (popover on desktop, bottom sheet on mobile) with All / Unread tabs, Today /
227
+ Earlier groups, mark one / all as read, and a link to Settings → Notifications.
228
+ - One event catalog (`core/notifications/types.ts` → `notificationEvents`) drives both the settings
229
+ toggles and the feed. Every notification has an `event`; events with in-app off are not returned.
230
+ Add an event there + its label in `account:notifications.events` (EN + TR).
231
+ - The API sends `template` + `params`, never rendered text; the client renders
232
+ `notifications:templates.<template>.title|body` in the active language (roles, money and numbers are
233
+ localized at render time). Add a template to `notificationTemplates` and both locale files.
234
+ - Data: `notificationsService` (per signed-in user; the mock seeds a different feed per role and
235
+ occasionally adds a new notification) → `useNotifications()` polls every 30 s. For a real backend
236
+ implement the interface with `http` (`GET /me/notifications`, `POST …/{id}/read`, `POST …/read-all`,
237
+ `GET|PUT /me/notification-settings`) and optionally `subscribe()` for SSE/WebSocket push; the hooks
238
+ already invalidate on pushed events and keep polling as a fallback.
239
+
240
+ ## How to add a module
241
+
242
+ `npm run new-module -- orders --en "Orders" --tr "Siparişler"` creates `src/modules/orders` (list with search,
243
+ filters, bulk delete and CSV export; create/edit form; mock data; REST switch; EN + TR) and registers it in
244
+ `registry.ts`, `locales.ts` and `app.config.ts`. Replace the sample fields. It leaves the `// fp:*` marker
245
+ comments in those files in place; keep them. What it sets up, if you need to do it by hand:
246
+
247
+ 1. Module folder with types, service, hooks, pages and `locales/`.
248
+ 2. `<name>/locales/en.ts` + `tr.ts` registered in `src/modules/locales.ts` as namespace `<name>`.
249
+ 3. In `<name>/index.ts` export an `AppModule` (labels/crumbs are translation keys):
250
+ ```ts
251
+ export const ordersModule: AppModule = {
252
+ id: 'orders',
253
+ nav: [{ label: 'orders:nav', path: '/app/orders', icon: ShoppingCart, group: 'catalog',
254
+ useBadge: usePendingCount, permission: 'orders.view' }],
255
+ quickActions: [{ label: 'orders:quickActions.new', path: '/app/orders/new', icon: Plus, permission: 'orders.create' }],
256
+ routes: [{ path: 'orders', handle: { crumb: 'orders:crumbs.list', permission: 'orders.view' }, children: [
257
+ { index: true, lazy: lazyPage(() => import('./pages/orders-list-page')) },
258
+ { path: 'new', handle: { crumb: 'orders:crumbs.new', permission: 'orders.create' }, lazy: … },
259
+ ] }],
260
+ }
261
+ ```
262
+ Routes are relative to `/app` and render inside the shell; pages are default exports, lazy-loaded.
263
+ Gate buttons with `<Can permission="orders.delete">`.
264
+ 4. Register it in `src/modules/registry.ts` (`orders: ordersModule`); this also creates the
265
+ `orders.view|create|edit|delete` permissions and their row in the roles matrix.
266
+ 5. Enable it in `src/app.config.ts` → `modules: { orders: true }`. Nav groups come from
267
+ `appConfig.navGroups` (`group` must match an id); empty groups are hidden.
268
+ 6. The sidebar, ⌘K menu, dashboard quick actions, routes and Settings → Modules pick it up automatically.
269
+
270
+ Extras: `dashboardWidgets: [Component]` on the module adds cards to the dashboard. Extra permissions
271
+ (`orders.refund`) and extra roles (`hr`) go in `src/app.roles.ts`, with their EN + TR labels.
272
+
273
+ ## How to change the brand color
274
+
275
+ - Per project: set `brand` in `src/app.config.ts` to a preset key from `src/core/theme/brand.ts`
276
+ (`indigo`, `blue`, `violet`, `emerald`, `orange`, `rose`, `graphite`) or a custom
277
+ `{ color: 'oklch(…)', foreground: 'oklch(1 0 0)' }`. Also update the `--brand` default in
278
+ `src/index.css` to the same value so the first paint matches.
279
+ - Add presets in `brandPresets` (`brand.ts`) and a label in `company:branding.presets` (EN + TR); they appear in Settings → Company → Branding.
280
+ - Settings → Company → Branding previews the brand live (`previewBrand`) and saves it for the company (cached in localStorage `fp:brand` for first paint).
281
+
282
+ ## Deployment & indexing
283
+
284
+ Backend connection, auth modes and serving under `/admin` (`VITE_BASE_PATH`) are in `BACKEND.md`.
285
+
286
+ - `Dockerfile`: node:24-alpine build → nginx:alpine on port 80 with `nginx.conf`.
287
+ - `nginx.conf`: SPA fallback (`try_files … /index.html`) so deep links work on refresh; immutable cache
288
+ for `/assets/`, `no-cache` for `index.html`; `X-Robots-Tag: noindex, nofollow` on every response
289
+ (repeated inside locations that set their own headers).
290
+ - Search engines must not index the site: keep the `robots` meta tag in `index.html`,
291
+ `public/robots.txt` (`Disallow: /`) and the nginx header.
292
+ - `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: 'acme'` 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.