@h-ai/cli 0.1.0-alpha.57 → 0.1.0-alpha.59

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 (177) hide show
  1. package/README.en.md +70 -21
  2. package/README.ja.md +70 -21
  3. package/README.md +70 -21
  4. package/dist/chunk-2H7UOFLK.js +0 -0
  5. package/dist/{chunk-O3ANVICQ.js → chunk-WUSFJFW5.js} +13 -7
  6. package/dist/chunk-WUSFJFW5.js.map +1 -0
  7. package/dist/{chunk-MJGUBOAV.js → chunk-WWG3U5ZD.js} +24 -42
  8. package/dist/chunk-WWG3U5ZD.js.map +1 -0
  9. package/dist/cli-config-templates-6XA7V6CC.js +5 -0
  10. package/dist/{cli-config-templates-OWBEGBZQ.js.map → cli-config-templates-6XA7V6CC.js.map} +1 -1
  11. package/dist/{cli-skill-templates-ZYXES3NT.js → cli-skill-templates-2KDUACT6.js} +3 -3
  12. package/dist/{cli-skill-templates-ZYXES3NT.js.map → cli-skill-templates-2KDUACT6.js.map} +1 -1
  13. package/dist/index.js +192 -125
  14. package/dist/index.js.map +1 -1
  15. package/dist/{src-TBIET7YU.js → src-DUGLX4YB.js} +7270 -2119
  16. package/dist/src-DUGLX4YB.js.map +1 -0
  17. package/package.json +19 -17
  18. package/templates/README.md +4 -4
  19. package/templates/apps/admin/src/routes/+layout.svelte.hbs +7 -0
  20. package/templates/apps/admin/src/routes/+page.svelte.hbs +5 -4
  21. package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +2 -2
  22. package/templates/apps/api/.npmrc.hbs +1 -2
  23. package/templates/apps/api/Dockerfile.hbs +6 -5
  24. package/templates/apps/api/apps/{{projectName}}-contract/package.json.hbs +1 -1
  25. package/templates/apps/api/apps/{{projectName}}-contract/src/app-schemas.ts.hbs +2 -0
  26. package/templates/apps/api/apps/{{projectName}}-contract/tsup.config.ts.hbs +1 -1
  27. package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +8 -0
  28. package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +1 -1
  29. package/templates/apps/api/apps/{{projectName}}-service/src/app.ts.hbs +7 -8
  30. package/templates/apps/api/apps/{{projectName}}-service/src/index.ts.hbs +4 -3
  31. package/templates/apps/api/apps/{{projectName}}-service/src/lib/server/init.ts.hbs +0 -1
  32. package/templates/apps/api/apps/{{projectName}}-service/src/server/procedures/app-procedures.ts.hbs +4 -0
  33. package/templates/apps/api/apps/{{projectName}}-service/tests/service.test.ts.hbs +3 -1
  34. package/templates/apps/api/e2e/api-service.spec.ts.hbs +3 -1
  35. package/templates/apps/api/package.json.hbs +2 -2
  36. package/templates/apps/api/pnpm-workspace.yaml.hbs +26 -0
  37. package/templates/apps/api/tsconfig.base.json +0 -1
  38. package/templates/apps/fullstack/.dockerignore.hbs +3 -0
  39. package/templates/apps/fullstack/.npmrc.hbs +1 -2
  40. package/templates/apps/fullstack/Dockerfile.hbs +6 -5
  41. package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +2 -2
  42. package/templates/apps/fullstack/apps/{{projectName}}-app/capacitor.config.ts.hbs +1 -1
  43. package/templates/apps/fullstack/apps/{{projectName}}-app/index.html.hbs +7 -2
  44. package/templates/apps/fullstack/apps/{{projectName}}-app/messages/en-US.json.hbs +1 -34
  45. package/templates/apps/fullstack/apps/{{projectName}}-app/messages/zh-CN.json.hbs +1 -34
  46. package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +2 -2
  47. package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +30 -170
  48. package/templates/apps/fullstack/apps/{{projectName}}-app/src/app.d.ts.hbs +1 -1
  49. package/templates/apps/fullstack/apps/{{projectName}}-app/src/lib/capacitor.ts.hbs +6 -5
  50. package/templates/apps/fullstack/apps/{{projectName}}-app/src/main.ts.hbs +1 -1
  51. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +48 -22
  52. package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +3 -1
  53. package/templates/apps/fullstack/apps/{{projectName}}-desktop/index.html.hbs +6 -1
  54. package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/en-US.json.hbs +1 -28
  55. package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/zh-CN.json.hbs +1 -28
  56. package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +5 -4
  57. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/App.svelte.hbs +10 -141
  58. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/Cargo.toml.hbs +5 -5
  59. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/tauri.conf.json.hbs +3 -3
  60. package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +21 -2
  61. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +2 -2
  62. package/templates/apps/fullstack/apps/{{projectName}}-web/index.html.hbs +6 -1
  63. package/templates/apps/fullstack/apps/{{projectName}}-web/messages/en-US.json.hbs +1 -28
  64. package/templates/apps/fullstack/apps/{{projectName}}-web/messages/zh-CN.json.hbs +1 -28
  65. package/templates/apps/fullstack/apps/{{projectName}}-web/package.json.hbs +2 -2
  66. package/templates/apps/fullstack/apps/{{projectName}}-web/src/App.svelte.hbs +5 -143
  67. package/templates/apps/fullstack/apps/{{projectName}}-web/src/app.d.ts.hbs +1 -1
  68. package/templates/apps/fullstack/apps/{{projectName}}-web/src/main.ts.hbs +1 -1
  69. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +48 -22
  70. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +206 -16
  71. package/templates/apps/fullstack/package.json.hbs +2 -2
  72. package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +2 -0
  73. package/templates/apps/fullstack/packages/{{projectName}}-contract/package.json.hbs +1 -1
  74. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs +66 -0
  75. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +96 -0
  76. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/app-schemas.ts.hbs +3 -0
  77. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/index.ts.hbs +3 -1
  78. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +5 -0
  79. package/templates/apps/fullstack/packages/{{projectName}}-contract/tests/contract.test.ts.hbs +3 -0
  80. package/templates/apps/fullstack/packages/{{projectName}}-contract/tsup.config.ts.hbs +1 -1
  81. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +13 -3
  82. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_audit.yml.hbs +3 -0
  83. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_cache.yml.hbs +2 -0
  84. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_core.yml.hbs +10 -3
  85. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_db.yml.hbs +3 -0
  86. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_iam.yml.hbs +21 -0
  87. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +10 -3
  88. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_storage.yml.hbs +3 -0
  89. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +10 -3
  90. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +63 -28
  91. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/index.ts.hbs +26 -11
  92. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +276 -0
  93. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/messages.ts.hbs +29 -0
  94. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +94 -0
  95. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/app-procedures.ts.hbs +6 -0
  96. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +5 -1
  97. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +98 -0
  98. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +446 -33
  99. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/runtime.test.ts.hbs +48 -0
  100. package/templates/apps/fullstack/packages/{{projectName}}-serv/tsup.config.ts.hbs +1 -1
  101. package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +19 -26
  102. package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/en-US.json.hbs +78 -15
  103. package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/zh-CN.json.hbs +78 -15
  104. package/templates/apps/fullstack/packages/{{projectName}}-shared/package.json.hbs +4 -1
  105. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/index.ts.hbs +3 -4
  106. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs +53 -27
  107. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +73 -0
  108. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +55 -45
  109. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AuditManagement.svelte.hbs +61 -0
  110. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/ProfileSettings.svelte.hbs +57 -0
  111. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/RoleManagement.svelte.hbs +49 -0
  112. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/UserManagement.svelte.hbs +174 -0
  113. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/management-store.svelte.ts.hbs +399 -0
  114. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/theme-store.svelte.ts.hbs +7 -39
  115. package/templates/apps/fullstack/packages/{{projectName}}-shared/svelte.config.js.hbs +1 -0
  116. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +151 -0
  117. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/management-store.test.ts.hbs +101 -0
  118. package/templates/apps/fullstack/packages/{{projectName}}-shared/vitest.config.ts.hbs +7 -0
  119. package/templates/apps/fullstack/playwright.config.ts.hbs +6 -0
  120. package/templates/apps/fullstack/pnpm-workspace.yaml.hbs +24 -0
  121. package/templates/apps/fullstack/tsconfig.base.json +0 -1
  122. package/templates/apps/h5/src/routes/+layout.svelte.hbs +4 -0
  123. package/templates/apps/h5/src/routes/+page.svelte.hbs +3 -2
  124. package/templates/apps/mobile-app/index.html.hbs +2 -2
  125. package/templates/apps/mobile-app/src/App.svelte.hbs +6 -2
  126. package/templates/apps/mobile-app/src/lib/app-config.ts.hbs +13 -0
  127. package/templates/apps/website/src/routes/+layout.svelte.hbs +8 -2
  128. package/templates/apps/website/src/routes/+page.svelte.hbs +4 -3
  129. package/templates/base/.npmrc.hbs +1 -15
  130. package/templates/base/Dockerfile.hbs +9 -8
  131. package/templates/base/e2e/app.spec.ts.hbs +1 -0
  132. package/templates/base/package.json.hbs +39 -42
  133. package/templates/base/pnpm-workspace.yaml.hbs +26 -0
  134. package/templates/base/src/app.d.ts.hbs +4 -0
  135. package/templates/base/src/hooks.server.ts.hbs +0 -8
  136. package/templates/base/src/lib/server/init.ts.hbs +26 -5
  137. package/templates/base/src/routes/+layout.server.ts.hbs +18 -0
  138. package/templates/base/tests/scaffold.test.ts.hbs +21 -1
  139. package/templates/base/vite.config.ts.hbs +21 -0
  140. package/templates/features/ai/routes/api/chat/+server.ts +6 -7
  141. package/templates/features/iam/routes-admin/(auth)/auth/login/+page.svelte.hbs +1 -1
  142. package/templates/features/iam/routes-h5/auth/login/+page.svelte.hbs +1 -1
  143. package/templates/features/iam/routes-shared/api/auth/login/+server.ts +14 -10
  144. package/templates/features/iam/routes-shared/api/auth/logout/+server.ts +6 -4
  145. package/templates/features/storage/routes/api/upload/+server.ts +10 -8
  146. package/templates/skills/CLAUDE.md +0 -14
  147. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +13 -2
  148. package/templates/skills/bridges/fullstack/copilot-instructions.md.hbs +3 -0
  149. package/templates/skills/hai-api-client/SKILL.md +1 -1
  150. package/templates/skills/hai-app-create/SKILL.md +2 -0
  151. package/templates/skills/hai-app-create/development.md +21 -0
  152. package/templates/skills/hai-build/reference.md +4 -2
  153. package/templates/skills/hai-core/SKILL.md +2 -0
  154. package/templates/skills/hai-core/reference.md +6 -4
  155. package/templates/skills/hai-deploy/SKILL.md +2 -0
  156. package/templates/skills/hai-framework-sync/SKILL.md +5 -2
  157. package/templates/skills/hai-serv/SKILL.md +1 -0
  158. package/templates/skills/hai-serv/reference.md +3 -1
  159. package/templates/skills/hai-ui/SKILL.md +6 -0
  160. package/templates/skills/hai-ui/reference.md +7 -4
  161. package/templates/skills/hai-vecdb/reference.md +53 -49
  162. package/dist/chunk-MJGUBOAV.js.map +0 -1
  163. package/dist/chunk-O3ANVICQ.js.map +0 -1
  164. package/dist/cli-config-templates-OWBEGBZQ.js +0 -5
  165. package/dist/src-TBIET7YU.js.map +0 -1
  166. package/templates/apps/fullstack/apps/{{projectName}}-app/tests/api.test.ts.hbs +0 -28
  167. package/templates/apps/fullstack/apps/{{projectName}}-desktop/tests/api.test.ts.hbs +0 -28
  168. package/templates/apps/fullstack/apps/{{projectName}}-web/tests/api.test.ts.hbs +0 -55
  169. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/LanguageSwitcher.svelte.hbs +0 -27
  170. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/ThemeSwitcher.svelte.hbs +0 -18
  171. package/templates/skills/bridges/admin/CLAUDE.md +0 -15
  172. package/templates/skills/bridges/api/CLAUDE.md +0 -15
  173. package/templates/skills/bridges/fullstack/CLAUDE.md.hbs +0 -9
  174. package/templates/skills/bridges/h5/CLAUDE.md +0 -15
  175. package/templates/skills/bridges/mobile-app/CLAUDE.md +0 -15
  176. package/templates/skills/bridges/website/CLAUDE.md +0 -15
  177. package/templates/skills/claude-skill.md.hbs +0 -7
