@h-ai/ui 0.1.0-alpha.61 → 0.1.0-alpha.62

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.
package/README.en.md CHANGED
@@ -248,7 +248,14 @@ For `BareInput` with `type="file"`, read `event.currentTarget.files` in `onchang
248
248
  | ------------ | ----------- | ----------------------------------------------- |
249
249
  | `PageHeader` | Page header | `title`, `description`; supports `actions` slot |
250
250
 
251
- ### Scenes (34)
251
+ ### Scenes (36)
252
+
253
+ #### Guide (2)
254
+
255
+ | Component | Description | Key props |
256
+ | --- | --- | --- |
257
+ | `GuideHost` | First-use invitations, tours and help dialog host | `guide`, `locale`, `scope`, `autoStart` |
258
+ | `HelpCenter` | Standalone searchable help center | `articles`, `tours`, `locale`, `selectedId`, `onstart` |
252
259
 
253
260
  #### App (7)
254
261
 
@@ -756,6 +763,58 @@ AuthPanel disables native form controls during SSR and enables input after clien
756
763
 
757
764
  [完整 IAM 配置 / IAM configuration](../iam/README.en.md)
758
765
 
766
+ ## Onboarding and help center
767
+
768
+ Import `createGuide`, `GuideHost` and `HelpCenter` from `@h-ai/ui/guide`. No init/close is required. The tour engine loads when a tour starts; controls follow the current theme and include Chinese/English translations. Business text uses application i18n callbacks.
769
+
770
+ The same entry exports `resolveGuideText(text?)` to evaluate strings or i18n callbacks; host components resolve text automatically.
771
+
772
+ Create the controller inside a layout component, register tours with id/title/steps and optional route/version, then mount `<GuideHost {guide} {locale} scope={user.id} autoStart='overview' />`. Add `data-guide` to existing target components. Supply the application's NavAdapter for SPA navigation.
773
+
774
+ ```svelte
775
+ <script lang='ts'>
776
+ import { createGuide, GuideHost } from '@h-ai/ui/guide'
777
+
778
+ const guide = createGuide({
779
+ nav,
780
+ persistKey: 'my-app-guide',
781
+ tours: [{
782
+ id: 'users',
783
+ version: 1,
784
+ route: '/users',
785
+ title: () => m.guide_users_title(),
786
+ description: () => m.guide_users_description(),
787
+ steps: [{
788
+ target: 'users-create',
789
+ title: () => m.guide_create_title(),
790
+ description: () => m.guide_create_description(),
791
+ }],
792
+ }],
793
+ articles: [{
794
+ id: 'users-help',
795
+ title: () => m.guide_users_title(),
796
+ content: () => m.guide_users_markdown(),
797
+ tourId: 'users',
798
+ }],
799
+ })
800
+ </script>
801
+
802
+ <GuideHost {guide} {locale} scope={user.id} autoStart='users' />
803
+ <Button onclick={() => guide.openHelp()}>{m.help()}</Button>
804
+ <!-- 在目标页面为现有按钮添加锚点,无需额外包装。 -->
805
+ <Button data-guide='users-create' onclick={createUser}>{m.create_user()}</Button>
806
+ ```
807
+
808
+ `start(id)` returns Promise<boolean> and always allows replay; failure returns false and sets error. `startIfNeeded(id)` returns boolean and offers a first-use invitation. `openHelp(articleId?)` opens searchable Markdown help; `stop()` cancels without marking completion; `reset(id)` clears the current user's preference.
809
+
810
+ The close button, Escape, and overlay clicks cancel pending highlighting and clear `busy` even before the first positioning frame completes. A pending `start()` resolves to false without recording completion.
811
+
812
+ Persistence supports cookie (default), localStorage and memory, with an in-instance fallback when storage is restricted. Preferences are isolated by user and version. Later delays reminders for one day; Cookie preferences expire after one year. The default full-page navigation uses a short-lived sessionStorage record to start on the destination page.
813
+
814
+ Use `() => m.key()` for localized content and pass the current locale. Steps render as plain text; help uses the existing safe Markdown renderer with raw HTML disabled. Targets wait up to 3000ms by default; before(signal) prepares tabs or drawers, optional skips nonessential missing targets, and missing required targets stop the tour. Unmounting cancels pending work. Never share controllers across SSR users.
815
+
816
+ HelpCenter can also render a standalone page using articles, optional tours, locale, selectedId and onstart. The admin-console example and CLI fullstack Web/App/Desktop include first-use invitations, help and profile tours by default. See the [complete integration guide](../cli/templates/skills/hai-ui/guide.md).
817
+
759
818
  ## Other exports
760
819
 
761
820
  ```ts
