@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
@@ -0,0 +1,61 @@
1
+ <script lang="ts">
2
+ import type { AdminAuditLog } from '{{fullstack.contractPackageName}}'
3
+ import type { ManagementStore } from '../stores/management-store.svelte.js'
4
+ import { DataTable, Modal, Pagination } from '@h-ai/ui/components/compounds/index'
5
+ import { Button, Input } from '@h-ai/ui/components/primitives/index'
6
+ import * as m from '../paraglide/messages.js'
7
+
8
+ interface Props { store: ManagementStore }
9
+ const { store }: Props = $props()
10
+ let selected = $state<AdminAuditLog | null>(null)
11
+ const columns = $derived([
12
+ { key: 'createdAt', label: m.created_at() },
13
+ { key: 'username', label: m.audit_actor() },
14
+ { key: 'action', label: m.audit_action() },
15
+ { key: 'resource', label: m.audit_resource() },
16
+ { key: 'ipAddress', label: m.audit_ip() },
17
+ ])
18
+ </script>
19
+
20
+ <section class="space-y-5" data-testid="audit-page">
21
+ <div><h1 class="text-xl font-semibold tracking-tight">{m.nav_audit()}</h1><p class="mt-1 text-sm text-base-content/50">{m.audit_description()}</p></div>
22
+ <div class="rounded-xl border border-base-content/8 bg-base-100">
23
+ <form class="grid items-end gap-3 border-b border-base-content/8 p-4 sm:grid-cols-2 xl:grid-cols-[repeat(4,minmax(0,1fr))_auto] [&_.fieldset]:p-0" onsubmit={(event) => {
24
+ event.preventDefault()
25
+ void store.loadAudit()
26
+ }}>
27
+ <div class="min-w-0 space-y-1"><label class="block text-xs text-base-content/60" for="audit-action">{m.audit_action()}</label><Input id="audit-action" bind:value={store.auditAction} placeholder={m.audit_action_placeholder()} data-testid="audit-action" /></div>
28
+ <div class="min-w-0 space-y-1"><label class="block text-xs text-base-content/60" for="audit-resource">{m.audit_resource()}</label><Input id="audit-resource" bind:value={store.auditResource} placeholder={m.audit_resource_placeholder()} data-testid="audit-resource" /></div>
29
+ <div class="min-w-0 space-y-1"><label class="block text-xs text-base-content/60" for="audit-start">{m.audit_start()}</label><input id="audit-start" type="date" class="input block h-10 min-w-0 w-full" bind:value={store.auditStartDate} /></div>
30
+ <div class="min-w-0 space-y-1"><label class="block text-xs text-base-content/60" for="audit-end">{m.audit_end()}</label><input id="audit-end" type="date" class="input block h-10 min-w-0 w-full" bind:value={store.auditEndDate} /></div>
31
+ <Button type="submit" variant="outline" disabled={store.loading} class="h-10 w-full">{m.search()}</Button>
32
+ </form>
33
+ <div class="overflow-x-auto">
34
+ <DataTable data={store.audit} {columns} keyField="id" loading={store.loading} striped={false}>
35
+ {#snippet cell(entry, key)}
36
+ {#if key === 'createdAt'}<span class="whitespace-nowrap text-xs">{store.formatDate(entry.createdAt)}</span>
37
+ {:else if key === 'username'}{entry.username || m.audit_system()}
38
+ {:else if key === 'action'}<span class="rounded-md bg-base-200 px-2 py-1 font-mono text-xs">{entry.action}</span>
39
+ {:else if key === 'resource'}<span class="text-sm">{entry.resource}</span>
40
+ {:else if key === 'ipAddress'}<span class="text-xs text-base-content/55">{entry.ipAddress || '—'}</span>{/if}
41
+ {/snippet}
42
+ {#snippet actions(entry)}<Button size="xs" variant="ghost" onclick={() => { selected = entry }}>{m.details()}</Button>{/snippet}
43
+ </DataTable>
44
+ </div>
45
+ <div class="border-t border-base-content/8 p-4"><Pagination page={store.auditPage} total={store.auditTotal} pageSize={store.pageSize} onchange={page => void store.loadAudit(page)} /></div>
46
+ </div>
47
+ </section>
48
+
49
+ <Modal open={!!selected} title={m.audit_details()} onclose={() => { selected = null }}>
50
+ {#if selected}
51
+ <dl class="grid grid-cols-[auto_1fr] gap-x-5 gap-y-3 text-sm">
52
+ <dt class="text-base-content/50">{m.created_at()}</dt><dd>{store.formatDate(selected.createdAt)}</dd>
53
+ <dt class="text-base-content/50">{m.audit_actor()}</dt><dd class="break-all">{selected.username || m.audit_system()}</dd>
54
+ <dt class="text-base-content/50">{m.audit_action()}</dt><dd>{selected.action}</dd>
55
+ <dt class="text-base-content/50">{m.audit_resource()}</dt><dd>{selected.resource}</dd>
56
+ <dt class="text-base-content/50">{m.audit_resource_id()}</dt><dd class="break-all">{selected.resourceId || '—'}</dd>
57
+ <dt class="text-base-content/50">{m.audit_ip()}</dt><dd>{selected.ipAddress || '—'}</dd>
58
+ </dl>
59
+ <pre class="mt-5 max-h-72 overflow-auto whitespace-pre-wrap rounded-lg bg-base-200 p-4 text-xs">{selected.details || '—'}</pre>
60
+ {/if}
61
+ </Modal>
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ import type { ManagementStore } from '../stores/management-store.svelte.js'
3
+ import { Card } from '@h-ai/ui/components/compounds/index'
4
+ import { Avatar, Select } from '@h-ai/ui/components/primitives/index'
5
+ import { ChangePasswordForm, UserProfile } from '@h-ai/ui/components/scenes/iam/index'
6
+ import { getThemeInfo } from '@h-ai/ui/theme'
7
+ import * as m from '../paraglide/messages.js'
8
+ import { locales } from '../paraglide/runtime.js'
9
+ import { AVAILABLE_THEMES, themeStore } from '../stores/theme-store.svelte.js'
10
+
11
+ interface Props { store: ManagementStore }
12
+ const { store }: Props = $props()
13
+ const languageOptions = $derived(locales.filter(locale => store.info?.supportedLocales.includes(locale)).map(locale => ({ value: locale, label: locale === 'zh-CN' ? '简体中文' : 'English' })))
14
+ const themeOptions = AVAILABLE_THEMES.map(theme => ({ value: theme, label: getThemeInfo(theme)?.name ?? theme }))
15
+ </script>
16
+
17
+ <section class="space-y-5" data-testid="settings-page">
18
+ <div>
19
+ <h1 class="text-xl font-semibold tracking-tight">{m.settings_title()}</h1>
20
+ <p class="mt-1 text-sm text-base-content/50">{m.settings_description()}</p>
21
+ </div>
22
+ {#if store.user}
23
+ <div class="grid gap-5 xl:grid-cols-[0.75fr_1.25fr]">
24
+ <Card bordered shadow={false} class="h-fit">
25
+ <div class="flex items-center gap-4">
26
+ <Avatar name={store.user.displayName || store.user.username} src={store.user.avatarUrl} size="lg" />
27
+ <div class="min-w-0">
28
+ <h2 class="truncate text-lg font-semibold">{store.user.displayName || store.user.username}</h2>
29
+ <p class="text-sm text-base-content/50">@{store.user.username}</p>
30
+ <p class="mt-2 text-xs text-primary">{store.isAdmin ? m.role_admin() : m.role_user()}</p>
31
+ </div>
32
+ </div>
33
+ <p class="mt-4 text-xs leading-6 text-base-content/50">{m.avatar_hint()}</p>
34
+ </Card>
35
+ <Card title={m.profile_title()} bordered shadow={false}>
36
+ <UserProfile user={store.user} editable alwaysEditable fields={['avatar', 'displayName', 'email']} avatarAccept="image/png,image/jpeg,image/webp" avatarHint={m.avatar_hint()} loading={store.saving} onsubmit={data => store.saveProfile(data)} onavatarchange={file => void store.uploadAvatar(file)} data-testid="profile-form" />
37
+ </Card>
38
+ </div>
39
+ <Card title={m.preferences_title()} bordered shadow={false}>
40
+ <p class="mb-5 text-sm text-base-content/50">{m.preferences_description()}</p>
41
+ <div class="grid gap-6 sm:grid-cols-2">
42
+ <div class="space-y-2">
43
+ <label class="text-sm font-medium" for="profile-language">{m.settings_language()}</label>
44
+ <Select id="profile-language" value={store.locale} options={languageOptions} disabled={store.saving} onchange={locale => void store.savePreferences({ locale })} data-testid="profile-language" />
45
+ </div>
46
+ <div class="space-y-2">
47
+ <label class="text-sm font-medium" for="profile-theme">{m.settings_theme()}</label>
48
+ <Select id="profile-theme" value={themeStore.current} options={themeOptions} disabled={store.saving} onchange={theme => void store.savePreferences({ theme })} data-testid="profile-theme" />
49
+ </div>
50
+ </div>
51
+ </Card>
52
+ <Card title={m.password_title()} bordered shadow={false}>
53
+ <p class="mb-5 text-sm text-base-content/50">{m.password_rules()}</p>
54
+ <div class="max-w-xl"><ChangePasswordForm loading={store.saving} minPasswordLength={8} onsubmit={data => store.changePassword(data)} data-testid="password-form" /></div>
55
+ </Card>
56
+ {/if}
57
+ </section>
@@ -0,0 +1,49 @@
1
+ <script lang="ts">
2
+ import type { AdminRole } from '{{fullstack.contractPackageName}}'
3
+ import type { ManagementStore } from '../stores/management-store.svelte.js'
4
+ import { Card, Modal } from '@h-ai/ui/components/compounds/index'
5
+ import { Badge, Button, Textarea } from '@h-ai/ui/components/primitives/index'
6
+ import * as m from '../paraglide/messages.js'
7
+
8
+ interface Props { store: ManagementStore }
9
+ const { store }: Props = $props()
10
+ let editing = $state<AdminRole | null>(null)
11
+ let description = $state('')
12
+ function edit(role: AdminRole) {
13
+ editing = role
14
+ description = role.description ?? ''
15
+ store.error = ''
16
+ }
17
+ async function save(event: SubmitEvent) {
18
+ event.preventDefault()
19
+ if (editing && await store.saveRole(editing.id, description))
20
+ editing = null
21
+ }
22
+ </script>
23
+
24
+ <section class="space-y-5" data-testid="roles-page">
25
+ <div class="flex items-center justify-between gap-3">
26
+ <div><h1 class="text-xl font-semibold tracking-tight">{m.nav_roles()}</h1><p class="mt-1 text-sm text-base-content/50">{m.roles_description()}</p></div>
27
+ <Button variant="outline" loading={store.loading} onclick={() => void store.loadRoles()}>{m.home_refresh()}</Button>
28
+ </div>
29
+ <div class="grid gap-5 md:grid-cols-2">
30
+ {#each store.roles as role (role.id)}
31
+ <Card bordered shadow={false}>
32
+ <div class="flex items-center justify-between gap-3"><h2 class="text-lg font-semibold">{role.code === 'admin' ? m.role_admin() : m.role_user()}</h2><Badge variant={role.code === 'admin' ? 'primary' : 'ghost'}>{role.code}</Badge></div>
33
+ <p class="mt-3 text-sm leading-6 text-base-content/60">{role.code === 'admin' ? m.role_admin_capabilities() : m.role_user_capabilities()}</p>
34
+ <p class="mt-4 text-xs text-base-content/45">{m.role_members({ count: String(role.userCount) })}</p>
35
+ {#if role.description}<p class="mt-3 text-sm text-base-content/70">{role.description}</p>{/if}
36
+ <div class="mt-5"><Button variant="outline" size="sm" onclick={() => edit(role)}>{m.role_edit()}</Button></div>
37
+ </Card>
38
+ {/each}
39
+ </div>
40
+ </section>
41
+
42
+ <Modal open={!!editing} title={m.role_edit()} onclose={() => { editing = null }} closeOnBackdrop={!store.saving} closeOnEscape={!store.saving}>
43
+ <form class="space-y-4" onsubmit={save}>
44
+ <label class="block text-sm font-medium" for="role-description">{m.role_description()}</label>
45
+ <Textarea id="role-description" bind:value={description} rows={4} data-testid="role-description" />
46
+ {#if store.error}<p class="text-sm text-error" role="alert">{store.error}</p>{/if}
47
+ <div class="flex justify-end gap-2"><Button variant="ghost" disabled={store.saving} onclick={() => { editing = null }}>{m.cancel()}</Button><Button type="submit" variant="primary" loading={store.saving}>{m.save()}</Button></div>
48
+ </form>
49
+ </Modal>
@@ -0,0 +1,174 @@
1
+ <script lang="ts">
2
+ import type { AdminRoleCode, AdminUser } from '{{fullstack.contractPackageName}}'
3
+ import type { ManagementStore } from '../stores/management-store.svelte.js'
4
+ import { Card, DataTable, FormField, Modal, Pagination, Skeleton } from '@h-ai/ui/components/compounds/index'
5
+ import { Avatar, Badge, BareInput, Button, Checkbox, Input, Select } from '@h-ai/ui/components/primitives/index'
6
+ import { PasswordInput } from '@h-ai/ui/components/scenes/iam/index'
7
+ import * as m from '../paraglide/messages.js'
8
+
9
+ interface Props { store: ManagementStore }
10
+ const { store }: Props = $props()
11
+
12
+ let editorOpen = $state(false)
13
+ let editing = $state<AdminUser | null>(null)
14
+ let deleting = $state<AdminUser | null>(null)
15
+ let username = $state('')
16
+ let displayName = $state('')
17
+ let email = $state('')
18
+ let password = $state('')
19
+ let role = $state<AdminRoleCode>('user')
20
+ let enabled = $state(true)
21
+ const columns = $derived([
22
+ { key: 'username', label: m.user_account() },
23
+ { key: 'email', label: m.user_email() },
24
+ { key: 'roles', label: m.user_role() },
25
+ { key: 'enabled', label: m.user_status() },
26
+ { key: 'createdAt', label: m.created_at() },
27
+ ])
28
+ const roleOptions = $derived([{ value: 'admin' as const, label: m.role_admin() }, { value: 'user' as const, label: m.role_user() }])
29
+ const statusOptions = $derived([{ value: 'all', label: m.all_statuses() }, { value: 'enabled', label: m.enabled() }, { value: 'disabled', label: m.disabled() }])
30
+
31
+ function openEditor(user: AdminUser | null) {
32
+ editing = user
33
+ username = user?.username ?? ''
34
+ displayName = user?.displayName ?? ''
35
+ email = user?.email ?? ''
36
+ password = ''
37
+ role = user?.roles.includes('admin') ? 'admin' : 'user'
38
+ enabled = user?.enabled ?? true
39
+ store.error = ''
40
+ editorOpen = true
41
+ }
42
+
43
+ async function submit(event: SubmitEvent) {
44
+ event.preventDefault()
45
+ const fields = { displayName: displayName.trim(), role, enabled }
46
+ const saved = await store.saveUser(editing ? { id: editing.id, email: email.trim(), ...fields } : { username: username.trim(), password, email: email.trim() || undefined, ...fields })
47
+ if (saved) {
48
+ editorOpen = false
49
+ password = ''
50
+ }
51
+ }
52
+
53
+ async function confirmDelete() {
54
+ if (deleting && await store.deleteUser(deleting))
55
+ deleting = null
56
+ }
57
+
58
+ function upload(user: AdminUser, event: Event & { currentTarget: HTMLInputElement }) {
59
+ const file = event.currentTarget.files?.[0]
60
+ if (file)
61
+ void store.uploadAvatar(file, user.id)
62
+ event.currentTarget.value = ''
63
+ }
64
+ </script>
65
+
66
+ <section class="space-y-5" data-testid="users-page">
67
+ <div class="flex flex-wrap items-center justify-between gap-3">
68
+ <div>
69
+ <h1 class="text-xl font-semibold tracking-tight">{m.nav_users()}</h1>
70
+ <p class="mt-1 text-sm text-base-content/50">{m.users_description()}</p>
71
+ </div>
72
+ <Button variant="primary" onclick={() => openEditor(null)} data-testid="create-user">{m.user_create()}</Button>
73
+ </div>
74
+ <div class="rounded-xl border border-base-content/8 bg-base-100">
75
+ <form class="flex flex-wrap items-center gap-3 border-b border-base-content/8 p-4" onsubmit={(event) => {
76
+ event.preventDefault()
77
+ void store.loadUsers()
78
+ }}>
79
+ <div class="min-w-52 flex-1">
80
+ <Input type="search" bind:value={store.usersSearch} placeholder={m.users_search()} data-testid="users-search" />
81
+ </div>
82
+ <div class="w-40"><Select bind:value={store.usersStatus} options={statusOptions} /></div>
83
+ <Button type="submit" variant="outline" disabled={store.loading}>{m.search()}</Button>
84
+ </form>
85
+ <div class="hidden overflow-x-auto md:block">
86
+ <DataTable data={store.users} {columns} keyField="id" loading={store.loading} striped={false}>
87
+ {#snippet cell(user, key)}
88
+ {#if key === 'username'}
89
+ <div class="flex items-center gap-3">
90
+ <Avatar name={user.displayName || user.username} src={user.avatarUrl} size="sm" />
91
+ <div><p class="font-medium">{user.displayName || user.username}</p><p class="text-xs text-base-content/45">@{user.username}</p></div>
92
+ </div>
93
+ {:else if key === 'roles'}
94
+ <Badge variant={user.roles.includes('admin') ? 'primary' : 'ghost'}>{user.roles.includes('admin') ? m.role_admin() : m.role_user()}</Badge>
95
+ {:else if key === 'enabled'}
96
+ <Badge variant={user.enabled ? 'success' : 'ghost'}>{user.enabled ? m.enabled() : m.disabled()}</Badge>
97
+ {:else if key === 'createdAt'}
98
+ <span class="whitespace-nowrap text-xs text-base-content/60">{store.formatDate(user.createdAt)}</span>
99
+ {:else if key === 'email'}
100
+ {user.email || '—'}
101
+ {/if}
102
+ {/snippet}
103
+ {#snippet actions(user)}
104
+ {@render userActions(user)}
105
+ {/snippet}
106
+ </DataTable>
107
+ </div>
108
+ <div class="space-y-3 p-4 md:hidden" data-testid="user-cards">
109
+ {#if store.loading}
110
+ <Skeleton count={3} />
111
+ {:else if store.users.length === 0}
112
+ <p class="py-8 text-center text-sm text-base-content/50">{m.users_empty()}</p>
113
+ {:else}
114
+ {#each store.users as user (user.id)}
115
+ <Card bordered shadow={false}>
116
+ <div class="flex items-center gap-3">
117
+ <Avatar name={user.displayName || user.username} src={user.avatarUrl} size="sm" />
118
+ <div class="min-w-0 flex-1"><p class="truncate font-medium">{user.displayName || user.username}</p><p class="truncate text-xs text-base-content/45">@{user.username}</p></div>
119
+ <Badge size="sm" variant={user.enabled ? 'success' : 'ghost'}>{user.enabled ? m.enabled() : m.disabled()}</Badge>
120
+ </div>
121
+ <p class="mt-3 truncate text-xs text-base-content/60" title={user.email}>{user.email || '—'}</p>
122
+ <div class="mt-3 flex flex-wrap items-center justify-between gap-2">
123
+ <Badge size="sm" variant={user.roles.includes('admin') ? 'primary' : 'ghost'}>{user.roles.includes('admin') ? m.role_admin() : m.role_user()}</Badge>
124
+ {@render userActions(user)}
125
+ </div>
126
+ </Card>
127
+ {/each}
128
+ {/if}
129
+ </div>
130
+ <div class="border-t border-base-content/8 p-4">
131
+ <Pagination page={store.usersPage} total={store.usersTotal} pageSize={store.pageSize} onchange={page => void store.loadUsers(page)} />
132
+ </div>
133
+ </div>
134
+ </section>
135
+
136
+ <Modal bind:open={editorOpen} title={editing ? m.user_edit() : m.user_create()} closeOnBackdrop={!store.saving} closeOnEscape={!store.saving} data-testid="user-editor">
137
+ <form class="space-y-4" onsubmit={submit}>
138
+ <FormField label={m.user_username()} required>
139
+ <Input bind:value={username} name="username" readonly={!!editing} required minlength={3} maxlength={64} pattern="[\w\-]+" autocomplete="off" data-testid="user-username" />
140
+ </FormField>
141
+ <FormField label={m.user_display_name()}><Input bind:value={displayName} maxlength={100} data-testid="user-display-name" /></FormField>
142
+ <FormField label={m.user_email()}><Input type="email" bind:value={email} data-testid="user-email" /></FormField>
143
+ {#if !editing}
144
+ <FormField label={m.user_password()} hint={m.initial_password_rules()} required><PasswordInput bind:value={password} autocomplete="new-password" minLength={8} data-testid="user-password" /></FormField>
145
+ {/if}
146
+ <FormField label={m.user_role()}><Select bind:value={role} options={roleOptions} disabled={editing?.id === store.user?.id} data-testid="user-role" /></FormField>
147
+ <Checkbox bind:checked={enabled} label={m.account_enabled()} disabled={editing?.id === store.user?.id} />
148
+ {#if store.error}<p class="text-sm text-error" role="alert">{store.error}</p>{/if}
149
+ <div class="flex justify-end gap-2 border-t border-base-content/8 pt-4">
150
+ <Button variant="ghost" disabled={store.saving} onclick={() => { editorOpen = false }}>{m.cancel()}</Button>
151
+ <Button type="submit" variant="primary" loading={store.saving} disabled={!editing && password.length < 8} data-testid="user-save">{m.save()}</Button>
152
+ </div>
153
+ </form>
154
+ </Modal>
155
+
156
+ {#snippet userActions(user: AdminUser)}
157
+ <div class="flex items-center gap-1" data-testid="user-actions" data-user={user.username}>
158
+ <Button size="xs" variant="ghost" onclick={() => openEditor(user)}>{m.edit()}</Button>
159
+ <label class="cursor-pointer rounded-lg px-2 py-1.5 text-xs text-primary hover:bg-primary/5">
160
+ {m.avatar_upload()}
161
+ <BareInput type="file" class="sr-only" accept="image/png,image/jpeg,image/webp" disabled={store.saving} onchange={event => upload(user, event)} data-testid="user-avatar-input" />
162
+ </label>
163
+ <Button size="xs" variant="ghost" class="text-error" disabled={user.id === store.user?.id || store.saving} onclick={() => { deleting = user }}>{m.delete()}</Button>
164
+ </div>
165
+ {/snippet}
166
+
167
+ <Modal open={!!deleting} title={m.user_delete_title()} onclose={() => { deleting = null }} closeOnBackdrop={!store.saving} closeOnEscape={!store.saving}>
168
+ <p class="text-sm leading-6 text-base-content/70">{m.user_delete_confirm({ name: deleting?.displayName || deleting?.username || '' })}</p>
169
+ {#if store.error}<p class="mt-3 text-sm text-error" role="alert">{store.error}</p>{/if}
170
+ {#snippet footer()}
171
+ <Button variant="ghost" disabled={store.saving} onclick={() => { deleting = null }}>{m.cancel()}</Button>
172
+ <Button variant="error" loading={store.saving} onclick={confirmDelete} data-testid="confirm-delete-user">{m.delete()}</Button>
173
+ {/snippet}
174
+ </Modal>