@@ -1,39 +1,32 @@
1
1
  # {{fullstack.sharedPackageName}}
2
2
 
3
- Frontend shared package for **{{projectName}}**. Provides:
3
+ Shared mini administration workspace for **{{projectName}}**. Web, Capacitor App and Tauri desktop use the same `AdminWorkspace`, responsive `AppShell`, typed API client and management store.
4
4
 
5
- - `AppShell` — common layout (header / footer / theme + language switchers)
6
- - `ThemeSwitcher`, `LanguageSwitcher`
7
- - `themeStore` — daisyUI theme persistence
8
- - `fetchAppInfo` — typed API client wrapping `@h-ai/api-client`
9
- - Shared Inlang Paraglide messages compiled from this package's `project.inlang/` + `messages/`
5
+ - Administrators manage users, upload user avatars, assign the built-in administrator/user roles, edit role descriptions and query audit logs.
6
+ - Ordinary users can only update their own profile, avatar, language, appearance and password. The server enforces access on every request.
7
+ - `ManagementStore` owns API calls and business state; components render the state and forward user interactions.
8
+ - Application name, version, environment and default locale come from the backend `app.info` contract, which reads `core` configuration. Do not duplicate these values in translations or frontend constants.
9
+ - Requests, token refresh and JSON responses use the same `@h-ai/api-client` + `@h-ai/crypto` transport. Avatar uploads use encrypted JSON and validate PNG/JPEG/WebP content, with a 512 KiB limit.
10
+ - Read metadata through the same initialized `managementApi.app.info()` client; inspect `success` before using `data`.
10
11
 