package/README.ja.md CHANGED
@@ -248,7 +248,14 @@ components/
248
248
  | -------------- | -------------- | ---------------------------------------------- |
249
249
  | `PageHeader` | ページヘッダー | `title`, `description`、`actions` スロット対応 |
250
250
 
251
- ### Scenes:用途別コンポーネント(34)
251
+ ### Scenes:用途別コンポーネント(36)
252
+
253
+ #### Guide:案内・ヘルプ(2)
254
+
255
+ | コンポーネント | 説明 | 主なプロパティ |
256
+ | --- | --- | --- |
257
+ | `GuideHost` | 初回案内・ガイド・ヘルプダイアログ | `guide`, `locale`, `scope`, `autoStart` |
258
+ | `HelpCenter` | 独立して使える検索付きヘルプ | `articles`, `tours`, `locale`, `selectedId`, `onstart` |
252
259
 
253
260
  #### App:アプリ全体(7)
254
261
 
@@ -756,6 +763,58 @@ AuthPanel は SSR 中のフォーム操作を無効にし、クライアント
756
763
 
757
764
  [完整 IAM 配置 / IAM configuration](../iam/README.ja.md)
758
765
 
766
+ ## 初回ガイドとヘルプセンター
767
+
768
+ `@h-ai/ui/guide` から `createGuide`、`GuideHost`、`HelpCenter` をインポートします。init/close は不要です。ガイド開始時にエンジンを読み込み、既存テーマと中国語・英語の共通文言を使用します。業務内容はアプリの翻訳関数を使います。
769
+
770
+ 同じ入口の `resolveGuideText(text?)` は文字列や i18n コールバックを評価します。ホストコンポーネントは文言を自動で解析します。
771
+
772
+ レイアウト内でコントローラーを作成し、id/title/steps と任意の route/version を登録します。`<GuideHost {guide} {locale} scope={user.id} autoStart='overview' />` を配置し、対象コンポーネントに data-guide を付けます。SPA は既存の NavAdapter を提供してください。
773
+
774
+ ```svelte
775
+ <script lang='ts'>
776
+ import { createGuide, GuideHost } from '@h-ai/ui/guide'
777
+
778
+ const guide = createGuide({
779
+ nav,
780
+ persistKey: 'my-app-guide',
781
+ tours: [{
782
+ id: 'users',
783
+ version: 1,
784
+ route: '/users',
785
+ title: () => m.guide_users_title(),
786
+ description: () => m.guide_users_description(),
787
+ steps: [{
788
+ target: 'users-create',
789
+ title: () => m.guide_create_title(),
790
+ description: () => m.guide_create_description(),
791
+ }],
792
+ }],
793
+ articles: [{
794
+ id: 'users-help',
795
+ title: () => m.guide_users_title(),
796
+ content: () => m.guide_users_markdown(),
797
+ tourId: 'users',
798
+ }],
799
+ })
800
+ </script>
801
+
802
+ <GuideHost {guide} {locale} scope={user.id} autoStart='users' />
803
+ <Button onclick={() => guide.openHelp()}>{m.help()}</Button>
804
+ <!-- 在目标页面为现有按钮添加锚点,无需额外包装。 -->
805
+ <Button data-guide='users-create' onclick={createUser}>{m.create_user()}</Button>
806
+ ```
807
+
808
+ start(id) は Promise<boolean> を返し、手動で再表示できます。失敗時は false と error を設定します。startIfNeeded(id) は初回案内の有無を boolean で返します。openHelp(articleId?) は検索付き Markdown ヘルプを開き、stop() は完了として記録せず終了し、reset(id) は現在のユーザーの状態を削除します。
809
+
810
+ 閉じるボタン、Escape、オーバーレイのクリックは、最初の位置決めフレームが完了する前でも待機と `busy` 状態を解除します。開始中の `start()` は false を返し、完了として記録しません。
811
+
812
+ 保存先は cookie(既定)、localStorage、memory です。保存が制限された環境ではインスタンス内のメモリーに戻ります。ユーザーとバージョンごとに状態を分け、後で表示は一日延期、Cookie は一年保存します。既定のページ遷移は短期 sessionStorage 記録で移動先から開始します。
813
+
814
+ 文言は `() => m.key()` で遅延評価し、現在の locale を渡します。説明はプレーンテキスト、ヘルプは安全な MarkdownRenderer を使用し、生 HTML を無効にします。要素の待機は既定で 3000ms。before(signal) でタブ等を準備し、optional の要素のみ省略できます。必要な要素がなければ終了し、アンマウント時に処理を解除します。SSR ユーザー間でコントローラーを共有しないでください。
815
+
816
+ HelpCenter は articles、任意の tours、locale、selectedId、onstart で独立ページにも配置できます。admin-console と CLI fullstack Web/App/Desktop は初回案内、ヘルプ、プロフィールガイドを標準搭載します。[詳細ガイド](../cli/templates/skills/hai-ui/guide.md)を参照してください。
817
+
759
818
  ## その他のエクスポート
