@h-ai/cli 0.1.0-alpha.67 → 0.1.0-alpha.68

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 (164) hide show
  1. package/README.en.md +40 -4
  2. package/README.ja.md +40 -4
  3. package/README.md +40 -4
  4. package/dist/{src-2V6VP4I5.js → chunk-FI56GONY.js} +32 -3103
  5. package/dist/chunk-FI56GONY.js.map +1 -0
  6. package/dist/chunk-MBPB37FR.js +1348 -0
  7. package/dist/chunk-MBPB37FR.js.map +1 -0
  8. package/dist/{chunk-7DSURTJ4.js → chunk-ZFNBUF77.js} +327 -199
  9. package/dist/chunk-ZFNBUF77.js.map +1 -0
  10. package/dist/cli-config-templates-W2LLPMB6.js +5 -0
  11. package/dist/{cli-config-templates-6AHNUGBN.js.map → cli-config-templates-W2LLPMB6.js.map} +1 -1
  12. package/dist/{cli-skill-templates-FC3QRKDO.js → cli-skill-templates-US5CCAJ4.js} +3 -3
  13. package/dist/{cli-skill-templates-FC3QRKDO.js.map → cli-skill-templates-US5CCAJ4.js.map} +1 -1
  14. package/dist/index.js +920 -336
  15. package/dist/index.js.map +1 -1
  16. package/dist/metafile-esm.json +1 -0
  17. package/dist/src-DSJJXM2F.js +3849 -0
  18. package/dist/src-DSJJXM2F.js.map +1 -0
  19. package/package.json +9 -8
  20. package/templates/apps/admin/messages/ja-JP.json +38 -0
  21. package/templates/apps/admin/src/routes/+layout.svelte.hbs +16 -6
  22. package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +3 -3
  23. package/templates/apps/api/apps/{{projectName}}-contract/src/index.ts.hbs +2 -0
  24. package/templates/apps/api/apps/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +4 -1
  25. package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +5 -1
  26. package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +0 -10
  27. package/templates/apps/api/apps/{{projectName}}-service/config/_serv.yml.hbs +2 -1
  28. package/templates/apps/api/apps/{{projectName}}-service/src/app.ts.hbs +24 -11
  29. package/templates/apps/api/apps/{{projectName}}-service/src/index.ts.hbs +8 -1
  30. package/templates/apps/api/apps/{{projectName}}-service/src/lib/server/init.ts.hbs +68 -11
  31. package/templates/apps/api/apps/{{projectName}}-service/src/server/procedures/index.ts.hbs +10 -2
  32. package/templates/apps/api/e2e/api-service.spec.ts.hbs +2 -2
  33. package/templates/apps/api/package.json.hbs +1 -1
  34. package/templates/apps/api/playwright.config.ts.hbs +15 -7
  35. package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +1 -1
  36. package/templates/apps/fullstack/apps/{{projectName}}-app/messages/ja-JP.json.hbs +7 -0
  37. package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +7 -1
  38. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +1 -1
  39. package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +1 -1
  40. package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/ja-JP.json.hbs +7 -0
  41. package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +1 -1
  42. package/templates/apps/fullstack/apps/{{projectName}}-miniapp/messages/ja-JP.json.hbs +12 -0
  43. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +1 -1
  44. package/templates/apps/fullstack/apps/{{projectName}}-web/messages/ja-JP.json.hbs +7 -0
  45. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +1 -1
  46. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +19 -8
  47. package/templates/apps/fullstack/package.json.hbs +1 -1
  48. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +1 -1
  49. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/index.ts.hbs +2 -0
  50. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +3 -0
  51. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +5 -2
  52. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_core.yml.hbs +0 -6
  53. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +8 -0
  54. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +2 -1
  55. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +6 -0
  56. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +9 -5
  57. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +3 -2
  58. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/messages.ts.hbs +16 -1
  59. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +7 -3
  60. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +3 -0
  61. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +58 -7
  62. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +13 -3
  63. package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +9 -4
  64. package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/ja-JP.json.hbs +105 -0
  65. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +3 -3
  66. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +5 -5
  67. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/audit.svelte.hbs +7 -7
  68. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +1 -1
  69. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/users.svelte.hbs +8 -8
  70. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +11 -12
  71. package/templates/apps/fullstack/playwright.config.ts.hbs +15 -6
  72. package/templates/apps/h5/messages/ja-JP.json +40 -0
  73. package/templates/apps/h5/src/routes/+layout.svelte.hbs +14 -3
  74. package/templates/apps/mobile-app/messages/ja-JP.json +36 -0
  75. package/templates/apps/mobile-app/src/App.svelte.hbs +13 -3
  76. package/templates/apps/mobile-app/src/lib/api.ts.hbs +173 -14
  77. package/templates/apps/website/messages/ja-JP.json +32 -0
  78. package/templates/apps/website/src/routes/+layout.svelte.hbs +10 -2
  79. package/templates/base/e2e/app.spec.ts.hbs +13 -8
  80. package/templates/base/package.json.hbs +4 -1
  81. package/templates/base/playwright.config.ts +15 -4
  82. package/templates/base/src/hooks.server.ts.hbs +15 -6
  83. package/templates/base/src/lib/server/init.ts.hbs +139 -30
  84. package/templates/base/vite.config.ts.hbs +2 -2
  85. package/templates/features/iam/routes-shared/api/auth/logout/+server.ts +11 -6
  86. package/templates/features/payment/routes/api/v1/payment/create/+server.ts +18 -25
  87. package/templates/features/payment/routes/api/v1/payment/notify/[provider]/+server.ts +4 -0
  88. package/templates/i18n/project.inlang/settings.json +1 -1
  89. package/templates/shared/e2e-env.ts +98 -0
  90. package/templates/shared/run-e2e.mjs +51 -12
  91. package/templates/skills/AGENTS.md +4 -0
  92. package/templates/skills/bridges/admin/AGENTS.md +5 -1
  93. package/templates/skills/bridges/api/AGENTS.md +2 -0
  94. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +4 -0
  95. package/templates/skills/bridges/h5/AGENTS.md +5 -1
  96. package/templates/skills/bridges/mobile-app/AGENTS.md +5 -1
  97. package/templates/skills/bridges/website/AGENTS.md +5 -1
  98. package/templates/skills/hai-ai/SKILL.md +8 -2
  99. package/templates/skills/hai-ai/guide.md +134 -60
  100. package/templates/skills/hai-ai/reference.md +51 -11
  101. package/templates/skills/hai-api-client/SKILL.md +2 -0
  102. package/templates/skills/hai-api-client/reference.md +17 -5
  103. package/templates/skills/hai-api-contract/SKILL.md +1 -0
  104. package/templates/skills/hai-api-contract/reference.md +4 -2
  105. package/templates/skills/hai-app-create/SKILL.md +4 -0
  106. package/templates/skills/hai-app-create/development.md +20 -4
  107. package/templates/skills/hai-app-create/reference.md +2 -1
  108. package/templates/skills/hai-app-review/SKILL.md +6 -2
  109. package/templates/skills/hai-app-review/differential.md +34 -0
  110. package/templates/skills/hai-app-tests/SKILL.md +3 -1
  111. package/templates/skills/hai-app-tests/reference.md +4 -4
  112. package/templates/skills/hai-app-tests/validation.md +10 -3
  113. package/templates/skills/hai-audit/SKILL.md +10 -8
  114. package/templates/skills/hai-audit/reference.md +3 -0
  115. package/templates/skills/hai-build/reference.md +6 -4
  116. package/templates/skills/hai-cache/SKILL.md +9 -3
  117. package/templates/skills/hai-cache/reference.md +53 -11
  118. package/templates/skills/hai-capacitor/SKILL.md +3 -1
  119. package/templates/skills/hai-capacitor/reference.md +9 -5
  120. package/templates/skills/hai-core/SKILL.md +4 -0
  121. package/templates/skills/hai-core/reference.md +25 -5
  122. package/templates/skills/hai-crypto/SKILL.md +10 -2
  123. package/templates/skills/hai-crypto/reference.md +64 -10
  124. package/templates/skills/hai-datapipe/SKILL.md +2 -0
  125. package/templates/skills/hai-datapipe/reference.md +3 -1
  126. package/templates/skills/hai-deploy/SKILL.md +9 -4
  127. package/templates/skills/hai-deploy/middleware.md +51 -37
  128. package/templates/skills/hai-iam/SKILL.md +24 -8
  129. package/templates/skills/hai-iam/reference.md +181 -154
  130. package/templates/skills/hai-kit/SKILL.md +23 -7
  131. package/templates/skills/hai-kit/reference.md +42 -24
  132. package/templates/skills/hai-payment/SKILL.md +13 -11
  133. package/templates/skills/hai-payment/reference.md +80 -41
  134. package/templates/skills/hai-pr-review/SKILL.md +21 -88
  135. package/templates/skills/hai-reach/SKILL.md +5 -2
  136. package/templates/skills/hai-reach/reference.md +83 -22
  137. package/templates/skills/hai-reldb/SKILL.md +5 -2
  138. package/templates/skills/hai-reldb/reference.md +68 -25
  139. package/templates/skills/hai-scheduler/SKILL.md +8 -5
  140. package/templates/skills/hai-scheduler/reference.md +38 -24
  141. package/templates/skills/hai-serv/SKILL.md +23 -8
  142. package/templates/skills/hai-serv/reference.md +123 -87
  143. package/templates/skills/hai-spec-implement/SKILL.md +4 -0
  144. package/templates/skills/hai-spec-implement/evaluation.md +18 -0
  145. package/templates/skills/hai-spec-implement/handoff.md +47 -0
  146. package/templates/skills/hai-spec-implement/templates.md +5 -0
  147. package/templates/skills/hai-spec-implement/workflow.md +2 -0
  148. package/templates/skills/hai-spec-review/SKILL.md +4 -0
  149. package/templates/skills/hai-storage/SKILL.md +8 -2
  150. package/templates/skills/hai-storage/reference.md +64 -25
  151. package/templates/skills/hai-ui/SKILL.md +16 -2
  152. package/templates/skills/hai-ui/guide.md +3 -3
  153. package/templates/skills/hai-ui/reference.md +17 -6
  154. package/templates/skills/hai-ui-design/SKILL.md +31 -0
  155. package/templates/skills/hai-ui-design/patterns.md +26 -0
  156. package/templates/skills/hai-ui-review/SKILL.md +30 -0
  157. package/templates/skills/hai-ui-review/checklist.md +22 -0
  158. package/templates/skills/hai-vecdb/SKILL.md +12 -8
  159. package/templates/skills/hai-vecdb/reference.md +33 -3
  160. package/dist/chunk-7DSURTJ4.js.map +0 -1
  161. package/dist/chunk-AOQ4N657.js +0 -640
  162. package/dist/chunk-AOQ4N657.js.map +0 -1
  163. package/dist/cli-config-templates-6AHNUGBN.js +0 -5
  164. package/dist/src-2V6VP4I5.js.map +0 -1
