@h-ai/ui 0.1.0-alpha.57 → 0.1.0-alpha.58

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
@@ -98,6 +98,16 @@ With auto-import enabled, use components directly in templates without individua
98
98
 
99
99
  > `@h-ai/ui/auto-import` injects all public Svelte components except `toast`, type imports, and `Range` (which conflicts with the DOM `Range` constructor).
100
100
 
101
+ Pages using a subset of components, such as administration screens, can import public component-directory entry points directly. Import theme utilities from the lightweight `@h-ai/ui/theme` entry so theme consumers do not load the root entry's AI and chart components. Check the installed package's exports; do not import its internal `src/` or `dist/` paths.
102
+
103
+ The preprocessor recognizes semicolon-free, multiline and aliased imports without injecting components already bound locally. Type-only imports do not replace runtime components.
104
+
105
+ ```ts
106
+ import { Card } from '@h-ai/ui/components/compounds/index'
107
+ import { Button } from '@h-ai/ui/components/primitives/index'
108
+ import { getThemeInfo, THEMES } from '@h-ai/ui/theme'
109
+ ```
110
+
101
111
  ### data-\* attribute forwarding
102
112
 
103
113
  Public Svelte components forward caller-supplied `data-*` attributes to their root or primary interactive element for tests, analytics, and automation. Other unknown attributes are not forwarded.
@@ -154,6 +164,8 @@ components/
154
164
  | `Spinner` | Loading animation | `size`, `variant` |
155
165
  | `Progress` | Progress bar | `value`, `max`, `variant`, `striped`, `animated` |
156
166
 
167
+ For `BareInput` with `type="file"`, read `event.currentTarget.files` in `onchange` and leave the file path `value` unbound. Other input types support `bind:value`.
168
+
157
169
  ### Compounds (36)
158
170
 
159
171
  #### Forms
@@ -175,7 +187,7 @@ components/
175
187
 
176
188
  | Component | Description | Key props |
177
189
  | --------- | ------------------- | ---------------------------------------------------------------------------- |
178
- | `Modal` | Modal | `open`, `title`, `size`, `radius`, `bodyOverflow`, `showClose` |
190
+ | `Modal` | Modal with dialog semantics, focus trap and restoration | `open`, `title`, `size`, `radius`, `bodyOverflow`, `showClose` |
179
191
  | `Drawer` | Drawer | `open`, `position`, `title`, `size`, `width`, `resizable`, `widthStorageKey` |
180
192
  | `Confirm` | Confirmation dialog | `open`, `title`, `message`, `variant`, `onconfirm` |
181
193
  | `Popover` | Popover | `open`, `position`, `trigger`, `offset`, `portal` |
@@ -262,7 +274,7 @@ components/
262
274
  | `ChangePasswordForm` | Change password | `loading`, `errors`, `requireOldPassword`, `onsubmit` |
263
275
  | `PasswordInput` | Password input | `value`, `showToggle`, `showStrength`, `minLength` |
264
276
  | `PermGuard` | Permission guard | `permissions`, `mode`, `fallback` |
265
- | `UserProfile` | User profile | `user`, `editable`, `fields`, `onsubmit` |
277
+ | `UserProfile` | User profile | `user`, `editable`, `fields`, `avatarAccept`, `avatarHint`, `onsubmit` |
266
278
 
267
279
  #### Storage (4)
268
280
 
@@ -628,13 +640,14 @@ import {
628
640
  applyTheme, // 应用主题(自动持久化到 localStorage)
629
641
  createThemeBootstrapScript, // 生成可注入 app.html 的首屏主题恢复脚本
630
642
  getCurrentTheme, // 获取当前主题
643
+ getSavedTheme, // Read saved appearance, with an optional application default
631
644
  getThemeInitScript, // 返回可注入到 HTML shell 的防闪烁脚本文本
632
645
  isDarkTheme, // 检查是否暗色主题
633
646
  normalizeHexColor, // 归一化 #RGB / #RRGGBB 主题色
634
647
  normalizeThemeId, // 校验主题 ID 并自动回退
635
648
  THEME_GROUPS, // 按亮色/暗色分组
636
649
  THEMES, // ThemeInfo[] — 15 个精选主题元数据
637
- } from '@h-ai/ui'
650
+ } from '@h-ai/ui/theme'
638
651
  ```
639
652
 
640
653
  Use `getThemeInitScript()` for simple cases. For custom storage keys, theme color CSS variables, or language preferences, generate `createThemeBootstrapScript()` on the server and inject it into `app.html`.
@@ -650,7 +663,7 @@ Place a placeholder in SvelteKit `app.html`:
650
663
  Then inject it from `hooks.server.ts`:
651
664
 
652
665
  ```ts