11
- Consumed by `{{projectName}}-web`, `{{projectName}}-app`, `{{projectName}}-desktop`, and the reserved `{{projectName}}-miniapp` guidance.
12
+ All three Vite apps read the workspace root .env. Development and preview servers proxy /api to the local backend when no explicit API base is configured, so browser clients can use a single origin. In deployments, configure PUBLIC_API_BASE or the Web container runtime API origin for the actual service.
12
13
 
13
- ## i18n collaboration
14
+ An origin-only API base uses `/api/v1`. For a custom service `http.apiPrefix`, include that path in `PUBLIC_API_BASE` or the runtime API base; the client preserves the configured path and restricts avatar URLs to its matching resource prefix.
14
15
 
15
- - `packages/{{fullstack.sharedPackageName}}/messages/` stores **shared UI copy** used by reusable components such as `AppShell`, `ThemeSwitcher`, and `LanguageSwitcher`.
16
- - `apps/{{projectName}}-web/messages/`, `apps/{{projectName}}-app/messages/`, and `apps/{{projectName}}-desktop/messages/` store **frontend-specific product copy**.
17
- - `apps/{{projectName}}-miniapp/messages/` is prepared for mini-app-specific copy even though the mini target is currently a placeholder.
18
- - Keep `baseLocale` and `locales` aligned across shared and every frontend `project.inlang/settings.json`. The language switcher updates shared and the host app runtime together.
16
+ ## Authentication and preferences
19
17
 
20
- Rule of thumb: if a string appears inside reusable shared components, put it here; if it belongs to one product surface, put it in that frontend's `messages/` directory.
18
+ Web and browser previews keep access tokens in memory and refresh tokens in the server's httpOnly Cookie. Requests include credentials; reload restores the session through the encrypted refresh flow. Never store authentication tokens in localStorage.
21
19
 
22
- ## Scripts
20
+ Capacitor native uses `createCapacitorTokenStorage()` and the trusted native token channel. Tauri native uses memory token storage and requires a new login after restarting the application. Native builds require a reachable `PUBLIC_API_BASE` in the workspace root `.env`; missing configuration produces a connection configuration error. Both send their frontend `clientName` and `X-HAI-Token-Transport: body`; the service verifies the allowed native origin before returning a refresh token in JSON.
23
21
 
24
- - `pnpm paraglide:compile` — compile messages → `src/lib/paraglide/`
25
- - `pnpm paraglide:watch` — watch + recompile on message edits
26
- - `pnpm typecheck`, `pnpm lint`, `pnpm test`
22
+ Language and theme preferences are saved in IAM user metadata, restored after login and synchronized with the shared/host Paraglide runtimes and `core.i18n`. The small reactive theme store delegates DOM updates and safe appearance caching to `@h-ai/ui/theme` (`applyTheme` / `getSavedTheme`); the framework uses the `theme` localStorage key. Appearance selectors live in personal settings and save through the authenticated profile contract.
27
23
 
28
- ## Adding a locale
24
+ ## i18n and themes
29
25
 
30
- 1. Add the locale to `project.inlang/settings.json` `locales` array.
31
- 2. Add the same locale to every enabled frontend `project.inlang/settings.json`.
32
- 3. Create `messages/{locale}.json` in shared and each enabled frontend.
33
- 4. Run root `pnpm i18n:compile` to compile shared and every enabled frontend Paraglide output. You can also run `pnpm paraglide:compile` inside one package when editing that package only.
26
+ Shared page copy belongs in `messages/{zh-CN,en-US}.json`. Frontend-specific copy belongs in each app's own messages directory. Keep `baseLocale` and `locales` aligned across all enabled projects, then run root `pnpm i18n:compile`; do not edit generated Paraglide files.
34
27
 
35
- ## Customizing theme and shell
28
+ The scaffold theme configuration determines the themes compiled into frontend CSS. Personal settings offer only those available themes. Customize the common layout in `src/lib/components/AppShell.svelte`, the management pages in adjacent components, and business behavior in `src/lib/stores/management-store.svelte.ts`.
36
29
 
