robodev 0.47.2 → 0.48.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 (172) hide show
  1. package/package.json +3 -3
  2. package/src/emit-starter.test.ts +6 -3
  3. package/templates/auth-chat/ROBODEV.md +7 -1
  4. package/templates/auth-chat/package.json +2 -2
  5. package/templates/backend/ROBODEV.md +7 -1
  6. package/templates/backend/package.json +2 -2
  7. package/templates/catalog.json +5 -0
  8. package/templates/empty/ROBODEV.md +7 -1
  9. package/templates/empty/package.json +2 -2
  10. package/templates/marketplace/ROBODEV.md +7 -1
  11. package/templates/marketplace/package.json +2 -2
  12. package/templates/povio/.config/local.spa.template.yml +27 -0
  13. package/templates/povio/.oxlint-base.json +29 -0
  14. package/templates/povio/.oxlintrc.json +78 -0
  15. package/templates/povio/.rulesync/rules/frontend-api-boundary.md +36 -0
  16. package/templates/povio/.rulesync/rules/frontend-component-structure.md +40 -0
  17. package/templates/povio/.rulesync/rules/frontend-forms.md +27 -0
  18. package/templates/povio/.rulesync/rules/frontend-notifications.md +24 -0
  19. package/templates/povio/.rulesync/rules/frontend-povio-components.md +42 -0
  20. package/templates/povio/.rulesync/rules/frontend-povio-ui.md +45 -0
  21. package/templates/povio/.rulesync/rules/frontend-query-autocomplete.md +24 -0
  22. package/templates/povio/.rulesync/rules/frontend-tables-and-lists.md +26 -0
  23. package/templates/povio/.rulesync/rules/frontend-translations.md +25 -0
  24. package/templates/povio/.rulesync/rules/project-overview.md +32 -0
  25. package/templates/povio/.rulesync/rules/robodev-api.md +30 -0
  26. package/templates/povio/.rulesync/rules/robodev-auth.md +40 -0
  27. package/templates/povio/.rulesync/rules/robodev-database.md +22 -0
  28. package/templates/povio/.rulesync/rules/role-based-app-structure.md +36 -0
  29. package/templates/povio/.rulesync/skills/media-feature/SKILL.md +21 -0
  30. package/templates/povio/.rulesync/skills/povio-ui-styling/SKILL.md +211 -0
  31. package/templates/povio/.rulesync/skills/povio-ui-styling/agents/openai.yaml +4 -0
  32. package/templates/povio/.rulesync/skills/robodev-api-route/SKILL.md +13 -0
  33. package/templates/povio/.rulesync/skills/robodev-table/SKILL.md +12 -0
  34. package/templates/povio/README.md +40 -0
  35. package/templates/povio/ROBODEV.md +84 -0
  36. package/templates/povio/apps/fe/index.html +24 -0
  37. package/templates/povio/apps/fe/openapi-codegen.config.ts +72 -0
  38. package/templates/povio/apps/fe/package.json +71 -0
  39. package/templates/povio/apps/fe/public/apple-touch-icon.png +0 -0
  40. package/templates/povio/apps/fe/public/favicon-96x96.png +0 -0
  41. package/templates/povio/apps/fe/public/favicon.ico +0 -0
  42. package/templates/povio/apps/fe/public/favicon.svg +3 -0
  43. package/templates/povio/apps/fe/public/site.webmanifest +21 -0
  44. package/templates/povio/apps/fe/public/web-app-manifest-192x192.png +0 -0
  45. package/templates/povio/apps/fe/public/web-app-manifest-512x512.png +0 -0
  46. package/templates/povio/apps/fe/src/assets/locales/en/translation.json +166 -0
  47. package/templates/povio/apps/fe/src/clients/app-rest-client.ts +15 -0
  48. package/templates/povio/apps/fe/src/clients/auth-token-store.ts +101 -0
  49. package/templates/povio/apps/fe/src/clients/rest/app-error-handler.ts +31 -0
  50. package/templates/povio/apps/fe/src/clients/rest/interceptors/authorization-header.interceptor.ts +15 -0
  51. package/templates/povio/apps/fe/src/clients/rest/interceptors/refresh-token.interceptor.ts +45 -0
  52. package/templates/povio/apps/fe/src/clients/rest/interceptors/response.interceptor.ts +17 -0
  53. package/templates/povio/apps/fe/src/components/404.tsx +18 -0
  54. package/templates/povio/apps/fe/src/components/features/auth/AuthBrandPanel.tsx +60 -0
  55. package/templates/povio/apps/fe/src/components/features/auth/AuthLayout.tsx +23 -0
  56. package/templates/povio/apps/fe/src/components/features/auth/LoginPage.tsx +111 -0
  57. package/templates/povio/apps/fe/src/components/features/auth/RegisterPage.tsx +143 -0
  58. package/templates/povio/apps/fe/src/components/features/home/HomePage.tsx +21 -0
  59. package/templates/povio/apps/fe/src/components/features/profile/ProfilePage.tsx +96 -0
  60. package/templates/povio/apps/fe/src/components/layout/AppLayout.tsx +19 -0
  61. package/templates/povio/apps/fe/src/components/layout/app-header/AppHeader.tsx +91 -0
  62. package/templates/povio/apps/fe/src/components/layout/app-header/MobileNavigation.tsx +51 -0
  63. package/templates/povio/apps/fe/src/components/layout/app-header/NavLink.tsx +29 -0
  64. package/templates/povio/apps/fe/src/components/shared/branding/BrandLogo.tsx +13 -0
  65. package/templates/povio/apps/fe/src/components/shared/error/ErrorFallback.tsx +43 -0
  66. package/templates/povio/apps/fe/src/components/shared/error/ErrorText.tsx +20 -0
  67. package/templates/povio/apps/fe/src/components/shared/error/NotFound.tsx +36 -0
  68. package/templates/povio/apps/fe/src/components/shared/forms/RequiredLabel.tsx +21 -0
  69. package/templates/povio/apps/fe/src/components/shared/forms/RowInputWrapper.tsx +50 -0
  70. package/templates/povio/apps/fe/src/components/shared/head/AppHead.tsx +32 -0
  71. package/templates/povio/apps/fe/src/components/shared/head/DefaultAppHead.tsx +34 -0
  72. package/templates/povio/apps/fe/src/components/shared/layout/LoadingState.tsx +9 -0
  73. package/templates/povio/apps/fe/src/components/shared/layout/ThinPageWrapper.tsx +5 -0
  74. package/templates/povio/apps/fe/src/components/shared/page/PageHeader.tsx +69 -0
  75. package/templates/povio/apps/fe/src/components/shared/ui/Card.tsx +60 -0
  76. package/templates/povio/apps/fe/src/components/shared/ui/GoogleLoginButton.tsx +19 -0
  77. package/templates/povio/apps/fe/src/components/shared/ui/RequiredLabel.tsx +22 -0
  78. package/templates/povio/apps/fe/src/components/shared/ui/TableActions.tsx +22 -0
  79. package/templates/povio/apps/fe/src/config/app.config.ts +23 -0
  80. package/templates/povio/apps/fe/src/config/i18n.ts +37 -0
  81. package/templates/povio/apps/fe/src/config/inits/logger.ts +7 -0
  82. package/templates/povio/apps/fe/src/config/jwt.config.ts +2 -0
  83. package/templates/povio/apps/fe/src/config/query.config.ts +20 -0
  84. package/templates/povio/apps/fe/src/hooks/useAuth.ts +5 -0
  85. package/templates/povio/apps/fe/src/main.tsx +52 -0
  86. package/templates/povio/apps/fe/src/pages/(guest)/login.tsx +23 -0
  87. package/templates/povio/apps/fe/src/pages/(guest)/register.tsx +23 -0
  88. package/templates/povio/apps/fe/src/pages/(guest)/route.tsx +18 -0
  89. package/templates/povio/apps/fe/src/pages/(private)/index.tsx +23 -0
  90. package/templates/povio/apps/fe/src/pages/(private)/profile.tsx +23 -0
  91. package/templates/povio/apps/fe/src/pages/(private)/route.tsx +18 -0
  92. package/templates/povio/apps/fe/src/pages/(public)/auth.tsx +37 -0
  93. package/templates/povio/apps/fe/src/pages/(public)/route.tsx +9 -0
  94. package/templates/povio/apps/fe/src/pages/__root.tsx +133 -0
  95. package/templates/povio/apps/fe/src/providers/AppErrorBoundary.tsx +44 -0
  96. package/templates/povio/apps/fe/src/providers/OpenApiRuntimeProvider.tsx +31 -0
  97. package/templates/povio/apps/fe/src/providers/index.tsx +44 -0
  98. package/templates/povio/apps/fe/src/providers/jwt.provider.tsx +95 -0
  99. package/templates/povio/apps/fe/src/routeTree.gen.ts +225 -0
  100. package/templates/povio/apps/fe/src/styles/base.css +84 -0
  101. package/templates/povio/apps/fe/src/styles/fonts/fonts.tsx +10 -0
  102. package/templates/povio/apps/fe/src/styles/globals.css +26 -0
  103. package/templates/povio/apps/fe/src/styles/overrides/defaults/button.override.ts +536 -0
  104. package/templates/povio/apps/fe/src/styles/overrides/defaults/checkbox.override.ts +71 -0
  105. package/templates/povio/apps/fe/src/styles/overrides/defaults/input.override.ts +252 -0
  106. package/templates/povio/apps/fe/src/styles/overrides/defaults/label.override.ts +91 -0
  107. package/templates/povio/apps/fe/src/styles/overrides/defaults/modal.override.ts +62 -0
  108. package/templates/povio/apps/fe/src/styles/overrides/defaults/radio.override.ts +46 -0
  109. package/templates/povio/apps/fe/src/styles/overrides/defaults/table.override.ts +104 -0
  110. package/templates/povio/apps/fe/src/styles/overrides/defaults/tag.override.ts +66 -0
  111. package/templates/povio/apps/fe/src/styles/overrides/defaults/typography.override.ts +115 -0
  112. package/templates/povio/apps/fe/src/styles/overrides/outline.clsx.ts +10 -0
  113. package/templates/povio/apps/fe/src/styles/overrides/uiOverrides.override.ts +60 -0
  114. package/templates/povio/apps/fe/src/styles/theme.css +2177 -0
  115. package/templates/povio/apps/fe/src/types/i18next.d.ts +12 -0
  116. package/templates/povio/apps/fe/src/types/table.d.ts +17 -0
  117. package/templates/povio/apps/fe/src/types/ui.d.ts +26 -0
  118. package/templates/povio/apps/fe/src/types/vite-env.d.ts +13 -0
  119. package/templates/povio/apps/fe/src/utils/date.utils.ts +17 -0
  120. package/templates/povio/apps/fe/src/utils/image-fallback.ts +9 -0
  121. package/templates/povio/apps/fe/src/utils/number.utils.ts +12 -0
  122. package/templates/povio/apps/fe/src/utils/string.utils.ts +5 -0
  123. package/templates/povio/apps/fe/src/vite-env.d.ts +1 -0
  124. package/templates/povio/apps/fe/tsconfig.app.json +32 -0
  125. package/templates/povio/apps/fe/tsconfig.json +9 -0
  126. package/templates/povio/apps/fe/tsconfig.node.json +31 -0
  127. package/templates/povio/apps/fe/vite.config.ts +150 -0
  128. package/templates/povio/openapi.json +911 -0
  129. package/templates/povio/oxfmt.config.js +23 -0
  130. package/templates/povio/package.json +19 -0
  131. package/templates/povio/robodev/api/_lib.ts +99 -0
  132. package/templates/povio/robodev/api/health.ts +7 -0
  133. package/templates/povio/robodev/database.ts +28 -0
  134. package/templates/povio/robodev/package.json +4 -0
  135. package/templates/povio/rulesync.jsonc +18 -0
  136. package/templates/povio/tsconfig.json +11 -0
  137. package/templates/shadcn/.rulesync/rules/frontend-api-boundary.md +29 -0
  138. package/templates/shadcn/.rulesync/rules/frontend-component-structure.md +19 -0
  139. package/templates/shadcn/.rulesync/rules/frontend-hosted-ui.md +38 -0
  140. package/templates/shadcn/.rulesync/rules/project-overview.md +29 -0
  141. package/templates/shadcn/.rulesync/rules/robodev-api.md +22 -0
  142. package/templates/shadcn/.rulesync/rules/robodev-auth.md +47 -0
  143. package/templates/shadcn/.rulesync/rules/robodev-database.md +21 -0
  144. package/templates/shadcn/.rulesync/skills/robodev-crud-page/SKILL.md +181 -0
  145. package/templates/shadcn/README.md +9 -4
  146. package/templates/shadcn/ROBODEV.md +14 -6
  147. package/templates/shadcn/package.json +4 -6
  148. package/templates/shadcn/rulesync.jsonc +18 -0
  149. package/templates/shadcn/src/App.tsx +12 -3
  150. package/templates/shadcn/src/app.css +359 -0
  151. package/templates/shadcn/src/components/auth/AuthCard.tsx +29 -0
  152. package/templates/shadcn/src/components/ui/badge.tsx +17 -0
  153. package/templates/shadcn/src/components/ui/card.tsx +28 -0
  154. package/templates/shadcn/src/components/ui/dialog.tsx +38 -0
  155. package/templates/shadcn/src/components/ui/empty-state.tsx +17 -0
  156. package/templates/shadcn/src/components/ui/input.tsx +8 -0
  157. package/templates/shadcn/src/components/ui/label.tsx +8 -0
  158. package/templates/shadcn/src/components/ui/skeleton.tsx +6 -0
  159. package/templates/shadcn/src/components/ui/spinner.tsx +3 -0
  160. package/templates/shadcn/src/components/ui/table.tsx +30 -0
  161. package/templates/shadcn/src/components/ui/textarea.tsx +8 -0
  162. package/templates/shadcn/src/lib/auth.tsx +72 -0
  163. package/templates/shadcn/src/lib/client.ts +8 -0
  164. package/templates/shadcn/src/pages/Home.tsx +15 -0
  165. package/templates/shadcn/src/pages/Login.tsx +70 -0
  166. package/templates/shadcn/src/pages/NotFound.tsx +14 -0
  167. package/templates/shadcn/src/pages/Register.tsx +82 -0
  168. package/templates/space/ROBODEV.md +7 -1
  169. package/templates/space/package.json +2 -2
  170. package/templates/shadcn/components.json +0 -21
  171. package/templates/shadcn/postcss.config.js +0 -6
  172. package/templates/shadcn/tailwind.config.ts +0 -42
