@h-ai/ui 0.1.0-alpha.57 → 0.1.0-alpha.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +17 -4
- package/README.ja.md +17 -4
- package/README.md +17 -4
- package/auto-import.js +9 -66
- package/dist/components/compounds/Modal.svelte +53 -6
- package/dist/components/primitives/BareInput.svelte +4 -2
- package/dist/components/scenes/app/LanguageSwitch.svelte +1 -1
- package/dist/components/scenes/app/LanguageSwitch.svelte.d.ts +1 -1
- package/dist/components/scenes/iam/UserProfile.svelte +9 -5
- package/dist/components/scenes/types.d.ts +4 -0
- package/dist/messages/en-US.json +2 -0
- package/dist/messages/zh-CN.json +2 -0
- package/dist/theme-config.d.ts +4 -2
- package/dist/theme-config.js +5 -3
- package/dist/types.d.ts +1 -1
- package/package.json +25 -21
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
|
|
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` |
|
|
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` |
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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={
|
|
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
|
|
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
|
-
|
|
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 '
|
|
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 '
|
|
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='
|
|
176
|
-
accept=
|
|
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
|
/** 错误信息 */
|
package/dist/messages/en-US.json
CHANGED
|
@@ -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",
|
package/dist/messages/zh-CN.json
CHANGED
|
@@ -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": "显示名称",
|
package/dist/theme-config.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
*
|
package/dist/theme-config.js
CHANGED
|
@@ -185,10 +185,12 @@ export function getCurrentTheme() {
|
|
|
185
185
|
return document.documentElement.getAttribute('data-theme') ?? DEFAULT_THEME;
|
|
186
186
|
}
|
|
187
187
|
/**
|
|
188
|
-
*
|
|
188
|
+
* 获取保存的主题;存储为空或不可用时使用宿主默认主题。
|
|
189
|
+
*
|
|
190
|
+
* @param fallbackTheme - 未保存主题时的默认值,默认 light
|
|
189
191
|
*/
|
|
190
|
-
export function getSavedTheme() {
|
|
191
|
-
return readStoredValue(THEME_STORAGE_KEY) ??
|
|
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.
|
|
4
|
+
"version": "0.1.0-alpha.59",
|
|
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": "
|
|
46
|
+
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
|
43
47
|
},
|
|
44
48
|
"peerDependencies": {
|
|
45
|
-
"svelte": "^5.
|
|
49
|
+
"svelte": "^5.57.2"
|
|
46
50
|
},
|
|
47
51
|
"dependencies": {
|
|
48
|
-
"@
|
|
49
|
-
"@
|
|
50
|
-
"
|
|
52
|
+
"@h-ai/core": "0.1.0-alpha.59",
|
|
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.
|
|
54
|
-
"marked": "^18.
|
|
55
|
-
"mermaid": "^
|
|
56
|
-
"shiki": "^4.0
|
|
57
|
-
"tailwind-merge": "^3.
|
|
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": "^
|
|
62
|
-
"@sveltejs/vite-plugin-svelte": "^7.1
|
|
63
|
-
"@types/node": "^
|
|
64
|
-
"jsdom": "^
|
|
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.
|
|
67
|
-
"svelte-check": "^4.
|
|
70
|
+
"svelte": "^5.57.2",
|
|
71
|
+
"svelte-check": "^4.7.6",
|
|
68
72
|
"typescript": "^6.0.3",
|
|
69
|
-
"vite": "^8.
|
|
70
|
-
"vitest": "^4.1.
|
|
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": "
|
|
82
|
+
"typecheck": "node ../../scripts/typecheck.mjs --noEmit",
|
|
79
83
|
"lint": "eslint .",
|
|
80
84
|
"lint:fix": "eslint . --fix",
|
|
81
85
|
"clean": "rimraf dist .turbo"
|