37
- - Theme names come from the scaffold `fullstack.theme` config and are rendered into each frontend `src/app.css` daisyUI plugin block.
38
- - `themeStore` persists the selected theme in local storage under `{{projectName}}:theme`.
39
- - To customize the global chrome, edit `src/lib/components/AppShell.svelte`; to customize a single frontend page, edit that frontend's route and messages.
30
+ ## Validation
31
+
32
+ Run root `pnpm typecheck`, `pnpm lint`, `pnpm build`, `pnpm test` and `pnpm test:e2e`. Shared API tests exercise real transport key exchange, encrypted request/response handling, configured core metadata and credential forwarding. Root browser tests cover administrator and ordinary-user workflows, avatar rendering, persisted preferences, password/session invalidation and narrow-screen layout.
@@ -1,24 +1,87 @@
1
1
  {{! @skipIfEmpty }}
2
2
  {
3
3
  "$schema": "https://inlang.com/schema/inlang-message-format",
4
- "app_name": "{{projectName}}",
5
- "nav_home": "Home",
6
- "nav_settings": "Settings",
7
- "settings_title": "Settings",
4
+ "settings_title": "Personal settings",
8
5
  "settings_language": "Language",
9
6
  "settings_theme": "Theme",
10
- "shell_footer": "Hai Framework · {platform}",
11
- "shell_tagline": "Shared shell · Theme · Locale",
12
- "home_welcome": "Welcome to {name}",
13
- "home_subtitle": "Hai Framework fullstack starter for web / app / desktop / mini collaboration",
14
- "home_status_title": "Service status",
15
7
  "home_refresh": "Refresh",
16
8
  "home_loading": "Loading…",
17
9
  "home_error": "Unable to reach the API service",
18
- "home_started_at": "Started at: {time}",
19
- "home_frontends": "Enabled frontends: {list}",
20
- "platform_web": "Web",
21
- "platform_app": "App",
22
- "platform_desktop": "Desktop",
23
- "platform_miniapp": "Mini"
10
+ "nav_users": "Users",
11
+ "nav_roles": "Roles",
12
+ "nav_audit": "Audit logs",
13
+ "navigation": "Main navigation",
14
+ "open_menu": "Open navigation",
15
+ "close_menu": "Close navigation",
16
+ "logout": "Sign out",
17
+ "login_title": "Sign in",
18
+ "login_description": "Sign in to manage your account and workspace",
19
+ "login_invalid": "The account or password is incorrect. Please try again.",
20
+ "session_expired": "Your session has expired. Please sign in again.",
21
+ "saved": "Changes saved",
22
+ "save": "Save",
23
+ "edit": "Edit",
24
+ "delete": "Delete",
25
+ "cancel": "Cancel",
26
+ "search": "Search",
27
+ "details": "Details",
28
+ "created_at": "Created at",
29
+ "enabled": "Active",
30
+ "disabled": "Disabled",
31
+ "all_statuses": "All statuses",
32
+ "operation_failed": "The operation could not be completed. Please try again.",
33
+ "network_error": "Connection failed. Check your network and try again.",
34
+ "forbidden": "You do not have permission to perform this operation.",
35
+ "conflict": "This account already exists or the change conflicts with the current data.",
36
+ "validation_failed": "Check the information entered and try again.",
37
+ "settings_description": "Manage your profile, preferences and account security",
38
+ "profile_title": "Profile",
39
+ "preferences_title": "Preferences",
40
+ "preferences_description": "Your language and appearance preferences follow your account across devices.",
41
+ "password_title": "Change password",
42
+ "password_rules": "Use at least 8 characters. Choose a different password. You will need to sign in again after changing it.",
43
+ "password_changed": "Password updated. Please sign in with your new password.",
44
+ "avatar_hint": "Upload a PNG, JPEG or WebP image, up to 512 KB.",
45
+ "avatar_invalid": "Choose a PNG, JPEG or WebP image no larger than 512 KB.",
46
+ "avatar_saved": "Avatar updated",
47
+ "avatar_upload": "Upload avatar",
48
+ "users_description": "Create accounts, assign roles and manage access",
49
+ "users_search": "Search username, name or email",
50
+ "user_account": "Account",
51
+ "user_username": "Username",
52
+ "user_display_name": "Display name",
53
+ "user_email": "Email",
54
+ "user_password": "Initial password",
55
+ "user_role": "Role",
56
+ "user_status": "Status",
57
+ "user_create": "Add user",
58
+ "initial_password_rules": "Use at least 8 characters.",
59
+ "account_enabled": "Allow sign-in",
60
+ "users_empty": "No matching accounts. Try another search or create a user.",
61
+ "user_edit": "Edit user",
62
+ "user_deleted": "User deleted",
63
+ "user_delete_title": "Delete user",
64
+ "user_delete_confirm": "Delete {name}? This permanently removes the account and its access.",
65
+ "roles_description": "Two roles keep access simple. Assign roles from user management.",
66
+ "role_admin": "Administrator",
67
+ "role_user": "User",
68
+ "role_admin_capabilities": "Manage users, role descriptions and audit logs, and update personal settings.",
69
+ "role_user_capabilities": "Update personal profile, avatar, language, appearance and password.",
70
+ "role_members": "{count} members",
71
+ "role_edit": "Edit description",
72
+ "role_description": "Description",
73
+ "audit_description": "Review account and administration activity",
74
+ "audit_actor": "Actor",
75
+ "audit_action": "Action",
76
+ "audit_resource": "Resource",
77
+ "audit_resource_id": "Resource ID",
78
+ "audit_ip": "IP address",
79
+ "audit_start": "From",
80
+ "audit_end": "To",
81
+ "audit_action_placeholder": "All actions",
82
+ "audit_resource_placeholder": "All resources",
83
+ "audit_system": "System",
84
+ "audit_details": "Audit log details",
85
+ "audit_date_invalid": "The end date must be on or after the start date.",
86
+ "api_not_configured": "The service connection is not configured. Please contact your administrator."
24
87
  }
@@ -1,24 +1,87 @@
1
1
  {{! @skipIfEmpty }}