@@ -22,6 +22,12 @@ export const get = defineApi({
22
22
  });
23
23
  ```
24
24
 
25
+ Method exports: one export per method — `get` / `post` / `put` / `patch` / `delete`. `delete` is a reserved word — write `export const del = ...` or `export { remove as delete }`; both serve DELETE.
26
+
27
+ `ctx.user.id` is a string like `usr_abc123`, not a UUID. Type user id columns as `text`, never `uuid`.
28
+
29
+ Throw an Error with an integer `statusCode` (400-599) to answer with that status: `throw Object.assign(new Error("Not found"), { statusCode: 404 })`. Any other throw is a generic 500.
30
+
25
31
  ## Hard rules
26
32
 
27
33
  - Do not run npm, pnpm, or yarn install.
@@ -34,7 +40,7 @@ export const get = defineApi({
34
40
 
35
41
  ## Auth and product data
36
42
 
37
- Persist primary product data with `defineApi` and `robodev-publish be`. Do not use localStorage as the product store unless the user asked for a public, static, or no-account page. Auth is on by default: add login and register and protect routes unless the product is public or unauthenticated. Use `auth: "required"` and `ctx.user.id`. Call reserved `/api/user/auth/*` — do not implement `api/user/**`. Tiny: reuse seed login/register and AuthGuard. Shadcn, Empty, and legacy: create login/register with `@robodev-ai/client` tokens. A missing `.rulesync` skill is not a reason to skip APIs. Do not require `robodev-auth set` every run.
43
+ Persist primary product data with `defineApi` and `robodev-publish be`. Do not use localStorage as the product store unless the user asked for a public, static, or no-account page. Auth is on by default: add login and register and protect routes unless the product is public or unauthenticated. Use `auth: "required"` and `ctx.user.id`. Call reserved `/api/user/auth/*` — do not implement `api/user/**`. Tiny and Shadcn: reuse seed login/register and AuthGuard. Empty and legacy: create login/register with `@robodev-ai/client` tokens. A missing `.rulesync` skill is not a reason to skip APIs. Do not require `robodev-auth set` every run.
38
44
 
39
45
  ## Backend
40
46
 
@@ -69,14 +75,16 @@ This is the hosted Shadcn layout. Keep it. Do not convert this tree to Tiny (`pa
69
75
  - Schema: `robodev/database.ts` (`defineDatabase` from `@robodev-ai/sdk`)
70
76
  - HTTP handlers: `robodev/api/**/*.ts` (`defineApi` from `@robodev-ai/sdk`)
71
77
  - Optional jobs: `robodev/jobs/**/*.ts`. Optional sockets: `robodev/sockets/**/*.ts` (`defineSocket`). Optional runtime packages go in `robodev/package.json` (allowlisted exact semver only). Do not npm install in this runner — Starbase installs on publish.
72
- - Frontend: root `index.html` + `src/**` (React, React Router, shadcn, generated orval client)
78
+ - Frontend: root `index.html` + `src/**` (React + React Router, hosted UI kit, `@robodev-ai/client`)
73
79
  - Auth is reserved at `/api/user/*`. Do not add `robodev/api/user/**`.
80
+ - AI rules and skills live in `.rulesync/` — read `.rulesync/rules/project-overview.md` first; `.rulesync/skills/robodev-crud-page/SKILL.md` is the CRUD recipe.
74
81
 
75
82
  Do not write a `frontend` block in `robodev/.robodev` — that block is for Tiny `apps/fe`. Root `index.html` + `src/` deploy as hosted React.
76
83
 
77
- - Route with React Router in `src/`. `src/pages/Home.tsx` is `/`. Add a page by adding a component and a `<Route>` in `src/App.tsx`.
78
- - Call APIs through the committed orval client under `src/api/generated`. After API changes, update `src/api/openapi.json` and regenerate.
79
- - UI: shadcn under `src/components/ui`. Hosted compile does not run Tailwind. Keep hosted CSS in `src/app.css`. `tailwind.config.ts` and `postcss.config.js` are for local `shadcn add` / Vite only.
84
+ - Route with React Router in `src/`. `src/pages/Home.tsx` is `/`. Add a page by adding a component and a `<Route>` in `src/App.tsx`. `src/pages/NotFound.tsx` is the `*` catch-all.
85
+ - Auth is seeded: `src/lib/client.ts`, `src/lib/auth.tsx` (`AuthProvider`, `useAuth`, `AuthGuard`), `src/pages/Login.tsx`, `src/pages/Register.tsx`. Reuse them; protect routes with `AuthGuard`. Never reimplement token handling.
86
+ - Call app APIs through `client` from `@/lib/client` (`client.get/post/put/patch/del`) — it sends the auth header and refreshes on 401. The committed orval client under `src/api/generated/` serves the seed `getHealth` only; it does not send auth headers, and this runner cannot regenerate it (no node_modules).
87
+ - UI: hosted kit under `src/components/ui` (button, input, textarea, label, card, badge, table, dialog, skeleton, spinner, empty-state). Hosted compile does not run Tailwind — there is no Tailwind in this tree. Every class must be defined in `src/app.css` (tokens + `.page`, `.stack`, `.row`, `.field`, and component classes). See `.rulesync/rules/frontend-hosted-ui.md`.
80
88
  - Hosted FE packages: `react`, `react-dom`, `react-router-dom`, `@robodev-ai/client`. Do not import other npm packages in `src/` — hosted compile will fail.
81
89
 
82
- Allowed paths: `robodev/database.ts`, `robodev/api/**/*.ts`, `robodev/jobs/**/*.ts`, `robodev/sockets/**/*.ts`, `robodev/hooks/**/*.ts`, `robodev/package.json`, `index.html`, `src/**`, `package.json`, `README.md`, `ROBODEV.md`, `.gitignore`.
90
+ Allowed paths: `robodev/database.ts`, `robodev/api/**/*.ts`, `robodev/jobs/**/*.ts`, `robodev/sockets/**/*.ts`, `robodev/hooks/**/*.ts`, `robodev/package.json`, `index.html`, `src/**`, `.rulesync/**`, `package.json`, `README.md`, `ROBODEV.md`, `.gitignore`.
@@ -11,19 +11,17 @@
11
11
  "generate:api": "orval"
12
12
  },
13
13
  "dependencies": {
14
- "@robodev-ai/sdk": "^0.13.0",
14
+ "@robodev-ai/sdk": "^0.14.0",
15
15
  "@robodev-ai/client": "^0.5.0",
16
16
  "react": "^18.3.1",
17
17
  "react-dom": "^18.3.1",
18
18
  "react-router-dom": "^6.30.1"
19
19
  },
20
20
  "devDependencies": {
21
- "autoprefixer": "^10.4.21",
21
+ "@types/react": "^18.3.12",
22
+ "@types/react-dom": "^18.3.1",
22
23
  "orval": "^7.11.2",
23
- "postcss": "^8.5.6",
24
- "robodev": "^0.47.2",
25
- "shadcn": "^2.10.0",
26
- "tailwindcss": "^3.4.17",
24
+ "robodev": "^0.48.0",
27
25
  "typescript": "^5.9.2",
28
26
  "vite": "^6.0.0"
29
27
  }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://github.com/dyoshikawa/rulesync/releases/latest/download/config-schema.json",
3
+ "targets": {
4
+ "cursor": ["rules", "skills"],
5
+ "claudecode": ["rules", "skills"],
6
+ "codexcli": ["rules", "skills"],
7
+ },
8
+ "outputRoots": ["."],
9
+ // Clean stale outputs when source rules are removed. All sources live under .rulesync/.
10
+ "delete": true,
11
+ "verbose": false,
12
+ "silent": false,
13
+ "global": false,
14
+ "simulateCommands": false,
15
+ "simulateSubagents": false,
16
+ "simulateSkills": false,
17
+ "gitignoreTargetsOnly": true,
18
+ }
@@ -1,10 +1,19 @@
1
1
  import { Route, Routes } from "react-router-dom";
2
+ import { AuthProvider } from "./lib/auth";
2
3
  import { Home } from "./pages/Home";
4
+ import { Login } from "./pages/Login";
5
+ import { NotFound } from "./pages/NotFound";
6
+ import { Register } from "./pages/Register";
3
7
 
4
8
  export function App() {
5
9
  return (
6
- <Routes>
7
- <Route path="/" element={<Home />} />
8
- </Routes>
10
+ <AuthProvider>
11
+ <Routes>
12
+ <Route path="/" element={<Home />} />
13
+ <Route path="/login" element={<Login />} />
14
+ <Route path="/register" element={<Register />} />
15
+ <Route path="*" element={<NotFound />} />
16
+ </Routes>
17
+ </AuthProvider>
9
18
  );
10
19
  }
@@ -1,6 +1,10 @@
1
1
  :root {
2
2
  --background: 0 0% 100%;
3
3
  --foreground: 240 10% 3.9%;
4
+ --card: 0 0% 100%;
5
+ --card-foreground: 240 10% 3.9%;
6
+ --popover: 0 0% 100%;
7
+ --popover-foreground: 240 10% 3.9%;
4
8
  --primary: 240 5.9% 10%;
5
9
  --primary-foreground: 0 0% 98%;
6
10
  --secondary: 240 4.8% 95.9%;
@@ -12,12 +16,36 @@
12
16
  --destructive: 0 84.2% 60.2%;
13
17
  --destructive-foreground: 0 0% 98%;
14
18
  --border: 240 5.9% 90%;
19
+ --input: 240 5.9% 90%;
15
20
  --ring: 240 5.9% 10%;
16
21
  --radius: 0.5rem;
17
22
  color: hsl(var(--foreground));
18
23
  font-family: system-ui, sans-serif;
19
24
  }
20
25
 
26
+ .dark {
27
+ --background: 240 10% 3.9%;
28
+ --foreground: 0 0% 98%;
29
+ --card: 240 10% 3.9%;
30
+ --card-foreground: 0 0% 98%;
31
+ --popover: 240 10% 3.9%;
32
+ --popover-foreground: 0 0% 98%;
33
+ --primary: 0 0% 98%;
34
+ --primary-foreground: 240 5.9% 10%;
35
+ --secondary: 240 3.7% 15.9%;
36
+ --secondary-foreground: 0 0% 98%;
37
+ --muted: 240 3.7% 15.9%;
38
+ --muted-foreground: 240 5% 64.9%;
39
+ --accent: 240 3.7% 15.9%;
40
+ --accent-foreground: 0 0% 98%;
41
+ --destructive: 0 62.8% 30.6%;
42
+ --destructive-foreground: 0 0% 98%;
43
+ --border: 240 3.7% 15.9%;
44
+ --input: 240 3.7% 15.9%;
45
+ --ring: 240 4.9% 83.9%;
46
+ color: hsl(var(--foreground));
47
+ }
48
+
21
49
  body {
22
50
  margin: 0;
23
51
  background: hsl(var(--background));
@@ -31,6 +59,50 @@ main {
31
59
  padding: 2rem;
32
60
  }
33
61
 
62
+ /* Layout */
63
+
64
+ .page {
65
+ width: 100%;
66
+ max-width: 64rem;
67
+ margin: 0 auto;
68
+ padding: 2rem 1.5rem;
69
+ box-sizing: border-box;
70
+ }
71
+
72
+ .page-header {
73
+ display: flex;
74
+ align-items: center;
75
+ justify-content: space-between;
76
+ gap: 1rem;
77
+ margin-bottom: 1.5rem;
78
+ }
79
+
80
+ .page-title {
81
+ margin: 0;
82
+ font-size: 1.5rem;
83
+ font-weight: 600;
84
+ letter-spacing: -0.025em;
85
+ }
86
+
87
+ .stack {
88
+ display: flex;
89
+ flex-direction: column;
90
+ gap: 1rem;
91
+ }
92
+
93
+ .row {
94
+ display: flex;
95
+ align-items: center;
96
+ gap: 0.5rem;
97
+ }
98
+
99
+ .muted {
100
+ color: hsl(var(--muted-foreground));
101
+ font-size: 0.875rem;
102
+ }
103
+
104
+ /* Buttons */
105
+
34
106
  .btn {
35
107
  display: inline-flex;
36
108
  align-items: center;
@@ -78,6 +150,11 @@ main {
78
150
  color: hsl(var(--foreground));
79
151
  }
80
152
 
153
+ .btn-outline:hover {
154
+ background: hsl(var(--accent));
155
+ color: hsl(var(--accent-foreground));
156
+ }
157
+
81
158
  .btn-secondary {
82
159
  background: hsl(var(--secondary));
83
160
  color: hsl(var(--secondary-foreground));
@@ -88,6 +165,11 @@ main {
88
165
  color: hsl(var(--foreground));
89
166
  }
90
167
 
168
+ .btn-ghost:hover {
169
+ background: hsl(var(--accent));
170
+ color: hsl(var(--accent-foreground));
171
+ }
172
+
91
173
  .btn-link {
92
174
  background: transparent;
93
175
  color: hsl(var(--primary));
@@ -115,3 +197,280 @@ main {
115
197
  width: 2.25rem;
116
198
  padding: 0;
117
199
  }
200
+
201
+ /* Forms */
202
+
203
+ .input {
204
+ display: flex;
205
+ width: 100%;
206
+ height: 2.25rem;
207
+ box-sizing: border-box;
208
+ border-radius: calc(var(--radius) - 2px);
209
+ border: 1px solid hsl(var(--input));
210
+ background: transparent;
211
+ padding: 0 0.75rem;
212
+ font-size: 0.875rem;
213
+ font-family: inherit;
214
+ color: hsl(var(--foreground));
215
+ transition: border-color 0.15s ease;
216
+ }
217
+
218
+ textarea.input {
219
+ height: auto;
220
+ min-height: 4.5rem;
221
+ padding: 0.5rem 0.75rem;
222
+ resize: vertical;
223
+ }
224
+
225
+ .input::placeholder {
226
+ color: hsl(var(--muted-foreground));
227
+ }
228
+
229
+ .input:focus-visible {
230
+ outline: 2px solid hsl(var(--ring));
231
+ outline-offset: 1px;
232
+ }
233
+
234
+ .input:disabled {
235
+ cursor: not-allowed;
236
+ opacity: 0.5;
237
+ }
238
+
239
+ .label {
240
+ font-size: 0.875rem;
241
+ font-weight: 500;
242
+ line-height: 1;
243
+ }
244
+
245
+ .field {
246
+ display: flex;
247
+ flex-direction: column;
248
+ gap: 0.5rem;
249
+ width: 100%;
250
+ }
251
+
252
+ .field-error {
253
+ color: hsl(var(--destructive));
254
+ font-size: 0.8125rem;
255
+ margin: 0;
256
+ }
257
+
258
+ /* Card */
259
+
260
+ .card {
261
+ border-radius: var(--radius);
262
+ border: 1px solid hsl(var(--border));
263
+ background: hsl(var(--card));
264
+ color: hsl(var(--card-foreground));
265
+ }
266
+
267
+ .card-header {
268
+ display: flex;
269
+ flex-direction: column;
270
+ gap: 0.375rem;
271
+ padding: 1.5rem 1.5rem 0;
272
+ }
273
+
274
+ .card-title {
275
+ margin: 0;
276
+ font-size: 1rem;
277
+ font-weight: 600;
278
+ letter-spacing: -0.025em;
279
+ }
280
+
281
+ .card-description {
282
+ margin: 0;
283
+ color: hsl(var(--muted-foreground));
284
+ font-size: 0.875rem;
285
+ }
286
+
287
+ .card-content {
288
+ padding: 1.5rem;
289
+ }
290
+
291
+ .card-footer {
292
+ display: flex;
293
+ align-items: center;
294
+ gap: 0.5rem;
295
+ padding: 0 1.5rem 1.5rem;
296
+ }
297
+
298
+ /* Badge */
299
+
300
+ .badge {
301
+ display: inline-flex;
302
+ align-items: center;
303
+ border-radius: 9999px;
304
+ border: 1px solid transparent;
305
+ background: hsl(var(--primary));
306
+ color: hsl(var(--primary-foreground));
307
+ padding: 0.125rem 0.625rem;
308
+ font-size: 0.75rem;
309
+ font-weight: 600;
310
+ }
311
+
312
+ .badge-secondary {
313
+ background: hsl(var(--secondary));
314
+ color: hsl(var(--secondary-foreground));
315
+ }
316
+
317
+ .badge-destructive {
318
+ background: hsl(var(--destructive));
319
+ color: hsl(var(--destructive-foreground));
320
+ }
321
+
322
+ .badge-outline {
323
+ background: transparent;
324
+ border-color: hsl(var(--border));
325
+ color: hsl(var(--foreground));
326
+ }
327
+
328
+ /* Table */
329
+
330
+ .table-wrapper {
331
+ width: 100%;
332
+ overflow-x: auto;
333
+ border: 1px solid hsl(var(--border));
334
+ border-radius: var(--radius);
335
+ }
336
+
337
+ .table {
338
+ width: 100%;
339
+ border-collapse: collapse;
340
+ font-size: 0.875rem;
341
+ text-align: left;
342
+ }
343
+
344
+ .table th {
345
+ height: 2.5rem;
346
+ padding: 0 1rem;
347
+ vertical-align: middle;
348
+ font-weight: 500;
349
+ color: hsl(var(--muted-foreground));
350
+ border-bottom: 1px solid hsl(var(--border));
351
+ }
352
+
353
+ .table td {
354
+ padding: 0.75rem 1rem;
355
+ vertical-align: middle;
356
+ border-bottom: 1px solid hsl(var(--border));
357
+ }
358
+
359
+ .table tbody tr:last-child td {
360
+ border-bottom: none;
361
+ }
362
+
363
+ .table tbody tr:hover {
364
+ background: hsl(var(--muted) / 0.5);
365
+ }
366
+
367
+ /* Dialog */
368
+
369
+ .dialog-overlay {
370
+ position: fixed;
371
+ inset: 0;
372
+ z-index: 50;
373
+ display: flex;
374
+ align-items: center;
375
+ justify-content: center;
376
+ background: hsl(var(--foreground) / 0.4);
377
+ padding: 1rem;
378
+ }
379
+
380
+ .dialog {
381
+ width: 100%;
382
+ max-width: 28rem;
383
+ display: flex;
384
+ flex-direction: column;
385
+ gap: 1rem;
386
+ border-radius: var(--radius);
387
+ border: 1px solid hsl(var(--border));
388
+ background: hsl(var(--popover));
389
+ color: hsl(var(--popover-foreground));
390
+ padding: 1.5rem;
391
+ box-shadow: 0 10px 30px hsl(var(--foreground) / 0.15);
392
+ }
393
+
394
+ .dialog-title {
395
+ margin: 0;
396
+ font-size: 1rem;
397
+ font-weight: 600;
398
+ }
399
+
400
+ .dialog-footer {
401
+ display: flex;
402
+ justify-content: flex-end;
403
+ gap: 0.5rem;
404
+ }
405
+
406
+ /* Feedback */
407
+
408
+ .skeleton {
409
+ border-radius: calc(var(--radius) - 2px);
410
+ background: hsl(var(--muted));
411
+ animation: skeleton-pulse 1.5s ease-in-out infinite;
412
+ min-height: 1rem;
413
+ }
414
+
415
+ @keyframes skeleton-pulse {
416
+ 50% {
417
+ opacity: 0.5;
418
+ }
419
+ }
420
+
421
+ .spinner {
422
+ width: 1.25rem;
423
+ height: 1.25rem;
424
+ border-radius: 9999px;
425
+ border: 2px solid hsl(var(--muted));
426
+ border-top-color: hsl(var(--foreground));
427
+ animation: spinner-spin 0.8s linear infinite;
428
+ }
429
+
430
+ @keyframes spinner-spin {
431
+ to {
432
+ transform: rotate(360deg);
433
+ }
434
+ }
435
+
436
+ .empty-state {
437
+ display: flex;
438
+ flex-direction: column;
439
+ align-items: center;
440
+ justify-content: center;
441
+ gap: 0.5rem;
442
+ text-align: center;
443
+ border: 1px dashed hsl(var(--border));
444
+ border-radius: var(--radius);
445
+ padding: 3rem 1.5rem;
446
+ width: 100%;
447
+ box-sizing: border-box;
448
+ }
449
+
450
+ .empty-state-title {
451
+ margin: 0;
452
+ font-size: 1rem;
453
+ font-weight: 600;
454
+ }
455
+
456
+ .empty-state-description {
457
+ margin: 0;
458
+ color: hsl(var(--muted-foreground));
459
+ font-size: 0.875rem;
460
+ }
461
+
462
+ /* Auth pages */
463
+
464
+ .auth-page {
465
+ min-height: 100vh;
466
+ display: flex;
467
+ align-items: center;
468
+ justify-content: center;
469
+ padding: 1.5rem;
470
+ box-sizing: border-box;
471
+ }
472
+
473
+ .auth-card {
474
+ width: 100%;
475
+ max-width: 24rem;
476
+ }
@@ -0,0 +1,29 @@
1
+ import type { ReactNode } from "react";
2
+ import {
3
+ Card,
4
+ CardContent,
5
+ CardDescription,
6
+ CardHeader,
7
+ CardTitle,
8
+ } from "@/components/ui/card";
9
+
10
+ export type AuthCardProps = {
11
+ title: string;
12
+ description: string;
13
+ children: ReactNode;
14
+ };
15
+
16
+ /** Centered card shell shared by the auth pages. */
17
+ export function AuthCard({ title, description, children }: AuthCardProps) {
18
+ return (
19
+ <div className="auth-page">
20
+ <Card className="auth-card">
21
+ <CardHeader>
22
+ <CardTitle>{title}</CardTitle>
23
+ <CardDescription>{description}</CardDescription>
24
+ </CardHeader>
25
+ <CardContent>{children}</CardContent>
26
+ </Card>
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,17 @@
1
+ import type { HTMLAttributes } from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ const variantClasses = {
5
+ default: "badge",
6
+ secondary: "badge badge-secondary",
7
+ destructive: "badge badge-destructive",
8
+ outline: "badge badge-outline",
9
+ } as const;
10
+
11
+ export type BadgeProps = HTMLAttributes<HTMLSpanElement> & {
12
+ variant?: keyof typeof variantClasses;
13
+ };
14
+
15
+ export function Badge({ className, variant = "default", ...props }: BadgeProps) {
16
+ return <span className={cn(variantClasses[variant], className)} {...props} />;
17
+ }
@@ -0,0 +1,28 @@
1
+ import type { HTMLAttributes } from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ type DivProps = HTMLAttributes<HTMLDivElement>;
5
+
6
+ export function Card({ className, ...props }: DivProps) {
7
+ return <div className={cn("card", className)} {...props} />;
8
+ }
9
+
10
+ export function CardHeader({ className, ...props }: DivProps) {
11
+ return <div className={cn("card-header", className)} {...props} />;
12
+ }
13
+
14
+ export function CardTitle({ className, ...props }: HTMLAttributes<HTMLHeadingElement>) {
15
+ return <h3 className={cn("card-title", className)} {...props} />;
16
+ }
17
+
18
+ export function CardDescription({ className, ...props }: HTMLAttributes<HTMLParagraphElement>) {
19
+ return <p className={cn("card-description", className)} {...props} />;
20
+ }
21
+
22
+ export function CardContent({ className, ...props }: DivProps) {
23
+ return <div className={cn("card-content", className)} {...props} />;
24
+ }
25
+
26
+ export function CardFooter({ className, ...props }: DivProps) {
27
+ return <div className={cn("card-footer", className)} {...props} />;
28
+ }
@@ -0,0 +1,38 @@
1
+ import { useEffect, type ReactNode } from "react";
2
+
3
+ export type DialogProps = {
4
+ open: boolean;
5
+ onClose: () => void;
6
+ title?: string;
7
+ children: ReactNode;
8
+ footer?: ReactNode;
9
+ };
10
+
11
+ /** Controlled modal. Closes on overlay click and Escape. */
12
+ export function Dialog({ open, onClose, title, children, footer }: DialogProps) {
13
+ useEffect(() => {
14
+ if (!open) return;
15
+ function onKeyDown(event: KeyboardEvent) {
16
+ if (event.key === "Escape") onClose();
17
+ }
18
+ document.addEventListener("keydown", onKeyDown);
19
+ return () => document.removeEventListener("keydown", onKeyDown);
20
+ }, [open, onClose]);
21
+
22
+ if (!open) return null;
23
+ return (
24
+ <div className="dialog-overlay" onClick={onClose}>
25
+ <div
26
+ className="dialog"
27
+ role="dialog"
28
+ aria-modal="true"
29
+ aria-label={title}
30
+ onClick={(event) => event.stopPropagation()}
31
+ >
32
+ {title ? <h2 className="dialog-title">{title}</h2> : null}
33
+ {children}
34
+ {footer ? <div className="dialog-footer">{footer}</div> : null}
35
+ </div>
36
+ </div>
37
+ );
38
+ }
@@ -0,0 +1,17 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export type EmptyStateProps = {
4
+ title: string;
5
+ description?: string;
6
+ action?: ReactNode;
7
+ };
8
+
9
+ export function EmptyState({ title, description, action }: EmptyStateProps) {
10
+ return (
11
+ <div className="empty-state">
12
+ <p className="empty-state-title">{title}</p>
13
+ {description ? <p className="empty-state-description">{description}</p> : null}
14
+ {action}
15
+ </div>
16
+ );
17
+ }
@@ -0,0 +1,8 @@
1
+ import type { InputHTMLAttributes } from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ export type InputProps = InputHTMLAttributes<HTMLInputElement>;
5
+
6
+ export function Input({ className, ...props }: InputProps) {
7
+ return <input className={cn("input", className)} {...props} />;
8
+ }
@@ -0,0 +1,8 @@
1
+ import type { LabelHTMLAttributes } from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ export type LabelProps = LabelHTMLAttributes<HTMLLabelElement>;
5
+
6
+ export function Label({ className, ...props }: LabelProps) {
7
+ return <label className={cn("label", className)} {...props} />;
8
+ }
@@ -0,0 +1,6 @@
1
+ import type { HTMLAttributes } from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ export function Skeleton({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
5
+ return <div className={cn("skeleton", className)} {...props} />;
6
+ }
@@ -0,0 +1,3 @@
1
+ export function Spinner() {
2
+ return <div className="spinner" role="status" aria-label="Loading" />;
3
+ }