@@ -0,0 +1,105 @@
1
+ {{! @skipIfEmpty }}
2
+ {
3
+ "$schema": "https://inlang.com/schema/inlang-message-format",
4
+ "settings_title": "個人設定",
5
+ "settings_language": "言語",
6
+ "settings_theme": "テーマ",
7
+ "home_refresh": "更新",
8
+ "home_loading": "読み込み中…",
9
+ "home_error": "APIサービスに接続できません",
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": "現在と異なるパスワードを選んでください。",
43
+ "password_changed": "パスワードを更新しました。新しいパスワードでログインしてください。",
44
+ "avatar_hint": "PNG、JPEG、WebPの画像をアップロードしてください。最大512KBです。",
45
+ "avatar_invalid": "512KB以下の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": "{min}文字以上で入力してください。",
59
+ "account_enabled": "ログインを許可",
60
+ "users_empty": "一致するアカウントがありません。検索条件を変更するか、ユーザーを作成してください。",
61
+ "user_edit": "ユーザーを編集",
62
+ "user_deleted": "ユーザーを削除しました",
63
+ "user_delete_title": "ユーザーを削除",
64
+ "user_delete_confirm": "{name}を削除しますか?アカウントとアクセス権は完全に削除されます。",
65
+ "roles_description": "2つのロールでアクセスを管理します。ロールはユーザー管理から割り当ててください。",
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": "サービスへの接続が設定されていません。管理者にお問い合わせください。",
87
+ "guide_overview_title": "管理ワークスペースを見てみましょう",
88
+ "guide_overview_description": "簡単な手順でワークスペース、個人設定、ヘルプを紹介します。いつでも終了でき、ヘルプセンターから再開できます。",
89
+ "guide_content_title": "ワークスペース",
90
+ "guide_content_description": "現在のページがここに表示されます。ナビゲーションで機能を切り替えます。利用できる操作は権限に応じて変わります。",
91
+ "guide_theme_title": "外観",
92
+ "guide_theme_description": "ライトとダークの表示を切り替えます。設定でさらに多くのテーマを選択できます。",
93
+ "guide_profile_title": "プロフィール",
94
+ "guide_profile_description": "アバターからプロフィールや設定を開き、個人情報やパスワードを管理できます。",
95
+ "guide_help_title": "ヘルプセンター",
96
+ "guide_help_description": "困ったときは、ここで説明を検索したり、製品ツアーを再生したりできます。",
97
+ "guide_profile_tour_title": "プロフィールを設定",
98
+ "guide_profile_tour_description": "個人情報、環境設定、パスワード設定の場所を紹介します。ツアーでは変更を送信しません。",
99
+ "guide_profile_form_description": "プロフィールの表示や編集、アバターの選択、変更の保存ができます。アップロードするファイルは、画面に表示された形式とサイズの条件を満たす必要があります。",
100
+ "guide_preferences_description": "言語とテーマの設定を保存します。画面とヘルプの言語は選択に従います。",
101
+ "guide_password_title": "パスワードを変更",
102
+ "guide_password_description": "現在のパスワード、新しいパスワード、確認用パスワードを入力します。送信前にパスワードのルールを確認してください。",
103
+ "guide_overview_article": "## はじめに\n\n- ナビゲーションから各機能を開きます。メニューと操作は現在の権限に応じて変わります。\n- ワークスペースには一覧、詳細、設定が表示されます。画面の説明に従い、送信前に入力内容を確認してください。\n- アバターからプロフィールと設定を開きます。\n- ヘルプボタンで説明を検索したり、ツアーを再生したりできます。\n\n## 初回ツアー\n\n「後で通知」を選ぶと1日間通知を停止します。「今後通知しない」は設定を保存します。完了したツアーは自動で表示されなくなりますが、手動で再生できます。",
104
+ "guide_profile_article": "## プロフィールと環境設定\n\nプロフィールを開くと個人情報やアバターを確認・更新できます。ファイルの条件を確認し、変更を保存して結果のメッセージを確認してください。外観と言語は設定で変更できます。\n\n## パスワード\n\nパスワードの変更には、現在のパスワード、新しいパスワード、確認用パスワードが必要です。画面のルールに従い、送信後に結果を確認してください。\n\n「ツアーを開始」で関連するページを開きます。ツアーが自動でフォームを保存したり、アカウントを変更したりすることはありません。"
105
+ }
@@ -59,7 +59,7 @@
59
59
 