653
- import { createThemeBootstrapScript, DEFAULT_THEME_COLOR_CSS_VAR } from '@h-ai/ui'
666
+ import { createThemeBootstrapScript, DEFAULT_THEME_COLOR_CSS_VAR } from '@h-ai/ui/theme'
654
667
 
655
668
  const themeBootstrapScript = createThemeBootstrapScript({
656
669
  storageKey: 'hai-demo-preferences',
package/README.ja.md CHANGED
@@ -98,6 +98,16 @@ export default defineConfig({
98
98
 
99
99
  > `toast`・型 import・DOM の `Range` と衝突する `Range` を除き、公開 Svelte コンポーネントは `@h-ai/ui/auto-import` で自動注入できます。
100
100
 
101
+ 管理画面など一部のコンポーネントを使うページは、公開コンポーネントディレクトリーの入口から直接 import できます。テーマツールは軽量な `@h-ai/ui/theme` から読み込み、テーマの利用だけでルート入口の AI・グラフコンポーネントを読み込まないようにします。インストール済みパッケージの exports を確認し、内部 `src/`・`dist/` は import しないでください。
102
+
103
+ 自動 import はセミコロンなし、複数行、別名の import を認識し、既存のローカル名を重複注入しません。型のみの import は実行時コンポーネントの代わりにはなりません。
104
+
105
+ ```ts
106
+ import { Card } from '@h-ai/ui/components/compounds/index'
107
+ import { Button } from '@h-ai/ui/components/primitives/index'
108
+ import { getThemeInfo, THEMES } from '@h-ai/ui/theme'
109
+ ```
110
+
101
111
  ### data-\* 属性の転送
102
112
 
103
113
  公開 Svelte コンポーネントは `data-*` をルート・主操作要素へ転送し、テスト・計測・自動化に使えます。通常の未知属性は転送しません。
@@ -154,6 +164,8 @@ components/
154
164
  | `Spinner` | 読み込み表示 | `size`, `variant` |
155
165
  | `Progress` | プログレスバー | `value`, `max`, `variant`, `striped`, `animated` |
156
166
 
167
+ `BareInput` の `type="file"` では `onchange` の `event.currentTarget.files` を読み取り、ファイルパスの `value` はバインドしません。他の入力型では `bind:value` を使用できます。
168
+
157
169
  ### Compounds:複合コンポーネント(36)
158
170
 
159
171
  #### フォーム
@@ -175,7 +187,7 @@ components/
175
187
 
176
188
  | コンポーネント | 説明 | 主なプロパティ |
177
189
  | -------------- | -------------- | ---------------------------------------------------------------------------- |
178
- | `Modal` | モーダル | `open`, `title`, `size`, `radius`, `bodyOverflow`, `showClose` |
190
+ | `Modal` | モーダル(ダイアログ属性・フォーカス制御と復元) | `open`, `title`, `size`, `radius`, `bodyOverflow`, `showClose` |
179
191
  | `Drawer` | ドロワー | `open`, `position`, `title`, `size`, `width`, `resizable`, `widthStorageKey` |
180
192
  | `Confirm` | 確認ダイアログ | `open`, `title`, `message`, `variant`, `onconfirm` |
181
193
  | `Popover` | ポップオーバー | `open`, `position`, `trigger`, `offset`, `portal` |
@@ -262,7 +274,7 @@ components/
262
274
  | `ChangePasswordForm` | パスワード変更 | `loading`, `errors`, `requireOldPassword`, `onsubmit` |
263
275
  | `PasswordInput` | パスワード入力 | `value`, `showToggle`, `showStrength`, `minLength` |
264
276
  | `PermGuard` | 権限ガード | `permissions`, `mode`, `fallback` |
265
- | `UserProfile` | ユーザープロフィール | `user`, `editable`, `fields`, `onsubmit` |
277
+ | `UserProfile` | ユーザープロフィール | `user`, `editable`, `fields`, `avatarAccept`, `avatarHint`, `onsubmit` |
266
278
 
267
279
  #### Storage:ストレージ(4)
268
280
 
@@ -628,13 +640,14 @@ import {
628
640
  applyTheme, // 应用主题(自动持久化到 localStorage)
629
641
  createThemeBootstrapScript, // 生成可注入 app.html 的首屏主题恢复脚本
630
642
  getCurrentTheme, // 获取当前主题
643
+ getSavedTheme, // 保存済みテーマ。アプリ既定値を指定可能
631
644
  getThemeInitScript, // 返回可注入到 HTML shell 的防闪烁脚本文本
632
645
  isDarkTheme, // 检查是否暗色主题
633
646
  normalizeHexColor, // 归一化 #RGB / #RRGGBB 主题色
634
647
  normalizeThemeId, // 校验主题 ID 并自动回退
635
648
  THEME_GROUPS, // 按亮色/暗色分组
636
649
  THEMES, // ThemeInfo[] — 15 个精选主题元数据
637
- } from '@h-ai/ui'
650
+ } from '@h-ai/ui/theme'
638
651
  ```
639
652
 
640
653
  単純な場合は `getThemeInitScript()` を使えます。独自ストレージ key・テーマカラーの CSS 変数・言語設定がある場合、サーバーの `createThemeBootstrapScript()` で生成して `app.html` に注入することを推奨します。
@@ -650,7 +663,7 @@ SvelteKit の `app.html` にプレースホルダーを置きます:
650
663
  次に `hooks.server.ts` で注入します:
651
664
 
652
665
  ```ts
653
- import { createThemeBootstrapScript, DEFAULT_THEME_COLOR_CSS_VAR } from '@h-ai/ui'
666
+ import { createThemeBootstrapScript, DEFAULT_THEME_COLOR_CSS_VAR } from '@h-ai/ui/theme'
654
667
 
655
668
  const themeBootstrapScript = createThemeBootstrapScript({
656
669
  storageKey: 'hai-demo-preferences',
package/README.md CHANGED
@@ -98,6 +98,16 @@ export default defineConfig({
98
98
 
99
99
  > 除 `toast`、类型导入与 `Range`(会和 DOM `Range` 构造器冲突)外,其余公开 Svelte 组件都可通过 `@h-ai/ui/auto-import` 自动注入。
100
100
 
101
+ 管理后台等只使用部分组件的页面,可直接使用公开组件目录入口;主题工具通过轻量 `@h-ai/ui/theme` 导入,避免主题消费加载根入口中的 AI、图表等组件。组件路径以已安装版本的 exports 为准,不导入包内 `src/` 或 `dist/`。
102
+
103
+ 自动导入器识别无分号、跨行及带别名的显式导入,已绑定的组件名称不会重复注入;仅类型导入不会替代运行时组件。
104
+
105
+ ```ts
106
+ import { Card } from '@h-ai/ui/components/compounds/index'
107
+ import { Button } from '@h-ai/ui/components/primitives/index'
108
+ import { getThemeInfo, THEMES } from '@h-ai/ui/theme'
109
+ ```
110
+
101
111
  ### data-* 属性透传
102
112
 
103
113
  公开 Svelte 组件支持把调用方传入的 `data-*` 属性透传到组件根节点或主交互节点,便于测试选择器、埋点和自动化标记。普通未知属性不会被透传。
@@ -154,6 +164,8 @@ components/
154
164
  | `Spinner` | 加载动画 | `size`, `variant` |
155
165
  | `Progress` | 进度条 | `value`, `max`, `variant`, `striped`, `animated` |
156
166
 
167
+ `BareInput` 的 `file` 类型通过 `onchange` 读取 `event.currentTarget.files`,不绑定文件路径 `value`;其他输入类型可使用 `bind:value`。
168
+
157
169
  ### 组合组件 Compounds(36 个)
158
170
 
159
171
  #### 表单
@@ -175,7 +187,7 @@ components/
175
187
 
176
188
  | 组件 | 描述 | 主要属性 |
177
189
  | --------- | ------ | ------------------------------------------------------- |
178
- | `Modal` | 模态框 | `open`, `title`, `size`, `radius`, `bodyOverflow`, `showClose` |
190
+ | `Modal` | 模态框(语义标签、焦点约束与恢复) | `open`, `title`, `size`, `radius`, `bodyOverflow`, `showClose` |
179
191
  | `Drawer` | 抽屉 | `open`, `position`, `title`, `size`, `width`, `resizable`, `widthStorageKey` |
180
192
  | `Confirm` | 确认框 | `open`, `title`, `message`, `variant`, `onconfirm` |
181
193
  | `Popover` | 弹出层 | `open`, `position`, `trigger`, `offset`, `portal` |
@@ -262,7 +274,7 @@ components/
262
274
  | `ChangePasswordForm` | 修改密码 | `loading`, `errors`, `requireOldPassword`, `onsubmit` |
263
275
  | `PasswordInput` | 密码输入框 | `value`, `showToggle`, `showStrength`, `minLength` |
264
276
  | `PermGuard` | 权限守卫 | `permissions`, `mode`, `fallback` |
265
- | `UserProfile` | 用户资料 | `user`, `editable`, `fields`, `onsubmit` |
277
+ | `UserProfile` | 用户资料 | `user`, `editable`, `fields`, `avatarAccept`, `avatarHint`, `onsubmit` |
266
278
 
267
279
  #### Storage 存储(4 个)
268
280
 
@@ -627,13 +639,14 @@ import {
627
639
  applyTheme, // 应用主题(自动持久化到 localStorage)
628
640
  createThemeBootstrapScript, // 生成可注入 app.html 的首屏主题恢复脚本
629
641
  getCurrentTheme, // 获取当前主题
642
+ getSavedTheme, // 读取安全缓存,可传入应用配置的默认主题
630
643
  getThemeInitScript, // 返回可注入到 HTML shell 的防闪烁脚本文本
631
644
  isDarkTheme, // 检查是否暗色主题
632
645
  normalizeHexColor, // 归一化 #RGB / #RRGGBB 主题色
633
646
  normalizeThemeId, // 校验主题 ID 并自动回退
634
647
  THEME_GROUPS, // 按亮色/暗色分组
635
648
  THEMES, // ThemeInfo[] — 15 个精选主题元数据
636
- } from '@h-ai/ui'
649
+ } from '@h-ai/ui/theme'
637
650
  ```
638
651
 
639
652
  简单场景可以直接使用 `getThemeInitScript()`;如果应用有自定义存储 key、主题色 CSS 变量或语言偏好,推荐在服务端通过 `createThemeBootstrapScript()` 生成脚本后再注入 `app.html`。
@@ -649,7 +662,7 @@ import {
649
662
  然后在 `hooks.server.ts` 里注入:
650
663
 
651
664
  ```ts
652
- import { createThemeBootstrapScript, DEFAULT_THEME_COLOR_CSS_VAR } from '@h-ai/ui'
665
+ import { createThemeBootstrapScript, DEFAULT_THEME_COLOR_CSS_VAR } from '@h-ai/ui/theme'
653
666
 
654
667
  const themeBootstrapScript = createThemeBootstrapScript({
655
668
  storageKey: 'hai-demo-preferences',
package/auto-import.js CHANGED
@@ -7,6 +7,8 @@
7
7
  * =============================================================================
8
8
  */
9
9
 
10
+ import { parse } from 'svelte/compiler'
11
+
10
12
  const UI_COMPONENTS = new Set([
11
13
  // primitives
12
14
  'Avatar',
@@ -112,10 +114,8 @@ const UI_COMPONENTS = new Set([
112
114
  const TAG_REGEX = /<([A-Z][A-Za-z0-9]*)\b/g
113
115
  const SCRIPT_REGEX = /<script\b([^>]*)>([\s\S]*?)<\/script>/g
114
116
  const UI_IMPORT_REGEX = /import\s*\{([^}]+)\}\s*from\s*['"]@h-ai\/ui['"];?/
115
- const IMPORT_ALIAS_SPLIT_REGEX = /\s+as\s+/i
116
117
  const WINDOWS_PATH_SEPARATOR_REGEX = /\\/g
117
118
  const MODULE_CONTEXT_ATTR_REGEX = /context\s*=\s*['"]module['"]/i
118
- const TYPE_IMPORT_PREFIX_REGEX = /^type\s+/
119
119
 
120
120
  function collectUsedComponents(content) {
121
121
  const used = new Set()
@@ -130,73 +130,16 @@ function collectUsedComponents(content) {
130
130
 
131
131
  function collectImportedNames(scriptContent) {
132
132
  const imported = new Set()
133
- const lines = scriptContent.split('\n')
134
- let buffer = ''
135
-
136
- const flushImport = (statement) => {
137
- const fromIndex = statement.lastIndexOf(' from ')
138
- if (fromIndex === -1)
139
- return
140
-
141
- let clause = statement.slice('import '.length, fromIndex).trim()
142
- clause = clause.replace(TYPE_IMPORT_PREFIX_REGEX, '')
143
- if (!clause)
144
- return
145
-
146
- // default + named: Button, { Input as UiInput }
147
- if (clause.includes('{')) {
148
- const [defaultPart, namedPart] = clause.split('{')
149
- const defaultName = defaultPart.replace(',', '').trim()
150
- if (defaultName)
151
- imported.add(defaultName)
152
-
153
- const raw = namedPart.replace('}', '')
154
- for (const segment of raw.split(',')) {
155
- const cleaned = segment.trim().replace(TYPE_IMPORT_PREFIX_REGEX, '')
156
- if (!cleaned)
157
- continue
158
- const [original] = cleaned.split(IMPORT_ALIAS_SPLIT_REGEX)
159
- if (original)
160
- imported.add(original.trim())
161
- }
162
- return
163
- }
164
-
165
- // namespace import: * as name
166
- if (clause.startsWith('* as ')) {
167
- const name = clause.replace('* as ', '').trim()
168
- if (name)
169
- imported.add(name)
170
- return
171
- }
172
-
173
- // default import
174
- imported.add(clause)
175
- }
176
-
177
- for (const line of lines) {
178
- const trimmed = line.trim()
179
- if (!buffer) {
180
- if (!trimmed.startsWith('import '))
181
- continue
182
- buffer = trimmed
183
- if (trimmed.endsWith(';')) {
184
- flushImport(buffer)
185
- buffer = ''
186
- }
133
+ // 复用 Svelte 的 TS 解析器识别绑定,兼容无分号、跨行、注释及别名。
134
+ const ast = parse(`<script lang="ts">${scriptContent}</script>`, { modern: true })
135
+ for (const statement of ast.instance?.content.body ?? []) {
136
+ if (statement.type !== 'ImportDeclaration' || statement.importKind === 'type')
187
137
  continue
188
- }
189
-
190
- buffer += ` ${trimmed}`
191
- if (trimmed.endsWith(';')) {
192
- flushImport(buffer)
193
- buffer = ''
138
+ for (const specifier of statement.specifiers) {
139
+ if (specifier.importKind !== 'type')
140
+ imported.add(specifier.local.name)
194
141
  }
195
142
  }
196
-
197
- if (buffer)
198
- flushImport(buffer)
199
-
200
143
  return imported
201
144
  }
202
145
 
@@ -28,6 +28,9 @@
28
28
  }: ModalProps & DataAttributes = $props()
29
29
 
30
30
  const dataAttributes = $derived(getDataAttributes(restProps))
31
+ const modalId = $props.id()
32
+ const titleId = `${modalId}-title`
33
+ let panel = $state<HTMLDivElement>()
31
34
 
32
35
  // 标题区仅在显式提供标题或自定义 header 时出现;
33
36
  // 仅需关闭按钮的场景改为右上角悬浮按钮,避免业务侧额外占一整行头部。
@@ -91,16 +94,60 @@
91
94
  onclose?.()
92
95
  }
93
96
 
97
+ function focusableElements(): HTMLElement[] {
98
+ return [...(panel?.querySelectorAll<HTMLElement>('button, a[href], input:not([type="hidden"]), select, textarea, [tabindex]') ?? [])]
99
+ .filter(element => element.tabIndex >= 0 && !element.hasAttribute('disabled') && element.getClientRects().length > 0)
100
+ }
101
+
102
+ // 打开时把焦点移入弹窗,关闭后恢复原操作位置。
103
+ $effect(() => {
104
+ if (!open || !panel)
105
+ return
106
+ const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null
107
+ const elements = focusableElements()
108
+ const input = elements.find(element => element.matches('input:not([readonly]), textarea, select'))
109
+ const target = input ?? elements[0] ?? panel
110
+ target.focus()
111
+ return () => {
112
+ if (previous?.isConnected)
113
+ previous.focus()
114
+ }
115
+ })
116
+
117
+ function handleKeydown(event: KeyboardEvent) {
118
+ if (!open)
119
+ return
120
+ if (event.key === 'Escape' && closeOnEscape) {
121
+ event.preventDefault()
122
+ event.stopPropagation()
123
+ handleClose()
124
+ }
125
+ else if (event.key === 'Tab') {
126
+ const elements = focusableElements()
127
+ const first = elements[0]
128
+ const last = elements.at(-1)
129
+ if (!first) {
130
+ event.preventDefault()
131
+ panel?.focus()
132
+ }
133
+ else if (event.shiftKey && (document.activeElement === first || document.activeElement === panel)) {
134
+ event.preventDefault()
135
+ last?.focus()
136
+ }
137
+ else if (!event.shiftKey && document.activeElement === last) {
138
+ event.preventDefault()
139
+ first.focus()
140
+ }
141
+ }
142
+ }
143
+
94
144
  </script>
95
145
 
96
146
  <div {...dataAttributes}
97
147
  class='hai-modal'
98
148
  style:display={open ? '' : 'none'}
99
149
  aria-hidden={open ? undefined : 'true'}
100
- onkeydown={(e) => {
101
- if (closeOnEscape && e.key === 'Escape')
102
- handleClose()
103
- }}
150
+ onkeydown={handleKeydown}
104
151
  >
105
152
  <div
106
153
  class='hai-modal__viewport'
@@ -117,7 +164,7 @@
117
164
  <div class='hai-modal__backdrop' aria-hidden='true'></div>
118
165
  {/if}
119
166
 
120
- <div class={modalBoxClass} style={panelStyle}>
167
+ <div bind:this={panel} class={modalBoxClass} style={panelStyle} role='dialog' aria-modal='true' aria-labelledby={title && !header ? titleId : undefined} aria-label={title && !header ? undefined : title || uiM('common_dialog')} tabindex='-1'>
121
168
  {#if hasFloatingClose}
122
169
  <button
123
170
  type='button'
@@ -145,7 +192,7 @@
145
192
  {@render header()}
146
193
  </div>
147
194
  {:else if title}
148
- <h3 class='flex-1 font-semibold text-[1.05rem] tracking-tight text-base-content/92'>
195
+ <h3 id={titleId} class='flex-1 font-semibold text-[1.05rem] tracking-tight text-base-content/92'>
149
196
  {title}
150
197
  </h3>
151
198
  {:else}
@@ -48,7 +48,9 @@
48
48
 
49
49
  const dataAttributes = $derived(getDataAttributes(restProps))
50
50
  function handleInput(e: Event & { currentTarget: HTMLInputElement }) {
51
- value = e.currentTarget.value
51
+ // 文件输入的 value 由浏览器管理,不能把 fakepath 写回原生控件。
52
+ if (type !== 'file')
53
+ value = e.currentTarget.value
52
54
  oninput?.(e)
53
55
  }
54
56
 
@@ -94,7 +96,7 @@
94
96
  aria-controls={ariaControls}
95
97
  aria-autocomplete={ariaAutocomplete}
96
98
  class={className}
97
- bind:value
99
+ value={type === 'file' ? undefined : value}
98
100
  oninput={handleInput}
99
101
  onchange={handleChange}
100
102
  onkeydown={handleKeydown}
@@ -10,7 +10,7 @@
10
10
  ```svelte
11
11
  <script>
12
12
  import { LanguageSwitch, createLocaleStore, setGlobalLocale } from '@h-ai/ui'
13
- import { setLocale, getLocale } from '../../../paraglide/runtime.js'
13
+ import { setLocale, getLocale } from '$lib/paraglide/runtime.js'
14
14
 
15
15
  const localeStore = createLocaleStore()
16
16
  </script>
@@ -31,7 +31,7 @@ type $$ComponentProps = Props & DataAttributes;
31
31
  * ```svelte
32
32
  * <script>
33
33
  * import { LanguageSwitch, createLocaleStore, setGlobalLocale } from '@h-ai/ui'
34
- * import { setLocale, getLocale } from '../../../paraglide/runtime.js'
34
+ * import { setLocale, getLocale } from '$lib/paraglide/runtime.js'
35
35
  *
36
36
  * const localeStore = createLocaleStore()
37
37
  * </script>
@@ -26,6 +26,8 @@
26
26
  alwaysEditable = false,
27
27
  loading = false,
28
28
  fields = ['avatar', 'username', 'email', 'displayName', 'phone'],
29
+ avatarAccept = 'image/*',
30
+ avatarHint,
29
31
  class: className = '',
30
32
  errors = {},
31
33
  onsubmit,
@@ -34,6 +36,7 @@
34
36
  }: UserProfileProps & DataAttributes = $props()
35
37
 
36
38
  const dataAttributes = $derived(getDataAttributes(restProps))
39
+ const avatarInputId = $props.id()
37
40
  let editMode = $state(false)
38
41
  let formData = $state<UserProfileSubmitData>({
39
42
  username: '',
@@ -163,24 +166,25 @@
163
166
  <Avatar
164
167
  src={user?.avatarUrl ?? user?.avatar}
165
168
  alt={uiM('user_profile_avatar')}
169
+ name={user?.displayName || user?.nickname || user?.username}
166
170
  size='lg'
167
- ring
168
171
  class='shadow-sm'
169
- placeholder={user?.username?.charAt(0).toUpperCase() || 'U'}
170
172
  />
171
173
  {#if editable && (editMode || alwaysEditable)}
172
174
  <div>
173
175
  <BareInput
176
+ id={avatarInputId}
174
177
  type='file'
175
- class='file-input file-input-sm w-full max-w-xs'
176
- accept='image/*'
178
+ class='hidden'
179
+ accept={avatarAccept}
177
180
  disabled={loading}
178
181
  onchange={handleAvatarChange}
179
182
  />
183
+ <Button type='button' variant='outline' size='sm' disabled={loading} onclick={() => document.getElementById(avatarInputId)?.click()}>{uiM('user_profile_upload_avatar')}</Button>
180
184
  {#if errors.avatar}
181
185
  <p class='text-xs text-error mt-1' role='alert'>{errors.avatar}</p>
182
186
  {:else}
183
- <p class='text-xs text-base-content/60 mt-1'>{uiM('user_profile_avatar_hint')}</p>
187
+ <p class='text-xs text-base-content/60 mt-1'>{avatarHint ?? uiM('user_profile_avatar_hint')}</p>
184
188
  {/if}
185
189
  </div>
186
190
  {/if}
@@ -299,6 +299,10 @@ export interface UserProfileProps extends DataAttributes {
299
299
  fields?: UserProfileField[];
300
300
  /** 头像上传地址 */
301
301
  avatarUploadUrl?: string;
302
+ /** 文件选择器允许的头像类型;最终校验由业务上传服务负责。 */
303
+ avatarAccept?: string;
304
+ /** 与业务上传限制一致的提示文案。 */
305
+ avatarHint?: string;
302
306
  /** 自定义类名 */
303
307
  class?: string;
304
308
  /** 错误信息 */
@@ -35,6 +35,7 @@
35
35
  "empty_title": "No data",
36
36
  "tag_input_placeholder": "Press Enter to add",
37
37
  "common_close": "Close",
38
+ "common_dialog": "Dialog",
38
39
  "modal_close": "Close modal",
39
40
  "drawer_close": "Close sidebar",
40
41
  "drawer_resize": "Resize drawer",
@@ -81,6 +82,7 @@
81
82
  "avatar_upload_invalid_type": "Please select an image file",
82
83
  "avatar_upload_failed": "Upload failed",
83
84
  "user_profile_avatar": "Avatar",
85
+ "user_profile_upload_avatar": "Upload avatar",
84
86
  "user_profile_username": "Username",
85
87
  "user_profile_email": "Email",
86
88
  "user_profile_display_name": "Display Name",
@@ -35,6 +35,7 @@
35
35
  "empty_title": "暂无数据",
36
36
  "tag_input_placeholder": "按 Enter 添加",
37
37
  "common_close": "关闭",
38
+ "common_dialog": "对话框",
38
39
  "modal_close": "关闭模态框",
39
40
  "drawer_close": "关闭侧边栏",
40
41
  "drawer_resize": "调整抽屉宽度",
@@ -81,6 +82,7 @@
81
82
  "avatar_upload_invalid_type": "请选择图片文件",
82
83
  "avatar_upload_failed": "上传失败",
83
84
  "user_profile_avatar": "头像",
85
+ "user_profile_upload_avatar": "上传头像",
84
86
  "user_profile_username": "用户名",
85
87
  "user_profile_email": "邮箱",
86
88
  "user_profile_display_name": "显示名称",
@@ -154,9 +154,11 @@ export declare function applyTheme(theme: string, persist?: boolean): void;
154
154
  */
155
155
  export declare function getCurrentTheme(): string;
156
156
  /**
157
- * 获取保存的主题(从 localStorage)
157
+ * 获取保存的主题;存储为空或不可用时使用宿主默认主题。
158
+ *
159
+ * @param fallbackTheme - 未保存主题时的默认值,默认 light
158
160
  */
159
- export declare function getSavedTheme(): string;
161
+ export declare function getSavedTheme(fallbackTheme?: string): string;
160
162
  /**
161
163
  * DaisyUI 主题配置字符串(用于 app.css)
162
164
  *
@@ -185,10 +185,12 @@ export function getCurrentTheme() {
185
185
  return document.documentElement.getAttribute('data-theme') ?? DEFAULT_THEME;
186
186
  }
187
187
  /**
188
- * 获取保存的主题(从 localStorage)
188
+ * 获取保存的主题;存储为空或不可用时使用宿主默认主题。
189
+ *
190
+ * @param fallbackTheme - 未保存主题时的默认值,默认 light
189
191
  */
190
- export function getSavedTheme() {
191
- return readStoredValue(THEME_STORAGE_KEY) ?? DEFAULT_THEME;
192
+ export function getSavedTheme(fallbackTheme = DEFAULT_THEME) {
193
+ return readStoredValue(THEME_STORAGE_KEY) ?? fallbackTheme;
192
194
  }
193
195
  /**
194
196
  * DaisyUI 主题配置字符串(用于 app.css)
package/dist/types.d.ts CHANGED
@@ -160,7 +160,7 @@ export interface InputProps extends DataAttributes {
160
160
  * 无样式输入框属性
161
161
  */
162
162
  export interface BareInputProps extends DataAttributes {
163
- /** 值 */
163
+ /** 文本值;file 类型通过事件读取 files,不绑定文件路径。 */
164
164
  value?: string;
165
165
  /** 占位符 */
166
166
  placeholder?: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@h-ai/ui",
3
3
  "type": "module",
4
- "version": "0.1.0-alpha.57",
4
+ "version": "0.1.0-alpha.58",
5
5
  "description": "Hai Framework UI component library for Svelte 5 Runes.",
6
6
  "license": "Apache-2.0",
7
7
  "svelte": "./dist/index.js",
@@ -19,6 +19,10 @@
19
19
  "types": "./vite.d.ts",
20
20
  "import": "./vite.js"
21
21
  },
22
+ "./theme": {
23
+ "types": "./dist/theme-config.d.ts",
24
+ "import": "./dist/theme-config.js"
25
+ },
22
26
  "./components/*": {
23
27
  "types": "./dist/components/*.d.ts",
24
28
  "svelte": "./dist/components/*.js",
@@ -39,35 +43,35 @@
39
43
  "access": "public"
40
44
  },
41
45
  "engines": {
42
- "node": ">=22.12.0"
46
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
43
47
  },
44
48
  "peerDependencies": {
45
- "svelte": "^5.55.9"
49
+ "svelte": "^5.57.2"
46
50
  },
47
51
  "dependencies": {
48
- "@internationalized/date": "^3.8.0",
49
- "@shikijs/langs": "^4.0.2",
50
- "bits-ui": "^2.18.1",
52
+ "@h-ai/core": "0.1.0-alpha.58",
53
+ "@internationalized/date": "^3.12.4",
54
+ "@shikijs/langs": "^4.5.0",
55
+ "bits-ui": "^2.19.5",
51
56
  "html2canvas": "^1.4.1",
52
57
  "jspdf": "^4.2.1",
53
- "layerchart": "2.0.1",
54
- "marked": "^18.0.0",
55
- "mermaid": "^11.12.0",
56
- "shiki": "^4.0.2",
57
- "tailwind-merge": "^3.5.0",
58
- "@h-ai/core": "0.1.0-alpha.57"
58
+ "layerchart": "2.6.1",
59
+ "marked": "^18.1.0",
60
+ "mermaid": "^12.1.0",
61
+ "shiki": "^4.5.0",
62
+ "tailwind-merge": "^3.7.0"
59
63
  },
60
64
  "devDependencies": {
61
- "@sveltejs/package": "^2.5.7",
62
- "@sveltejs/vite-plugin-svelte": "^7.1.2",
63
- "@types/node": "^25.9.1",
64
- "jsdom": "^29.0.2",
65
+ "@sveltejs/package": "^3.0.0",
66
+ "@sveltejs/vite-plugin-svelte": "^7.3.1",
67
+ "@types/node": "^26.6.5",
68
+ "jsdom": "^30.1.2",
65
69
  "rimraf": "^6.1.3",
66
- "svelte": "^5.55.9",
67
- "svelte-check": "^4.4.8",
70
+ "svelte": "^5.57.2",
71
+ "svelte-check": "^4.7.6",
68
72
  "typescript": "^6.0.3",
69
- "vite": "^8.0.14",
70
- "vitest": "^4.1.7"
73
+ "vite": "^8.3.4",
74
+ "vitest": "^4.1.11"
71
75
  },
72
76
  "scripts": {
73
77
  "build": "svelte-package -i src/lib",
@@ -75,7 +79,7 @@
75
79
  "check": "svelte-check --tsconfig ./tsconfig.json",
76
80
  "test": "vitest run",
77
81
  "test:watch": "vitest",
78
- "typecheck": "tsc --noEmit",
82
+ "typecheck": "node ../../scripts/typecheck.mjs --noEmit",
79
83
  "lint": "eslint .",
80
84
  "lint:fix": "eslint . --fix",
81
85
  "clean": "rimraf dist .turbo"