760
819
 
761
820
  ```ts
package/README.md CHANGED
@@ -248,7 +248,14 @@ components/
248
248
  | ------------ | -------- | ------------------------------------------- |
249
249
  | `PageHeader` | 页面头部 | `title`, `description`,支持 `actions` 插槽 |
250
250
 
251
- ### 场景组件 Scenes(34 个)
251
+ ### 场景组件 Scenes(36 个)
252
+
253
+ #### Guide 引导与帮助(2 个)
254
+
255
+ | 组件 | 描述 | 主要属性 |
256
+ | --- | --- | --- |
257
+ | `GuideHost` | 首次邀请、页面导览与帮助弹窗宿主 | `guide`, `locale`, `scope`, `autoStart` |
258
+ | `HelpCenter` | 可独立使用的帮助中心 | `articles`, `tours`, `locale`, `selectedId`, `onstart` |
252
259
 
253
260
  #### App 应用级(7 个)
254
261
 
@@ -773,6 +780,59 @@ import {
773
780
  上例的 subscribe 为应用自身的 $state,应用包装 register 动作校验/提交扩展业务数据;如需单独组合表单,使用 `<RegisterForm> {#snippet extraFields()}...{/snippet} </RegisterForm>`。扩展里的按钮须为 `type="button"`,避免误提交。
774
781
 
775
782
 
783
+ ## 新手引导与帮助中心
784
+
785
+ `@h-ai/ui/guide` 提供 `createGuide`、`GuideHost`、`HelpCenter` 和相关类型。无需 init/close;引擎在开始导览时按需加载,浮层复用当前主题。组件内置中英文按钮、进度和错误提示,业务内容使用应用 i18n。
786
+
787
+ 同一入口的 `resolveGuideText(text?)` 可解析字符串或 i18n 回调;宿主组件已自动解析文案。
788
+
789
+ 下例放在应用布局中;`nav` 是应用现有 NavAdapter,`locale`、`user` 与翻译函数 `m` 来自应用,翻译 key 需在应用消息中定义。目标按钮放在 `/users` 页面。
790
+
791
+ ```svelte
792
+ <script lang='ts'>
793
+ import { createGuide, GuideHost } from '@h-ai/ui/guide'
794
+
795
+ const guide = createGuide({
796
+ nav,
797
+ persistKey: 'my-app-guide',
798
+ tours: [{
799
+ id: 'users',
800
+ version: 1,
801
+ route: '/users',
802
+ title: () => m.guide_users_title(),
803
+ description: () => m.guide_users_description(),
804
+ steps: [{
805
+ target: 'users-create',
806
+ title: () => m.guide_create_title(),
807
+ description: () => m.guide_create_description(),
808
+ }],
809
+ }],
810
+ articles: [{
811
+ id: 'users-help',
812
+ title: () => m.guide_users_title(),
813
+ content: () => m.guide_users_markdown(),
814
+ tourId: 'users',
815
+ }],
816
+ })
817
+ </script>
818
+
819
+ <GuideHost {guide} {locale} scope={user.id} autoStart='users' />
820
+ <Button onclick={() => guide.openHelp()}>{m.help()}</Button>
821
+ <!-- 在目标页面为现有按钮添加锚点,无需额外包装。 -->
822
+ <Button data-guide='users-create' onclick={createUser}>{m.create_user()}</Button>
823
+ ```
824
+
825
+ - `start(id)` 返回 `Promise<boolean>`,允许手动重看;失败返回 false 并设置 `error`。`startIfNeeded(id)` 返回 boolean 并显示首次邀请,通常由 autoStart 处理。
826
+ - `openHelp(articleId?)` 打开帮助,`stop()` 取消导览,`reset(id)` 删除当前用户的偏好。完成、稍后和不再提醒分别记录;稍后暂缓一天,Cookie 默认保存一年。
827
+ - 关闭按钮、Escape 和遮罩点击在首帧定位期间也会取消等待、清除 `busy`;启动中的 `start()` 返回 false,不记录完成。
828
+ - `persistence` 支持 cookie(默认)、localStorage、memory;受限存储回退到实例内存。scope 使用非敏感用户标识,version 变化会重新提示。
829
+ - 通过已有 NavAdapter 接入 SvelteKit、SPA 或原生 WebView;默认完整导航使用 sessionStorage 保存短期启动记录。宿主只在浏览器挂载后读取记录,SSR 实例不得跨用户共享。
830
+ - 目标等待默认 3000ms,可用 waitTimeout 调整。before(signal) 可展开页签/抽屉;必要元素缺失停止,可选步骤使用 optional 跳过。引导不会自动提交业务表单。
831
+ - 业务文案用函数延迟求值,并传入当前 locale。步骤内容按纯文本显示;帮助正文复用安全 MarkdownRenderer,原始 HTML 关闭。
832
+ - HelpCenter 可独立放入帮助页面,props 为 articles、tours(可选)、locale、selectedId 和 onstart;支持类别标签、标题/正文搜索和关联导览。
833
+
834
+ admin-console 示例和 CLI fullstack Web/App/Desktop 默认包含帮助入口、首次邀请及个人资料导览。完整配置和验证场景见 [引导指南](../cli/templates/skills/hai-ui/guide.md)。
835
+
776
836
  ## 其他导出
777
837
 
778
838
  ```ts
package/auto-import.d.ts CHANGED
@@ -16,6 +16,8 @@ type AutoImportHaiUiPreprocessor = {
16
16
  export function autoImportHaiUi(): AutoImportHaiUiPreprocessor
17
17
 
18
18
  declare global {
19
+ const GuideHost: typeof import('@h-ai/ui')['GuideHost']
20
+ const HelpCenter: typeof import('@h-ai/ui')['HelpCenter']
19
21
  // primitives
20
22
  const Avatar: typeof import('@h-ai/ui')['Avatar']
21
23
  const Badge: typeof import('@h-ai/ui')['Badge']
package/auto-import.js CHANGED
@@ -77,6 +77,8 @@ const UI_COMPONENTS = new Set([
77
77
  'ThemeColorPicker',
78
78
  'ThemeSelector',
79
79
  'ThemeToggle',
80
+ 'GuideHost',
81
+ 'HelpCenter',
80
82
  // scenes - iam
81
83
  'AuthShell',
82
84
  'AuthPanel',
@@ -0,0 +1,111 @@
1
+ <script lang='ts'>
2
+ import type { GuideController } from '../../../guide-types.js'
3
+ import type { DataAttributes } from '../../../types.js'
4
+ import { onMount, untrack } from 'svelte'
5
+ import { resolveGuideText } from '../../../guide.svelte.js'
6
+ import { getUILocale, uiM } from '../../../messages.js'
7
+ import { getDataAttributes } from '../../../utils.js'
8
+ import Alert from '../../compounds/Alert.svelte'
9
+ import Modal from '../../compounds/Modal.svelte'
10
+ import Button from '../../primitives/Button.svelte'
11
+ import HelpCenter from './HelpCenter.svelte'
12
+ import 'driver.js/dist/driver.css'
13
+ import '../../../styles/guide.css'
14
+
15
+ interface Props {
16
+ guide: GuideController
17
+ locale?: string
18
+ /** 非敏感的用户标识,账号切换后独立记录引导偏好。 */
19
+ scope?: string
20
+ /** 挂载后提示首次使用者,不会直接打断操作或自动跳转。 */
21
+ autoStart?: string
22
+ }
23
+
24
+ const { guide, locale = getUILocale(), scope = 'anonymous', autoStart, ...restProps }: Props & DataAttributes = $props()
25
+ const dataAttributes = $derived(getDataAttributes(restProps))
26
+ let ready = $state(false)
27
+ const offered = $derived.by(() => {
28
+ void locale
29
+ const tour = guide.offeredTour
30
+ return tour ? { id: tour.id, title: resolveGuideText(tour.title), description: resolveGuideText(tour.description) } : null
31
+ })
32
+ const text = $derived.by(() => ({
33
+ welcome: uiM('guide_welcome', undefined, locale),
34
+ start: uiM('guide_start', undefined, locale),
35
+ later: uiM('guide_later', undefined, locale),
36
+ dismiss: uiM('guide_dismiss', undefined, locale),
37
+ help: uiM('guide_help', undefined, locale),
38
+ close: uiM('guide_close', undefined, locale),
39
+ loading: uiM('guide_loading', undefined, locale),
40
+ }))
41
+
42
+ $effect(() => {
43
+ const currentLocale = locale
44
+ untrack(() => guide.setLocale(currentLocale))
45
+ })
46
+ $effect(() => {
47
+ const currentScope = scope
48
+ const tourId = autoStart
49
+ if (ready) {
50
+ untrack(() => {
51
+ guide.setScope(currentScope)
52
+ if (tourId)
53
+ guide.startIfNeeded(tourId)
54
+ })
55
+ }
56
+ })
57
+ $effect(() => {
58
+ const pathname = guide.pathname
59
+ untrack(() => guide.handleNavigation(pathname))
60
+ })
61
+ onMount(() => {
62
+ let alive = true
63
+ guide.setScope(scope)
64
+ guide.setLocale(locale)
65
+ void guide.resume().then(() => {
66
+ if (alive)
67
+ ready = true
68
+ })
69
+ return () => {
70
+ alive = false
71
+ guide.stop()
72
+ }
73
+ })
74
+ </script>
75
+
76
+ <div {...dataAttributes} class='contents'>
77
+
78
+ {#if offered}
79
+ <aside class='hai-guide-offer fixed bottom-4 right-4 z-[1150] w-96 max-w-[calc(100vw-2rem)] rounded-2xl border border-base-content/10 bg-base-100 p-5 text-base-content shadow-(--shadow-overlay)' aria-label={text.welcome} data-testid='guide-offer'>
80
+ <p class='text-xs font-medium text-primary'>{text.welcome}</p>
81
+ <h2 class='mt-2 text-base font-semibold'>{offered.title}</h2>
82
+ <p class='mt-2 text-sm leading-6 text-base-content/65'>{offered.description}</p>
83
+ <div class='mt-4 flex flex-wrap gap-2'>
84
+ <Button variant='primary' size='sm' onclick={() => offered && void guide.start(offered.id)} data-testid='guide-start'>{text.start}</Button>
85
+ <Button variant='ghost' size='sm' onclick={() => guide.defer()} data-testid='guide-later'>{text.later}</Button>
86
+ <Button variant='ghost' size='sm' onclick={() => guide.dismiss()} data-testid='guide-dismiss'>{text.dismiss}</Button>
87
+ </div>
88
+ </aside>
89
+ {/if}
90
+
91
+ {#if guide.busy}
92
+ <div class='hai-guide-loading fixed bottom-4 left-1/2 z-[1000000001] flex max-w-[calc(100vw-2rem)] -translate-x-1/2 items-center gap-3 rounded-xl border border-base-content/10 bg-base-100 px-4 py-2 text-sm text-base-content shadow-lg' role='status' data-testid='guide-loading'>
93
+ <span class='loading loading-spinner loading-xs'></span>{text.loading}
94
+ <Button size='sm' variant='ghost' onclick={() => guide.stop()} data-testid='guide-cancel'>{text.close}</Button>
95
+ </div>
96
+ {/if}
97
+
98
+ {#if guide.error}
99
+ <div class='fixed bottom-4 right-4 z-[1500] max-w-[calc(100vw-2rem)]' data-testid='guide-error'>
100
+ <Alert variant='warning'>
101
+ <div class='flex items-center gap-3'><span>{guide.error}</span><Button size='sm' variant='ghost' onclick={() => guide.clearError()}>{text.close}</Button></div>
102
+ </Alert>
103
+ </div>
104
+ {/if}
105
+
106
+ {#if guide.helpOpen}
107
+ <Modal open title={text.help} size='4xl' height='min(80dvh, 48rem)' bodyClass='p-0' onclose={() => guide.closeHelp()} data-testid='guide-help'>
108
+ <HelpCenter articles={guide.articles} tours={guide.tours} selectedId={guide.articleId} {locale} onstart={id => void guide.start(id)} />
109
+ </Modal>
110
+ {/if}
111
+ </div>
@@ -0,0 +1,16 @@
1
+ import type { GuideController } from '../../../guide-types.js';
2
+ import type { DataAttributes } from '../../../types.js';
3
+ import 'driver.js/dist/driver.css';
4
+ import '../../../styles/guide.css';
5
+ interface Props {
6
+ guide: GuideController;
7
+ locale?: string;
8
+ /** 非敏感的用户标识,账号切换后独立记录引导偏好。 */
9
+ scope?: string;
10
+ /** 挂载后提示首次使用者,不会直接打断操作或自动跳转。 */
11
+ autoStart?: string;
12
+ }
13
+ type $$ComponentProps = Props & DataAttributes;
14
+ declare const GuideHost: import("svelte").Component<$$ComponentProps, {}, "">;
15
+ type GuideHost = ReturnType<typeof GuideHost>;
16
+ export default GuideHost;
@@ -0,0 +1,94 @@
1
+ <script lang='ts'>
2
+ import type { GuideArticle, GuideTour } from '../../../guide-types.js'
3
+ import type { DataAttributes } from '../../../types.js'
4
+ import { resolveGuideText } from '../../../guide.svelte.js'
5
+ import { getUILocale, uiM } from '../../../messages.js'
6
+ import { getDataAttributes } from '../../../utils.js'
7
+ import BareButton from '../../primitives/BareButton.svelte'
8
+ import Button from '../../primitives/Button.svelte'
9
+ import Input from '../../primitives/Input.svelte'
10
+
11
+ interface Props {
12
+ articles: readonly GuideArticle[]
13
+ tours?: readonly GuideTour[]
14
+ locale?: string
15
+ selectedId?: string | null
16
+ onstart?: (tourId: string) => void
17
+ }
18
+ const { articles, tours = [], locale = getUILocale(), selectedId = null, onstart, ...restProps }: Props & DataAttributes = $props()
19
+ const dataAttributes = $derived(getDataAttributes(restProps))
20
+ let search = $state('')
21
+ const searchId = $props.id()
22
+ let currentId = $derived(selectedId)
23
+ // 显式依赖宿主语言,确保回调返回的业务翻译也重新求值。
24
+ const localized = $derived.by(() => {
25
+ void locale
26
+ return articles.filter(article => !article.enabled || article.enabled()).map(article => ({
27
+ ...article,
28
+ title: resolveGuideText(article.title),
29
+ description: resolveGuideText(article.description),
30
+ category: resolveGuideText(article.category),
31
+ content: resolveGuideText(article.content),
32
+ }))
33
+ })
34
+ const availableTours = $derived.by(() => {
35
+ void locale
36
+ return tours.filter(tour => !tour.enabled || tour.enabled()).map(tour => ({ id: tour.id, title: resolveGuideText(tour.title) }))
37
+ })
38
+ const visible = $derived(localized.filter(article => (`${article.title} ${article.description} ${article.category} ${article.content}`).toLowerCase().includes(search.trim().toLowerCase())))
39
+ const selected = $derived(visible.find(article => article.id === currentId) ?? visible[0])
40
+ const markdown = $derived(selected ? import('../ai/MarkdownRenderer.svelte') : null)
41
+ const text = $derived.by(() => ({
42
+ search: uiM('guide_search', undefined, locale),
43
+ empty: uiM('guide_empty', undefined, locale),
44
+ tours: uiM('guide_tours', undefined, locale),
45
+ start: uiM('guide_start', undefined, locale),
46
+ loading: uiM('guide_loading', undefined, locale),
47
+ failed: uiM('guide_startFailed', undefined, locale),
48
+ }))
49
+ </script>
50
+
51
+ <div data-testid='help-center' {...dataAttributes} class='flex min-h-0 flex-col text-base-content md:flex-row'>
52
+ <nav class='shrink-0 border-b border-base-content/8 p-4 md:w-64 md:border-b-0 md:border-r' aria-label={text.search}>
53
+ <label class='sr-only' for={searchId}>{text.search}</label>
54
+ <Input id={searchId} bind:value={search} placeholder={text.search} data-testid='guide-search' />
55
+ <div class='mt-3 max-h-40 space-y-1 overflow-y-auto md:max-h-[50dvh]'>
56
+ {#each visible as article (article.id)}
57
+ <BareButton class={`w-full rounded-lg px-3 py-2 text-left text-sm hover:bg-base-content/5 ${selected?.id === article.id ? 'bg-primary/10 font-semibold text-primary' : ''}`} onclick={() => currentId = article.id} data-testid={`guide-article-${article.id}`}>
58
+ {#if article.category}<span class='mb-1 block text-xs text-base-content/50'>{article.category}</span>{/if}
59
+ <span class='block'>{article.title}</span>
60
+ </BareButton>
61
+ {/each}
62
+ </div>
63
+ {#if onstart && availableTours.length}
64
+ <p class='mt-5 text-xs font-medium text-base-content/50'>{text.tours}</p>
65
+ <div class='mt-2 flex flex-wrap gap-2 md:flex-col'>
66
+ {#each availableTours as tour (tour.id)}
67
+ <Button variant='ghost' size='sm' onclick={() => onstart?.(tour.id)} data-testid={`guide-tour-${tour.id}`}>{tour.title}</Button>
68
+ {/each}
69
+ </div>
70
+ {/if}
71
+ </nav>
72
+ <article class='min-w-0 flex-1 p-5 md:p-7' data-testid='guide-article-content'>
73
+ {#if selected}
74
+ <h2 class='text-xl font-semibold'>{selected.title}</h2>
75
+ {#if selected.description}<p class='mt-2 text-sm text-base-content/60'>{selected.description}</p>{/if}
76
+ {#if selected.tourId && availableTours.some(tour => tour.id === selected.tourId) && onstart}
77
+ <Button variant='primary' size='sm' class='mt-4' onclick={() => onstart?.(selected!.tourId!)} data-testid='guide-article-start'>{text.start}</Button>
78
+ {/if}
79
+ <div class='mt-5'>
80
+ {#await markdown}
81
+ <p class='text-sm text-base-content/50' role='status'>{text.loading}</p>
82
+ {:then component}
83
+ {#if component}
84
+ <component.default content={selected.content} allowHtmlTags={false} showCopyButton={false} />
85
+ {/if}
86
+ {:catch}
87
+ <p class='text-sm text-error' role='alert'>{text.failed}</p>
88
+ {/await}
89
+ </div>
90
+ {:else}
91
+ <p class='py-8 text-center text-sm text-base-content/50'>{text.empty}</p>
92
+ {/if}
93
+ </article>
94
+ </div>
@@ -0,0 +1,13 @@
1
+ import type { GuideArticle, GuideTour } from '../../../guide-types.js';
2
+ import type { DataAttributes } from '../../../types.js';
3
+ interface Props {
4
+ articles: readonly GuideArticle[];
5
+ tours?: readonly GuideTour[];
6
+ locale?: string;
7
+ selectedId?: string | null;
8
+ onstart?: (tourId: string) => void;
9
+ }
10
+ type $$ComponentProps = Props & DataAttributes;
11
+ declare const HelpCenter: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type HelpCenter = ReturnType<typeof HelpCenter>;
13
+ export default HelpCenter;
@@ -0,0 +1,2 @@
1
+ export { default as GuideHost } from './GuideHost.svelte';
2
+ export { default as HelpCenter } from './HelpCenter.svelte';
@@ -0,0 +1,2 @@
1
+ export { default as GuideHost } from './GuideHost.svelte';
2
+ export { default as HelpCenter } from './HelpCenter.svelte';
@@ -1,4 +1,2 @@
1
- export declare function authErrorMessage(error: {
2
- code: string;
3
- message: string;
4
- }): string;
1
+ import type { HaiError } from '@h-ai/core';
2
+ export declare function authErrorMessage(error: Pick<HaiError, 'code' | 'message'>): string;
@@ -10,6 +10,7 @@ export * from './charts/index.js';
10
10
  export * from './crud/index.js';
11
11
  export * from './crypto/index.js';
12
12
  export * from './error/index.js';
13
+ export * from './guide/index.js';
13
14
  export * from './iam/index.js';
14
15
  export * from './storage/index.js';
15
16
  export * from './types.js';
@@ -16,6 +16,7 @@ export * from './crud/index.js';
16
16
  export * from './crypto/index.js';
17
17
  // 错误页场景组件
18
18
  export * from './error/index.js';
19
+ export * from './guide/index.js';
19
20
  // IAM 场景组件
20
21
  export * from './iam/index.js';
21
22
  // Storage 场景组件
@@ -0,0 +1,6 @@
1
+ import type { GuideOptions, GuideProgress } from './guide-types.js';
2
+ /** 存储受限时使用会话内存;只保存非敏感的引导偏好。 */
3
+ export declare function createGuideStorage(options: GuideOptions): {
4
+ get(key: string): GuideProgress | null;
5
+ set(key: string, progress: GuideProgress | null): void;
6
+ };
@@ -0,0 +1,67 @@
1
+ import { readStoredValue, writeStoredValue } from './internal/browser-safety.js';
2
+ /** 存储受限时使用会话内存;只保存非敏感的引导偏好。 */
3
+ export function createGuideStorage(options) {
4
+ const memory = new Map();
5
+ const persistence = options.persistence ?? 'cookie';
6
+ function read(key) {
7
+ if (memory.has(key))
8
+ return memory.get(key) ?? null;
9
+ try {
10
+ if (persistence === 'localStorage')
11
+ return readStoredValue(key) ?? memory.get(key) ?? null;
12
+ if (persistence === 'cookie' && typeof document !== 'undefined') {
13
+ const prefix = `${encodeURIComponent(key)}=`;
14
+ const value = document.cookie.split('; ').find(item => item.startsWith(prefix));
15
+ return value ? decodeURIComponent(value.slice(prefix.length)) : memory.get(key) ?? null;
16
+ }
17
+ }
18
+ catch {
19
+ // Cookie 禁用或内容损坏时不影响正常使用。
20
+ }
21
+ return memory.get(key) ?? null;
22
+ }
23
+ function write(key, value) {
24
+ memory.set(key, value);
25
+ try {
26
+ if (persistence === 'localStorage') {
27
+ if (value === null) {
28
+ if (typeof localStorage !== 'undefined')
29
+ localStorage.removeItem(key);
30
+ }
31
+ else {
32
+ writeStoredValue(key, value);
33
+ }
34
+ }
35
+ else if (persistence === 'cookie' && typeof document !== 'undefined') {
36
+ const secure = globalThis.location?.protocol === 'https:' ? '; Secure' : '';
37
+ document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(value ?? '')}; Path=/; SameSite=Lax; Max-Age=${value === null ? '0' : '31536000'}${secure}`;
38
+ }
39
+ }
40
+ catch {
41
+ // WebView 或隐私模式下保留内存态。
42
+ }
43
+ }
44
+ return {
45
+ get(key) {
46
+ try {
47
+ const raw = read(key);
48
+ if (!raw)
49
+ return null;
50
+ const value = JSON.parse(raw);
51
+ if (value && typeof value === 'object'
52
+ && 'version' in value && typeof value.version === 'number' && Number.isFinite(value.version)
53
+ && 'updatedAt' in value && typeof value.updatedAt === 'number' && Number.isFinite(value.updatedAt)
54
+ && 'status' in value && (value.status === 'completed' || value.status === 'dismissed' || value.status === 'deferred')) {
55
+ return { version: value.version, updatedAt: value.updatedAt, status: value.status };
56
+ }
57
+ }
58
+ catch {
59
+ // 损坏的偏好作为未看过处理。
60
+ }
61
+ return null;
62
+ },
63
+ set(key, progress) {
64
+ write(key, progress ? JSON.stringify(progress) : null);
65
+ },
66
+ };
67
+ }