60
60
  {#key store.locale}
61
61
  {#if store.initializing}
62
- <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>
62
+ <div role='status' aria-live='polite' 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>
63
63
  {:else if !store.user || resetToken}
64
64
  <header class='border-b border-base-content/8 bg-base-100 px-6 py-3 text-sm font-medium'>{store.info?.name}</header>
65
65
  <main data-testid='login-page'>
@@ -78,11 +78,11 @@
78
78
  </main>
79
79
  {:else}
80
80
  <AppShell {platform} {store} onhelp={() => guide.openHelp()}>
81
- <div class='space-y-5'>
81
+ <div class='flex min-h-0 flex-1 flex-col gap-5'>
82
82
  {#if store.error}<Alert variant='error' data-testid='error'>{store.error}</Alert>{/if}
83
83
  {#if store.notice}<Alert variant='success' data-testid='notice'>{store.notice}</Alert>{/if}
84
84
  {#await pageComponent}
85
- <div class='space-y-4'><Skeleton variant='title' /><Skeleton count={3} /></div>
85
+ <div role='status' aria-label={m.home_loading()} class='space-y-4'><Skeleton variant='title' /><Skeleton count={3} /></div>
86
86
  {:then { default: Page }}
87
87
  <Page {store} />
88
88
  {/await}
@@ -30,7 +30,7 @@
30
30
  }
31
31
  </script>
32
32
 
33
- <div class="flex min-h-dvh bg-base-200/50 text-base-content" class:hai-safe-area={platform === 'app'}>
33
+ <div class="flex h-dvh overflow-hidden bg-base-200/50 text-base-content" class:hai-safe-area={platform === 'app'}>
34
34
  {#if menuOpen}
35
35
  <BareButton class="fixed inset-0 z-30 bg-black/30 lg:hidden" ariaLabel={m.close_menu()} onclick={() => { menuOpen = false }} />
36
36
  {/if}
@@ -48,8 +48,8 @@
48
48
  </nav>
49
49
  <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>
50
50
  </aside>
51
- <div class="flex min-w-0 flex-1 flex-col">
52
- <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">
51
+ <div class="flex min-h-0 min-w-0 flex-1 flex-col">
52
+ <header class="sticky top-0 z-20 flex h-16 shrink-0 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">
53
53
  <div class="flex min-w-0 flex-1 items-center gap-2 sm:gap-3">
54
54
  <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>
55
55
  <span class="truncate text-sm font-medium">{title}</span>
@@ -66,7 +66,7 @@
66
66
  <Button variant="ghost" size="sm" loading={store.saving} onclick={() => void store.logout()} data-testid="logout">{m.logout()}</Button>
67
67
  </div>
68
68
  </header>
69
- <main class="w-full min-w-0 flex-1 p-4 pb-8 sm:p-6 lg:p-8" data-guide="admin-content">{@render children()}</main>
70
- <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
+ <main class="flex min-h-0 w-full min-w-0 flex-1 flex-col overflow-auto p-4 pb-8 sm:p-6 lg:p-8" data-guide="admin-content">{@render children()}</main>
70
+ <footer class="shrink-0 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>
71
71
  </div>
72
72
  </div>
@@ -17,10 +17,10 @@
17
17
  ])
18
18
  </script>
19
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) => {
20
+ <section class="flex min-h-0 flex-1 flex-col gap-5" data-testid="audit-page">
21
+ <div class="shrink-0"><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="flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl border border-base-content/8 bg-base-100">
23
+ <form class="grid shrink-0 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
24
  event.preventDefault()
25
25
  void store.loadAudit()
26
26
  }}>
@@ -30,8 +30,8 @@
30
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
31
  <Button type="submit" variant="outline" disabled={store.loading} class="h-10 w-full">{m.search()}</Button>
32
32
  </form>
33
- <div class="overflow-x-auto">
34
- <DataTable data={store.audit} {columns} keyField="id" loading={store.loading} striped={false}>
33
+ <div class="flex min-h-0 flex-1 flex-col overflow-auto">
34
+ <DataTable data={store.audit} {columns} keyField="id" loading={store.loading} striped={false} class="min-h-0 flex-1 overflow-auto">
35
35
  {#snippet cell(entry, key)}
36
36
  {#if key === 'createdAt'}<span class="whitespace-nowrap text-xs">{store.formatDate(entry.createdAt)}</span>
37
37
  {:else if key === 'username'}{entry.username || m.audit_system()}
@@ -42,7 +42,7 @@
42
42
  {#snippet actions(entry)}<Button size="xs" variant="ghost" onclick={() => { selected = entry }}>{m.details()}</Button>{/snippet}
43
43
  </DataTable>
44
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>
45
+ <div class="mt-auto shrink-0 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
46
  </div>
47
47
  </section>
48
48
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  interface Props { store: ManagementStore }
12
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' })))
13
+ const languageOptions = $derived(locales.filter(locale => store.info?.supportedLocales.includes(locale)).map(locale => ({ value: locale, label: locale === 'zh-CN' ? '简体中文' : locale === 'ja-JP' ? '日本語' : 'English' })))
14
14
  const themeOptions = AVAILABLE_THEMES.map(theme => ({ value: theme, label: getThemeInfo(theme)?.name ?? theme }))
15
15
  </script>
16
16
 
@@ -63,16 +63,16 @@
63
63
  }
64
64
  </script>
65
65
 
66
- <section class="space-y-5" data-testid="users-page">
67
- <div class="flex flex-wrap items-center justify-between gap-3">
66
+ <section class="flex min-h-0 flex-1 flex-col gap-5" data-testid="users-page">
67
+ <div class="flex shrink-0 flex-wrap items-center justify-between gap-3">
68
68
  <div>
69
69
  <h1 class="text-xl font-semibold tracking-tight">{m.nav_users()}</h1>
70
70
  <p class="mt-1 text-sm text-base-content/50">{m.users_description()}</p>
71
71
  </div>
72
72
  <Button variant="primary" onclick={() => openEditor(null)} data-testid="create-user">{m.user_create()}</Button>
73
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) => {
74
+ <div class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl border border-base-content/8 bg-base-100">
75
+ <form class="flex shrink-0 flex-wrap items-center gap-3 border-b border-base-content/8 p-4" onsubmit={(event) => {
76
76
  event.preventDefault()
77
77
  void store.loadUsers()
78
78
  }}>
@@ -82,8 +82,8 @@
82
82
  <div class="w-40"><Select bind:value={store.usersStatus} options={statusOptions} /></div>
83
83
  <Button type="submit" variant="outline" disabled={store.loading}>{m.search()}</Button>
84
84
  </form>
85
- <div class="hidden overflow-x-auto md:block">
86
- <DataTable data={store.users} {columns} keyField="id" loading={store.loading} striped={false}>
85
+ <div class="hidden min-h-0 flex-1 overflow-auto md:flex md:flex-col">
86
+ <DataTable data={store.users} {columns} keyField="id" loading={store.loading} striped={false} class="min-h-0 flex-1 overflow-auto">
87
87
  {#snippet cell(user, key)}
88
88
  {#if key === 'username'}
89
89
  <div class="flex items-center gap-3">
@@ -105,7 +105,7 @@
105
105
  {/snippet}
106
106
  </DataTable>
107
107
  </div>
108
- <div class="space-y-3 p-4 md:hidden" data-testid="user-cards">
108
+ <div class="min-h-0 flex-1 space-y-3 overflow-auto p-4 md:hidden" data-testid="user-cards">
109
109
  {#if store.loading}
110
110
  <Skeleton count={3} />
111
111
  {:else if store.users.length === 0}
@@ -127,7 +127,7 @@
127
127
  {/each}
128
128
  {/if}
129
129
  </div>
130
- <div class="border-t border-base-content/8 p-4">
130
+ <div class="mt-auto shrink-0 border-t border-base-content/8 p-4">
131
131
  <Pagination page={store.usersPage} total={store.usersTotal} pageSize={store.pageSize} onchange={page => void store.loadUsers(page)} />
132
132
  </div>
133
133
  </div>
@@ -1,6 +1,5 @@
1
- import type { EncryptedPayload } from '@h-ai/crypto'
2
1
  import { HaiApiClientError } from '@h-ai/api-client'
3
- import { crypto, TRANSPORT_PROTOCOL } from '@h-ai/crypto'
2
+ import { authenticateTransportRequest, createInMemoryKeyStore, crypto, TRANSPORT_PROTOCOL } from '@h-ai/crypto'
4
3
  import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
5
4
  import { initializeManagementApi, managementApi, resolveAvatarUrl } from '../src/lib/api/api-client.js'
6
5
 
@@ -16,7 +15,10 @@ const runtimeInfo = {
16
15
 
17
16
  /** 使用真实 crypto 公共入口模拟对端,覆盖握手及双向加解密。 */
18
17
  function createServerFetch() {
19
- const created = crypto.transport.createServer()
18
+ const key = crypto.asymmetric.generateKeyPair()
19
+ if (!key.success)
20
+ throw new Error(key.error.message)
21
+ const created = crypto.transport.createServer({ serverKeyRing: { activeKeyId: 'test-v1', keys: { 'test-v1': key.data } }, keyStore: createInMemoryKeyStore() })
20
22
  if (!created.success)
21
23
  throw new Error(created.error.message)
22
24
  const server = created.data
@@ -29,25 +31,22 @@ function createServerFetch() {
29
31
  if (url.pathname.endsWith('/_hai/key-exchange')) {
30
32
  const body = await request.json() as { clientPublicKey: string }
31
33
  const clientId = await server.registerClientKey(body.clientPublicKey)
32
- return Response.json({ serverPublicKey: server.getServerPublicKey(), clientId })
34
+ return Response.json({ serverKeyId: server.getServerKeyId(), serverPublicKey: server.getServerPublicKey(), clientId })
33
35
  }
34
36
  const clientId = request.headers.get(TRANSPORT_PROTOCOL.CLIENT_ID_HEADER)
35
37
  if (!clientId)
36
38
  return Response.json({ success: false }, { status: 400 })
39
+ const verified = await authenticateTransportRequest(server, request)
40
+ if (!verified.success)
41
+ throw new Error(verified.error.message)
37
42
  let data: unknown = runtimeInfo
38
43
  if (url.pathname.endsWith('/app/echo')) {
39
44
  expect(request.headers.get(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER)).toBe(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE)
40
- const encrypted = await request.json() as EncryptedPayload
41
- expect(JSON.stringify(encrypted)).not.toContain('private message')
42
- const decrypted = server.decryptRequest(encrypted)
43
- expect(decrypted.success).toBe(true)
44
- if (!decrypted.success)
45
- throw new Error(decrypted.error.message)
46
- const payload = JSON.parse(decrypted.data) as { message: string }
45
+ const payload = JSON.parse(verified.data.plaintext) as { message: string }
47
46
  messages.push(payload.message)
48
47
  data = { message: payload.message, echoedAt: '2026-01-01T00:00:00.000Z', requestId: 'test-request' }
49
48
  }
50
- const response = await server.encryptResponse(clientId, JSON.stringify({ success: true, data }))
49
+ const response = await server.encryptResponse(verified.data.context, JSON.stringify({ success: true, data }), 200)
51
50
  if (!response.success)
52
51
  throw new Error(response.error.message)
53
52
  return Response.json(response.data, { headers: { [TRANSPORT_PROTOCOL.ENCRYPTED_HEADER]: TRANSPORT_PROTOCOL.ENCRYPTED_HEADER_VALUE } })
@@ -1,5 +1,6 @@
1
1
  import process from 'node:process'
2
2
  import { defineConfig } from '@playwright/test'
3
+ import { createE2eNotificationEnv, createE2eServerEnv, createE2eTransportRing } from './scripts/e2e-env.js'
3
4
 
4
5
  const defaultBaseURL = '{{#if fullstack.e2eFrontend}}http://127.0.0.1:{{fullstack.e2eFrontend.port}}{{else}}http://127.0.0.1:3000{{/if}}'
5
6
  const defaultServiceBaseURL = 'http://127.0.0.1:3000'
@@ -49,32 +50,39 @@ function resolvePortFromBaseUrl(urlString: string, fallbackPort: string): string
49
50
 
50
51
  export default defineConfig({
51
52
  testDir: './e2e',
53
+ // 多个界面验收共用管理员资料与引导状态,保留串行以免互相覆盖。
52
54
  fullyParallel: false,
53
55
  forbidOnly: !!process.env.CI,
54
- retries: process.env.CI ? 2 : 0,
56
+ retries: 0,
55
57
  workers: 1,
56
58
  reporter: 'list',
57
59
  timeout: 30_000,
58
60
  use: {
59
61
  baseURL,
60
62
  channel: 'chrome',
61
- trace: 'on-first-retry',
63
+ trace: 'retain-on-failure',
62
64
  screenshot: 'only-on-failure',
63
65
  },
64
66
  webServer: [
65
67
  {
66
68
  name: '{{fullstack.servPackageName}}',
67
- command: 'pnpm --filter {{fullstack.servPackageName}} start',
68
- env: {
69
+ command: 'pnpm --filter {{fullstack.servPackageName}} exec node dist/index.js',
70
+ env: createE2eServerEnv({
71
+ ...createE2eNotificationEnv(),
72
+ HAI_DB: JSON.stringify({ type: 'sqlite', database: `${process.env.HAI_E2E_DATA_ROOT}/fullstack.db` }),
73
+ HAI_CACHE: JSON.stringify({ type: 'memory' }),
74
+ HAI_STORAGE: JSON.stringify({ type: 'local', root: `${process.env.HAI_E2E_DATA_ROOT}/uploads` }),
75
+ HAI_VECDB: JSON.stringify({ type: 'lancedb', path: `${process.env.HAI_E2E_DATA_ROOT}/vecdb` }),
76
+ HAI_CRYPTO_TRANSPORT_SERVERKEYRING: createE2eTransportRing(),
69
77
  HOST: serviceHost,
70
78
  PORT: servicePort,
71
79
  // 仅供脚手架验收:生产启动必须由部署环境提供初始管理员密码。
72
80
  HAI_ADMIN_DEFAULT_PASSWORD: process.env.E2E_ADMIN_PASSWORD || 'Scaffold-test-Admin123!',
73
81
  HAI_CORE_NAME: process.env.HAI_CORE_NAME || 'Configured scaffold',
74
82
  HAI_CORE_VERSION: process.env.HAI_CORE_VERSION || '9.8.7',
75
- HAI_CORE_ENV: process.env.HAI_CORE_ENV || 'test',
83
+ HAI_CORE_ENV: 'test',
76
84
  HAI_CORE_LOGGING_LEVEL: 'warn',
77
- },
85
+ }),
78
86
  url: `${serviceBaseURL}/health`,
79
87
  reuseExistingServer: false,
80
88
  timeout: 120_000,
@@ -84,6 +92,7 @@ export default defineConfig({
84
92
  return {
85
93
  name: frontend.name,
86
94
  command: `pnpm --filter ${frontend.name} exec vite preview --host ${resolveHostFromBaseUrl(url, '127.0.0.1')} --port ${resolvePortFromBaseUrl(url, '3000')} --strictPort`,
95
+ env: createE2eServerEnv({ PUBLIC_API_BASE: serviceBaseURL }),
87
96
  url,
88
97
  reuseExistingServer: false,
89
98
  timeout: 120_000,
@@ -0,0 +1,40 @@
1
+ {
2
+ "app_title": "H5アプリ",
3
+ "app_description": "hai Agent Frameworkで構築したH5モバイルアプリ",
4
+ "nav_home": "ホーム",
5
+ "nav_discover": "見つける",
6
+ "nav_profile": "プロフィール",
7
+ "page_home_title": "ホーム",
8
+ "page_home_desc": "H5アプリへようこそ",
9
+ "page_home_search": "検索...",
10
+ "page_home_feature1": "機能1",
11
+ "page_home_feature2": "機能2",
12
+ "page_home_feature3": "機能3",
13
+ "page_home_feature4": "機能4",
14
+ "page_home_latest": "最新情報",
15
+ "page_home_welcome": "H5アプリへようこそ",
16
+ "page_discover_title": "見つける",
17
+ "page_discover_desc": "さらに多くのコンテンツを探す。",
18
+ "page_profile_title": "プロフィール",
19
+ "page_profile_desc": "個人情報を管理します",
20
+ "page_profile_username": "ユーザー名",
21
+ "page_profile_login_hint": "タップしてログイン",
22
+ "page_profile_info": "個人情報",
23
+ "page_profile_settings": "設定",
24
+ "page_profile_about": "会社概要",
25
+ "auth_login_title": "ログイン",
26
+ "auth_login_submit": "ログイン",
27
+ "auth_login_loading": "ログイン中...",
28
+ "auth_login_failed": "ログインに失敗しました",
29
+ "auth_login_to_register": "アカウントをお持ちでない方は新規登録",
30
+ "auth_register_title": "登録",
31
+ "auth_register_submit": "登録",
32
+ "auth_register_loading": "登録中...",
33
+ "auth_register_failed": "登録に失敗しました",
34
+ "auth_register_to_login": "既にアカウントをお持ちの方はログイン",
35
+ "auth_label_username": "ユーザー名",
36
+ "auth_label_password": "パスワード",
37
+ "auth_label_confirm_password": "パスワードの確認",
38
+ "auth_error_network": "ネットワークエラー",
39
+ "auth_error_password_mismatch": "パスワードが一致しません"
40
+ }
@@ -13,8 +13,8 @@
13
13
  import { page } from '$app/state'
14
14
  {{#if hasI18n}}
15
15
  import * as m from '$lib/paraglide/messages'
16
- import { getLocale } from '$lib/paraglide/runtime.js'
17
- import { AppBar, BottomNav, setGlobalLocale } from '@h-ai/ui'
16
+ import { getLocale, setLocale } from '$lib/paraglide/runtime.js'
17
+ import { AppBar, BottomNav, LanguageSwitch, setGlobalLocale } from '@h-ai/ui'
18
18
  {{else}}
19
19
  import { AppBar, BottomNav } from '@h-ai/ui'
20
20
  {{/if}}
@@ -49,6 +49,11 @@
49
49
  }
50
50
  {{#if hasI18n}}
51
51
 
52
+ function handleLanguageChange(language: string) {
53
+ setGlobalLocale(language)
54
+ setLocale(language as Parameters<typeof setLocale>[0])
55
+ }
56
+
52
57
  if (browser) {
53
58
  const paraglideLocale = getLocale()
54
59
  setGlobalLocale(paraglideLocale)
@@ -62,7 +67,13 @@
62
67
 
63
68
  <div class='hai-mobile-viewport'>
64
69
  <div class='hai-mobile-shell min-w-0'>
65
- <AppBar title={page.data.appConfig.name} safeArea fixed={false} />
70
+ <AppBar title={page.data.appConfig.name} safeArea fixed={false}>
71
+ {{#if hasI18n}}
72
+ {#snippet trailing()}
73
+ <LanguageSwitch currentLanguage={getLocale()} onchange={handleLanguageChange} compact />
74
+ {/snippet}
75
+ {{/if}}
76
+ </AppBar>
66
77
  <main class='hai-mobile-main'>{@render children()}</main>
67
78
  <footer class='border-t border-base-300 bg-base-100 p-2 text-center text-xs text-base-content/60'>
68
79
  {page.data.appConfig.name} · {page.data.appConfig.version} · {page.data.appConfig.env}
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://inlang.com/schema/inlang-message-format",
3
+ "app_title": "モバイルワークスペース",
4
+ "app_description": "hai Agent Framework + Capacitor + Svelte 5で構築したモバイルアプリ",
5
+ "boot_failed_title": "起動に失敗しました",
6
+ "common_retry": "再試行",
7
+ "common_refresh": "更新",
8
+ "common_enabled": "有効",
9
+ "common_disabled": "無効",
10
+ "nav_home": "ワークスペース",
11
+ "nav_tasks": "タスク",
12
+ "nav_profile": "プロフィール",
13
+ "hero_eyebrow": "モバイルワークスペース",
14
+ "hero_title": "モバイルを中心にした実用的な出発点",
15
+ "hero_subtitle": "Capacitorのネイティブ機能、安全なトークン保存、共通APIクライアント、@h-ai/uiのモバイルコンポーネントを組み込んでいます。",
16
+ "feature_native_title": "ネイティブ対応",
17
+ "feature_native_desc": "ステータスバー、セーフエリア、デバイスランタイムを設定済み。プッシュ通知やカメラなどのプラグインを追加できます。",
18
+ "feature_api_title": "共通API",
19
+ "feature_api_desc": "PUBLIC_API_BASEでバックエンドに接続します。ネイティブ端末ではトークンを安全なストレージに保存します。",
20
+ "status_title": "実行状態",
21
+ "status_api_label": "APIベースURL",
22
+ "status_runtime_label": "実行環境",
23
+ "status_runtime_native": "ネイティブアプリ",
24
+ "status_runtime_web": "ブラウザープレビュー",
25
+ "status_token_label": "トークン保存",
26
+ "status_token_native": "安全なストレージ",
27
+ "status_token_memory": "メモリー内保存",
28
+ "tasks_title": "本日のタスク",
29
+ "tasks_sync": "バックエンドAPIを接続",
30
+ "tasks_polish": "モバイルUXを改善",
31
+ "tasks_release": "ネイティブのパッケージ化を準備",
32
+ "profile_title": "アプリ設定",
33
+ "profile_language": "言語はParaglideに従います",
34
+ "profile_theme": "テーマは@h-ai/uiに従います",
35
+ "profile_secure": "機密トークンはlocalStorageに保存しません"
36
+ }
@@ -6,7 +6,7 @@
6
6
  {{#if features.iam}}
7
7
  import { apiClient } from '@h-ai/api-client'
8
8
  {{/if}}
9
- import { applyTheme, getSavedTheme, setGlobalLocale } from '@h-ai/ui'
9
+ import { applyTheme, getSavedTheme, LanguageSwitch, setGlobalLocale } from '@h-ai/ui'
10
10
  {{#if features.iam}}
11
11
  import { AuthPanel } from '@h-ai/ui/components/scenes/iam/index'
12
12
  {{/if}}
@@ -17,7 +17,7 @@
17
17
  import { initCapacitor, isNativeApp } from './lib/capacitor.js'
18
18
  import { HashRouter } from './lib/navigation/hash-router.svelte.js'
19
19
  import * as m from './lib/paraglide/messages.js'
20
- import { getLocale } from './lib/paraglide/runtime.js'
20
+ import { getLocale, setLocale } from './lib/paraglide/runtime.js'
21
21
 
22
22
  {{#if features.iam}}
23
23
  const authActions = apiClient.createAuthActions(apiClient)
@@ -57,6 +57,7 @@
57
57
  document.title = appConfig.name
58
58
  applyTheme(getSavedTheme(), false)
59
59
  setGlobalLocale(getLocale())
60
+ document.documentElement.lang = getLocale()
60
61
  await initCapacitor()
61
62
  await initApi()
62
63
  {{#if features.iam}}
@@ -80,6 +81,11 @@
80
81
  }
81
82
  }
82
83
 
84
+ function handleLanguageChange(language: string): void {
85
+ setGlobalLocale(language)
86
+ setLocale(language as Parameters<typeof setLocale>[0])
87
+ }
88
+
83
89
  /** 切换底部导航;例如 handleTabChange('tasks') 会写入 #/tasks。 */
84
90
  function handleTabChange(id: string): void {
85
91
  router.navigate(`/${id}`)
@@ -101,7 +107,11 @@
101
107
 
102
108
  <div class='hai-mobile-viewport'>
103
109
  <div class='hai-mobile-shell min-w-0'>
104
- <AppBar title={appConfig.name} safeArea fixed={false} class='bg-base-100/95 backdrop-blur border-base-content/8' />
110
+ <AppBar title={appConfig.name} safeArea fixed={false} class='bg-base-100/95 backdrop-blur border-base-content/8'>
111
+ {#snippet trailing()}
112
+ <LanguageSwitch currentLanguage={getLocale()} onchange={handleLanguageChange} compact />
113
+ {/snippet}
114
+ </AppBar>
105
115
  <main class='hai-mobile-main'>
106
116
  {#if booting}
107
117
  <div class='grid min-h-64 place-items-center'><span class='loading loading-spinner loading-lg'></span></div>