2
2
  {
3
3
  "$schema": "https://inlang.com/schema/inlang-message-format",
4
- "app_name": "{{projectName}}",
5
- "nav_home": "首页",
6
- "nav_settings": "设置",
7
- "settings_title": "设置",
4
+ "settings_title": "个人设置",
8
5
  "settings_language": "语言",
9
6
  "settings_theme": "主题",
10
- "shell_footer": "Hai Framework · {platform}",
11
- "shell_tagline": "共享 Shell · 主题 · 语言",
12
- "home_welcome": "欢迎使用 {name}",
13
- "home_subtitle": "Hai Framework 全栈样板(web / app / desktop / mini 协同)",
14
- "home_status_title": "服务状态",
15
7
  "home_refresh": "刷新",
16
8
  "home_loading": "加载中…",
17
9
  "home_error": "无法连接到 API 服务",
18
- "home_started_at": "启动时间:{time}",
19
- "home_frontends": "已启用前端:{list}",
20
- "platform_web": "Web",
21
- "platform_app": "App",
22
- "platform_desktop": "Desktop",
23
- "platform_miniapp": "Mini"
10
+ "nav_users": "用户管理",
11
+ "nav_roles": "角色管理",
12
+ "nav_audit": "审计日志",
13
+ "navigation": "主导航",
14
+ "open_menu": "打开导航",
15
+ "close_menu": "关闭导航",
16
+ "logout": "退出登录",
17
+ "login_title": "登录",
18
+ "login_description": "登录后管理个人账户与工作空间",
19
+ "login_invalid": "账号或密码不正确,请重试。",
20
+ "session_expired": "登录已过期,请重新登录。",
21
+ "saved": "已保存修改",
22
+ "save": "保存",
23
+ "edit": "编辑",
24
+ "delete": "删除",
25
+ "cancel": "取消",
26
+ "search": "查询",
27
+ "details": "详情",
28
+ "created_at": "创建时间",
29
+ "enabled": "正常",
30
+ "disabled": "已禁用",
31
+ "all_statuses": "全部状态",
32
+ "operation_failed": "操作未完成,请重试。",
33
+ "network_error": "连接失败,请检查网络后重试。",
34
+ "forbidden": "没有执行此操作的权限。",
35
+ "conflict": "账户已存在,或修改与当前数据冲突。",
36
+ "validation_failed": "请检查填写的信息后重试。",
37
+ "settings_description": "管理个人资料、偏好与账户安全",
38
+ "profile_title": "个人资料",
39
+ "preferences_title": "偏好设置",
40
+ "preferences_description": "语言与界面风格保存到个人账户,在不同设备上保持一致。",
41
+ "password_title": "修改密码",
42
+ "password_rules": "密码至少 8 个字符,请使用与旧密码不同的新密码。修改后需要重新登录。",
43
+ "password_changed": "密码已修改,请使用新密码重新登录。",
44
+ "avatar_hint": "支持 PNG、JPEG 和 WebP 图片,大小不超过 512 KB。",
45
+ "avatar_invalid": "请选择不超过 512 KB 的 PNG、JPEG 或 WebP 图片。",
46
+ "avatar_saved": "头像已更新",
47
+ "avatar_upload": "上传头像",
48
+ "users_description": "创建账户、分配角色并管理访问权限",
49
+ "users_search": "搜索用户名、姓名或邮箱",
50
+ "user_account": "账户",
51
+ "user_username": "用户名",
52
+ "user_display_name": "显示名称",
53
+ "user_email": "邮箱",
54
+ "user_password": "初始密码",
55
+ "user_role": "角色",
56
+ "user_status": "状态",
57
+ "user_create": "新增用户",
58
+ "initial_password_rules": "密码至少 8 个字符。",
59
+ "account_enabled": "启用账户",
60
+ "users_empty": "没有匹配的账户,请调整查询条件或新增用户。",
61
+ "user_edit": "编辑用户",
62
+ "user_deleted": "用户已删除",
63
+ "user_delete_title": "删除用户",
64
+ "user_delete_confirm": "确认删除 {name}?此操作将永久删除账户及其访问权限。",
65
+ "roles_description": "以两种角色简化权限管理,在用户管理中分配角色。",
66
+ "role_admin": "管理员",
67
+ "role_user": "普通用户",
68
+ "role_admin_capabilities": "管理用户、角色说明和审计日志,并修改个人设置。",
69
+ "role_user_capabilities": "修改个人资料、头像、语言、界面风格与密码。",
70
+ "role_members": "{count} 名成员",
71
+ "role_edit": "编辑说明",
72
+ "role_description": "角色说明",
73
+ "audit_description": "查看账户与管理操作的审计记录",
74
+ "audit_actor": "操作用户",
75
+ "audit_action": "操作",
76
+ "audit_resource": "资源",
77
+ "audit_resource_id": "资源 ID",
78
+ "audit_ip": "IP 地址",
79
+ "audit_start": "开始日期",
80
+ "audit_end": "结束日期",
81
+ "audit_action_placeholder": "全部操作",
82
+ "audit_resource_placeholder": "全部资源",
83
+ "audit_system": "系统",
84
+ "audit_details": "审计日志详情",
85
+ "audit_date_invalid": "结束日期不能早于开始日期。",
86
+ "api_not_configured": "尚未配置服务连接,请联系应用管理员。"
24
87
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "engines": { "node": ">=22.12.0" },
2
+ "engines": { "node": "^22.22.2 || ^24.15.0 || >=26.0.0" },
3
3
  "name": "{{fullstack.sharedPackageName}}",
4
4
  "type": "module",
5
5
  "version": "0.1.0",
@@ -31,10 +31,13 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@h-ai/api-client": "{{haiPackageSpecifier}}",
34
+ "@h-ai/core": "{{haiPackageSpecifier}}",
35
+ "@h-ai/crypto": "{{haiPackageSpecifier}}",
34
36
  "@h-ai/ui": "{{haiPackageSpecifier}}",
35
37
  "{{fullstack.contractPackageName}}": "workspace:*"
36
38
  },
37
39
  "devDependencies": {
40
+ "@sveltejs/vite-plugin-svelte": "{{fullstack.packageSpecifiers.sveltePlugin}}",
38
41
  "@inlang/paraglide-js": "{{fullstack.packageSpecifiers.inlangParaglide}}",
39
42
  "@inlang/plugin-message-format": "{{fullstack.packageSpecifiers.inlangMessageFormat}}",
40
43
  "svelte": "{{fullstack.packageSpecifiers.svelte}}",
@@ -1,8 +1,7 @@
1
1
  export { default as AppShell } from './lib/components/AppShell.svelte'
2
- export { default as LanguageSwitcher } from './lib/components/LanguageSwitcher.svelte'
3
- export { default as ThemeSwitcher } from './lib/components/ThemeSwitcher.svelte'
2
+ export { default as AdminWorkspace } from './lib/components/AdminWorkspace.svelte'
4
3
  export { AVAILABLE_THEMES, themeStore } from './lib/stores/theme-store.svelte.js'
5
- export type { AppInfo, FetchAppInfoOptions } from './lib/api/api-client.js'
6
- export { fetchAppInfo } from './lib/api/api-client.js'
4
+ export type { AppInfo, ManagementApiOptions } from './lib/api/api-client.js'
5
+ export { initializeManagementApi, managementApi } from './lib/api/api-client.js'
7
6
  export * as messages from './lib/paraglide/messages.js'
8
7
  export { getLocale, locales, setLocale } from './lib/paraglide/runtime.js'
@@ -1,39 +1,61 @@
1
+ import type { TokenStorage } from '@h-ai/api-client'
1
2
  import type { AppInfoOutputData } from '{{fullstack.contractPackageName}}'
2
- import { apiClient } from '@h-ai/api-client'
3
+ import { apiClient, HaiApiClientError } from '@h-ai/api-client'
4
+ import { err } from '@h-ai/core'
5
+ import { crypto } from '@h-ai/crypto'
3
6
  import { {{fullstack.contractExportName}} } from '{{fullstack.contractPackageName}}'
4
7
 
5
8
  export type AppInfo = AppInfoOutputData
9
+ export const managementApi = apiClient.create({{fullstack.contractExportName}})
6
10
 
7
11
  const DEFAULT_API_ORIGIN = 'http://127.0.0.1:3000'
8
12
 
9
- export interface FetchAppInfoOptions {
13
+ export interface ManagementApiOptions {
10
14
  fetchFn?: typeof fetch
11
15
  baseUrl?: string
12
16
  clientName?: string
17
+ tokenStorage?: TokenStorage
18
+ headers?: Record<string, string>
19
+ requireApiBase?: boolean
20
+ onSessionExpired?: () => void
13
21
  }
14
22
 
15
- export async function fetchAppInfo(options: FetchAppInfoOptions = {}): Promise<AppInfo> {
16
- const fetchFn = options.fetchFn ?? fetch
17
- const baseUrl = options.baseUrl ?? resolveDefaultApiOrigin()
18
- const clientName = options.clientName ?? '{{projectName}}-shared'
23
+ /** 所有业务请求由 api-client 统一完成 Token 注入、刷新和双向加密。 */
24
+ export async function initializeManagementApi(options: ManagementApiOptions = {}) {
25
+ if (options.requireApiBase && !options.baseUrl && !resolveRuntimeApiOrigin() && !import.meta.env.PUBLIC_API_BASE)
26
+ return err(HaiApiClientError.CONFIG_ERROR, 'Native applications require a configured API origin')
27
+ if (managementApi.isInitialized)
28
+ await managementApi.close()
29
+ if (!crypto.isInitialized) {
30
+ const result = await crypto.init()
31
+ if (!result.success)
32
+ return result
33
+ }
19
34
 
20
- const client = apiClient.create({{fullstack.contractExportName}})
21
- const init = await client.init({
22
- baseUrl: normalizeApiBaseUrl(baseUrl),
23
- clientName,
24
- fetch: fetchFn,
35
+ return managementApi.init({
36
+ baseUrl: normalizeApiBaseUrl(options.baseUrl ?? resolveDefaultApiOrigin()),
37
+ clientName: options.clientName ?? '{{fullstack.sharedPackageName}}',
38
+ fetch: createCredentialedFetch(options.fetchFn),
39
+ headers: options.headers,
40
+ auth: { storage: options.tokenStorage, onRefreshFailed: options.onSessionExpired },
41
+ transport: { crypto },
25
42
  })
26
- if (!init.success)
27
- throw new Error(init.error?.message ?? 'Failed to initialize API client')
43
+ }
28
44
 
45
+ /** 头像仅允许服务端约定资源路径,并统一指向当前可信 API origin。 */
46
+ export function resolveAvatarUrl(value: string | undefined): string | undefined {
47
+ if (!value)
48
+ return undefined
29
49
  try {
30
- const result = await client.app.info()
31
- if (!result.success)
32
- throw new Error(result.error?.message ?? 'Failed to fetch app info')
33
- return result.data
50
+ const origin = managementApi.config?.baseUrl ?? normalizeApiBaseUrl(resolveDefaultApiOrigin())
51
+ const parsed = new URL(value, origin)
52
+ const prefix = `${new URL(origin).pathname.replace(/\/+$/, '')}/assets/avatars/`
53
+ if (!parsed.pathname.startsWith(prefix) || !/^[\w-]+\/[\w-]+\.(?:png|jpg|webp)$/.test(parsed.pathname.slice(prefix.length)))
54
+ return undefined
55
+ return new URL(parsed.pathname, origin).toString()
34
56
  }
35
- finally {
36
- await client.close()
57
+ catch {
58
+ return undefined
37
59
  }
38
60
  }
39
61
 
@@ -43,6 +65,14 @@ function resolveDefaultApiOrigin(): string {
43
65
  || (typeof globalThis.location === 'undefined' ? DEFAULT_API_ORIGIN : globalThis.location.origin)
44
66
  }
45
67
 
68
+ /** 跨端浏览器请求携带服务端管理的 httpOnly refresh Cookie。 */
69
+ function createCredentialedFetch(fetchFn: typeof fetch = globalThis.fetch.bind(globalThis)): typeof fetch {
70
+ return (input, init) => {
71
+ const request = new Request(input, init)
72
+ return fetchFn(new Request(request, { credentials: 'include' }))
73
+ }
74
+ }
75
+
46
76
  function resolveRuntimeApiOrigin(): string | undefined {
47
77
  const runtimeConfig = (globalThis as typeof globalThis & {
48
78
  __HAI_PUBLIC_CONFIG__?: { apiBase?: string }
@@ -51,12 +81,8 @@ function resolveRuntimeApiOrigin(): string | undefined {
51
81
  }
52
82
 
53
83
  function normalizeApiBaseUrl(value: string): string {
54
- const trimmed = trimTrailingSlash(value)
55
- return trimmed.endsWith('/api/v1')
56
- ? trimmed
57
- : `${trimmed}/api/v1`
58
- }
59
-
60
- function trimTrailingSlash(value: string): string {
61
- return value.replace(/\/+$/, '')
84
+ const url = new URL(value.trim())
85
+ if (url.pathname === '/')
86
+ url.pathname = '/api/v1'
87
+ return url.toString().replace(/\/+$/, '')
62
88
  }
@@ -0,0 +1,73 @@
1
+ <script lang="ts">
2
+ import type { ManagementApiOptions } from '../api/api-client.js'
3
+ import { Alert, Skeleton } from '@h-ai/ui/components/compounds/index'
4
+ import { Button } from '@h-ai/ui/components/primitives/index'
5
+ import { LoginForm } from '@h-ai/ui/components/scenes/iam/index'
6
+ import { onMount } from 'svelte'
7
+ import * as m from '../paraglide/messages.js'
8
+ import { ManagementStore } from '../stores/management-store.svelte.js'
9
+ import AppShell from './AppShell.svelte'
10
+
11
+ interface Props {
12
+ platform: 'web' | 'app' | 'desktop'
13
+ apiOptions?: ManagementApiOptions
14
+ onLocaleChange?: (locale: string) => void
15
+ }
16
+ const { platform, apiOptions = {}, onLocaleChange }: Props = $props()
17
+ const store = new ManagementStore()
18
+ const loginErrors = $derived({ general: store.error })
19
+ const pageComponent = $derived.by(() => {
20
+ if (store.isAdmin && store.page === 'users')
21
+ return import('./UserManagement.svelte')
22
+ if (store.isAdmin && store.page === 'roles')
23
+ return import('./RoleManagement.svelte')
24
+ if (store.isAdmin && store.page === 'audit')
25
+ return import('./AuditManagement.svelte')
26
+ return import('./ProfileSettings.svelte')
27
+ })
28
+ onMount(() => {
29
+ void store.initialize(apiOptions, onLocaleChange)
30
+ return () => {
31
+ void store.close()
32
+ }
33
+ })
34
+ $effect(() => {
35
+ if (store.info)
36
+ document.title = store.info.name
37
+ })
38
+ </script>
39
+
40
+ {#key store.locale}
41
+ {#if store.initializing}
42
+ <div class="grid min-h-dvh place-items-center bg-base-200/50"><div class="w-72 space-y-4"><Skeleton variant="title" /><Skeleton count={3} /><p class="text-center text-sm text-base-content/50">{m.home_loading()}</p></div></div>
43
+ {:else if !store.user}
44
+ <main class="grid min-h-dvh place-items-center bg-base-200/50 px-4 py-10">
45
+ <section class="w-full max-w-md rounded-2xl border border-base-content/8 bg-base-100 p-7 shadow-xl shadow-base-content/3 sm:p-9" data-testid="login-page">
46
+ <div class="mb-7 flex items-center gap-3">
47
+ <span class="grid size-11 place-items-center rounded-xl bg-primary text-primary-content"><span class="icon-[tabler--hexagon-letter-h] size-6"></span></span>
48
+ <div><h1 class="text-lg font-semibold" data-testid="app-name">{store.info?.name || m.login_title()}</h1><p class="mt-1 text-xs text-base-content/45">{m.login_description()}</p></div>
49
+ </div>
50
+ {#if store.notice}<Alert variant="success" class="mb-5" data-testid="notice">{store.notice}</Alert>{/if}
51
+ {#if !store.info}
52
+ <Alert variant="error">{store.error || m.home_error()}</Alert>
53
+ <Button class="mt-5 w-full" variant="primary" onclick={() => void store.initialize(apiOptions, onLocaleChange)}>{m.home_refresh()}</Button>
54
+ {:else}
55
+ <LoginForm loading={store.saving} showRememberMe={false} showForgotPassword={false} errors={loginErrors} onsubmit={data => store.login(data)} data-testid="login-form" />
56
+ <p class="mt-6 text-center text-xs text-base-content/40"><span data-testid="app-version">v{store.info.version}</span> · <span data-testid="app-environment">{store.info.env}</span></p>
57
+ {/if}
58
+ </section>
59
+ </main>
60
+ {:else}
61
+ <AppShell {platform} {store}>
62
+ <div class="space-y-5">
63
+ {#if store.error}<Alert variant="error" data-testid="error">{store.error}</Alert>{/if}
64
+ {#if store.notice}<Alert variant="success" data-testid="notice">{store.notice}</Alert>{/if}
65
+ {#await pageComponent}
66
+ <div class="space-y-4"><Skeleton variant="title" /><Skeleton count={3} /></div>
67
+ {:then { default: Page }}
68
+ <Page {store} />
69
+ {/await}
70
+ </div>
71
+ </AppShell>
72
+ {/if}
73
+ {/key}
@@ -1,60 +1,70 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte'
3
+ import type { ManagementPage, ManagementStore } from '../stores/management-store.svelte.js'
4
+ import { Avatar, Badge, BareButton, Button } from '@h-ai/ui/components/primitives/index'
3
5
  import * as m from '../paraglide/messages.js'
4
- import LanguageSwitcher from './LanguageSwitcher.svelte'
5
- import ThemeSwitcher from './ThemeSwitcher.svelte'
6
6
 
7
7
  interface Props {
8
- /** 当前运行的前端平台 */
9
8
  platform: 'web' | 'app' | 'desktop' | 'miniapp'
10
- /** 宿主应用本地 Paraglide runtime 的同步回调 */
11
- onLocaleChange?: (locale: string) => void
12
- /** 主内容 */
9
+ store: ManagementStore
13
10
  children: Snippet
14
11
  }
12
+ const { platform, store, children }: Props = $props()
13
+ let menuOpen = $state(false)
14
+ const items = $derived([
15
+ ...(store.isAdmin
16
+ ? [
17
+ { id: 'users' as const, label: m.nav_users(), icon: 'icon-[tabler--users]' },
18
+ { id: 'roles' as const, label: m.nav_roles(), icon: 'icon-[tabler--shield-lock]' },
19
+ { id: 'audit' as const, label: m.nav_audit(), icon: 'icon-[tabler--file-text]' },
20
+ ]
21
+ : []),
22
+ { id: 'settings' as const, label: m.settings_title(), icon: 'icon-[tabler--settings]' },
23
+ ])
24
+ const title = $derived(items.find(item => item.id === store.page)?.label ?? m.settings_title())
15
25
 
16
- const { platform, onLocaleChange, children }: Props = $props()
17
-
18
- function getPlatformLabel(value: Props['platform']): string {
19
- if (value === 'web')
20
- return m.platform_web()
21
- if (value === 'app')
22
- return m.platform_app()
23
- if (value === 'desktop')
24
- return m.platform_desktop()
25
- return m.platform_miniapp()
26
+ function navigate(page: ManagementPage) {
27
+ menuOpen = false
28
+ void store.navigate(page)
26
29
  }
27
-
28
- const platformLabel = $derived(getPlatformLabel(platform))
29
30
  </script>
30
31
 
31
- <div class="min-h-screen flex flex-col overflow-hidden bg-base-100 text-base-content">
32
- <div class="pointer-events-none fixed inset-0 -z-10 bg-[radial-gradient(circle_at_top_left,var(--color-primary)_0,transparent_34%),radial-gradient(circle_at_bottom_right,var(--color-secondary)_0,transparent_30%)] opacity-10"></div>
33
-
34
- <header class="navbar sticky top-0 z-20 border-b border-base-300/70 bg-base-100/80 px-4 shadow-sm backdrop-blur-xl lg:px-8">
35
- <div class="flex-1 flex items-center gap-3">
36
- <div class="grid size-10 place-items-center rounded-2xl bg-primary text-primary-content shadow-lg shadow-primary/20">
37
- <span class="icon-[tabler--sparkles] size-5"></span>
32
+ <div class="flex min-h-dvh bg-base-200/50 text-base-content" class:hai-safe-area={platform === 'app'}>
33
+ {#if menuOpen}
34
+ <BareButton class="fixed inset-0 z-30 bg-black/30 lg:hidden" ariaLabel={m.close_menu()} onclick={() => { menuOpen = false }} />
35
+ {/if}
36
+ <aside class="fixed inset-y-0 left-0 z-40 flex w-60 flex-col border-r border-base-content/8 bg-base-100 transition-transform duration-200 lg:sticky lg:top-0 lg:h-dvh lg:translate-x-0 {menuOpen ? 'translate-x-0' : '-translate-x-full'}">
37
+ <div class="flex h-16 items-center gap-3 border-b border-base-content/8 px-5">
38
+ <span class="grid size-8 shrink-0 place-items-center rounded-lg bg-primary text-primary-content"><span class="icon-[tabler--hexagon-letter-h] size-5"></span></span>
39
+ <span class="truncate text-sm font-semibold" data-testid="app-name">{store.info?.name}</span>
40
+ </div>
41
+ <nav class="flex-1 space-y-1 p-3" aria-label={m.navigation()}>
42
+ {#each items as item (item.id)}
43
+ <BareButton class="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm transition-colors {store.page === item.id ? 'bg-primary/10 font-medium text-primary' : 'text-base-content/60 hover:bg-base-content/4'}" onclick={() => navigate(item.id)} data-testid="nav-{item.id}">
44
+ <span class="{item.icon} size-4.5"></span>{item.label}
45
+ </BareButton>
46
+ {/each}
47
+ </nav>
48
+ <div class="border-t border-base-content/8 p-4 text-xs text-base-content/40"><span data-testid="app-version">v{store.info?.version}</span></div>
49
+ </aside>
50
+ <div class="flex min-w-0 flex-1 flex-col">
51
+ <header class="sticky top-0 z-20 flex h-16 items-center justify-between gap-2 border-b border-base-content/8 bg-base-100/95 px-3 backdrop-blur sm:px-4 lg:px-6">
52
+ <div class="flex min-w-0 flex-1 items-center gap-2 sm:gap-3">
53
+ <BareButton class="rounded-lg p-2 hover:bg-base-content/5 lg:hidden" ariaLabel={m.open_menu()} onclick={() => { menuOpen = !menuOpen }} data-testid="mobile-menu"><span class="icon-[tabler--menu-2] size-5"></span></BareButton>
54
+ <span class="truncate text-sm font-medium">{title}</span>
38
55
  </div>
39
- <div>
40
- <span class="block text-lg font-semibold leading-tight">{m.app_name()}</span>
41
- <span class="text-xs text-base-content/55">{m.shell_tagline()}</span>
56
+ <div class="flex shrink-0 items-center gap-1.5 sm:gap-3">
57
+ <Badge variant="ghost" size="sm" data-testid="app-environment">{store.info?.env}</Badge>
58
+ {#if store.user}
59
+ <BareButton class="flex items-center gap-2 rounded-lg px-1 py-1 hover:bg-base-content/5 sm:px-2" ariaLabel={m.settings_title()} onclick={() => navigate('settings')}>
60
+ <Avatar name={store.user.displayName || store.user.username} src={store.user.avatarUrl} size="sm" />
61
+ <span class="hidden max-w-36 truncate text-sm sm:block">{store.user.displayName || store.user.username}</span>
62
+ </BareButton>
63
+ {/if}
64
+ <Button variant="ghost" size="sm" loading={store.saving} onclick={() => void store.logout()} data-testid="logout">{m.logout()}</Button>
42
65
  </div>
43
- <span class="badge badge-primary badge-outline ml-1 hidden sm:inline-flex">{platformLabel}</span>
44
- </div>
45
- <div class="flex-none flex items-end gap-2">
46
- <ThemeSwitcher />
47
- <LanguageSwitcher {onLocaleChange} />
48
- </div>
49
- </header>
50
-
51
- <main class="flex-1 px-4 py-8 lg:px-8 lg:py-10">
52
- <div class="mx-auto w-full max-w-6xl">
53
- {@render children()}
54
- </div>
55
- </main>
56
-
57
- <footer class="footer footer-center border-t border-base-300/70 bg-base-100/70 p-4 text-sm text-base-content/60 backdrop-blur-xl">
58
- <span>{m.shell_footer({ platform: platformLabel })}</span>
59
- </footer>
66
+ </header>
67
+ <main class="mx-auto w-full max-w-7xl flex-1 p-4 pb-8 sm:p-6 lg:p-8">{@render children()}</main>
68
+ <footer class="border-t border-base-content/8 px-6 py-3 text-center text-xs text-base-content/35">{store.info?.name} · v{store.info?.version}</footer>
69
+ </div>
60
70
  </div>