@h-ai/ui 0.1.0-alpha5

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 (234) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +488 -0
  3. package/auto-import.d.ts +96 -0
  4. package/auto-import.js +259 -0
  5. package/dist/components/compounds/Accordion.svelte +142 -0
  6. package/dist/components/compounds/Accordion.svelte.d.ts +25 -0
  7. package/dist/components/compounds/ActionSheet.svelte +116 -0
  8. package/dist/components/compounds/ActionSheet.svelte.d.ts +22 -0
  9. package/dist/components/compounds/Alert.svelte +67 -0
  10. package/dist/components/compounds/Alert.svelte.d.ts +8 -0
  11. package/dist/components/compounds/AppBar.svelte +77 -0
  12. package/dist/components/compounds/AppBar.svelte.d.ts +20 -0
  13. package/dist/components/compounds/BottomNav.svelte +75 -0
  14. package/dist/components/compounds/BottomNav.svelte.d.ts +18 -0
  15. package/dist/components/compounds/Breadcrumb.svelte +49 -0
  16. package/dist/components/compounds/Breadcrumb.svelte.d.ts +4 -0
  17. package/dist/components/compounds/Calendar.svelte +116 -0
  18. package/dist/components/compounds/Calendar.svelte.d.ts +47 -0
  19. package/dist/components/compounds/Card.svelte +77 -0
  20. package/dist/components/compounds/Card.svelte.d.ts +8 -0
  21. package/dist/components/compounds/Combobox.svelte +327 -0
  22. package/dist/components/compounds/Combobox.svelte.d.ts +69 -0
  23. package/dist/components/compounds/Confirm.svelte +90 -0
  24. package/dist/components/compounds/Confirm.svelte.d.ts +4 -0
  25. package/dist/components/compounds/DataTable.svelte +133 -0
  26. package/dist/components/compounds/DataTable.svelte.d.ts +69 -0
  27. package/dist/components/compounds/DatePicker.svelte +135 -0
  28. package/dist/components/compounds/DatePicker.svelte.d.ts +46 -0
  29. package/dist/components/compounds/Drawer.svelte +113 -0
  30. package/dist/components/compounds/Drawer.svelte.d.ts +4 -0
  31. package/dist/components/compounds/Dropdown.svelte +91 -0
  32. package/dist/components/compounds/Dropdown.svelte.d.ts +8 -0
  33. package/dist/components/compounds/Empty.svelte +83 -0
  34. package/dist/components/compounds/Empty.svelte.d.ts +4 -0
  35. package/dist/components/compounds/Form.svelte +96 -0
  36. package/dist/components/compounds/Form.svelte.d.ts +7 -0
  37. package/dist/components/compounds/FormField.svelte +56 -0
  38. package/dist/components/compounds/FormField.svelte.d.ts +4 -0
  39. package/dist/components/compounds/InfiniteScroll.svelte +82 -0
  40. package/dist/components/compounds/InfiniteScroll.svelte.d.ts +22 -0
  41. package/dist/components/compounds/Modal.svelte +123 -0
  42. package/dist/components/compounds/Modal.svelte.d.ts +8 -0
  43. package/dist/components/compounds/PageHeader.svelte +44 -0
  44. package/dist/components/compounds/PageHeader.svelte.d.ts +28 -0
  45. package/dist/components/compounds/Pagination.svelte +157 -0
  46. package/dist/components/compounds/Pagination.svelte.d.ts +4 -0
  47. package/dist/components/compounds/Popover.svelte +137 -0
  48. package/dist/components/compounds/Popover.svelte.d.ts +4 -0
  49. package/dist/components/compounds/PullRefresh.svelte +98 -0
  50. package/dist/components/compounds/PullRefresh.svelte.d.ts +16 -0
  51. package/dist/components/compounds/Result.svelte +84 -0
  52. package/dist/components/compounds/Result.svelte.d.ts +4 -0
  53. package/dist/components/compounds/SafeArea.svelte +37 -0
  54. package/dist/components/compounds/SafeArea.svelte.d.ts +12 -0
  55. package/dist/components/compounds/Skeleton.svelte +75 -0
  56. package/dist/components/compounds/Skeleton.svelte.d.ts +8 -0
  57. package/dist/components/compounds/Steps.svelte +111 -0
  58. package/dist/components/compounds/Steps.svelte.d.ts +4 -0
  59. package/dist/components/compounds/SwipeCell.svelte +115 -0
  60. package/dist/components/compounds/SwipeCell.svelte.d.ts +19 -0
  61. package/dist/components/compounds/Tabs.svelte +79 -0
  62. package/dist/components/compounds/Tabs.svelte.d.ts +8 -0
  63. package/dist/components/compounds/TagInput.svelte +108 -0
  64. package/dist/components/compounds/TagInput.svelte.d.ts +4 -0
  65. package/dist/components/compounds/Timeline.svelte +167 -0
  66. package/dist/components/compounds/Timeline.svelte.d.ts +25 -0
  67. package/dist/components/compounds/ToastContainer.svelte +59 -0
  68. package/dist/components/compounds/ToastContainer.svelte.d.ts +3 -0
  69. package/dist/components/compounds/Tooltip.svelte +41 -0
  70. package/dist/components/compounds/Tooltip.svelte.d.ts +4 -0
  71. package/dist/components/compounds/accordion-types.d.ts +16 -0
  72. package/dist/components/compounds/accordion-types.js +5 -0
  73. package/dist/components/compounds/action-sheet-types.d.ts +14 -0
  74. package/dist/components/compounds/action-sheet-types.js +5 -0
  75. package/dist/components/compounds/bottom-nav-types.d.ts +17 -0
  76. package/dist/components/compounds/bottom-nav-types.js +5 -0
  77. package/dist/components/compounds/index.d.ts +44 -0
  78. package/dist/components/compounds/index.js +54 -0
  79. package/dist/components/compounds/swipe-cell-types.d.ts +14 -0
  80. package/dist/components/compounds/swipe-cell-types.js +5 -0
  81. package/dist/components/compounds/timeline-types.d.ts +20 -0
  82. package/dist/components/compounds/timeline-types.js +5 -0
  83. package/dist/components/index.d.ts +9 -0
  84. package/dist/components/index.js +12 -0
  85. package/dist/components/primitives/Avatar.svelte +102 -0
  86. package/dist/components/primitives/Avatar.svelte.d.ts +8 -0
  87. package/dist/components/primitives/Badge.svelte +33 -0
  88. package/dist/components/primitives/Badge.svelte.d.ts +8 -0
  89. package/dist/components/primitives/BareButton.svelte +45 -0
  90. package/dist/components/primitives/BareButton.svelte.d.ts +4 -0
  91. package/dist/components/primitives/BareInput.svelte +101 -0
  92. package/dist/components/primitives/BareInput.svelte.d.ts +4 -0
  93. package/dist/components/primitives/Button.svelte +60 -0
  94. package/dist/components/primitives/Button.svelte.d.ts +8 -0
  95. package/dist/components/primitives/Checkbox.svelte +58 -0
  96. package/dist/components/primitives/Checkbox.svelte.d.ts +4 -0
  97. package/dist/components/primitives/IconButton.svelte +105 -0
  98. package/dist/components/primitives/IconButton.svelte.d.ts +4 -0
  99. package/dist/components/primitives/Input.svelte +130 -0
  100. package/dist/components/primitives/Input.svelte.d.ts +8 -0
  101. package/dist/components/primitives/Progress.svelte +58 -0
  102. package/dist/components/primitives/Progress.svelte.d.ts +4 -0
  103. package/dist/components/primitives/Radio.svelte +60 -0
  104. package/dist/components/primitives/Radio.svelte.d.ts +25 -0
  105. package/dist/components/primitives/Range.svelte +117 -0
  106. package/dist/components/primitives/Range.svelte.d.ts +27 -0
  107. package/dist/components/primitives/Rating.svelte +148 -0
  108. package/dist/components/primitives/Rating.svelte.d.ts +27 -0
  109. package/dist/components/primitives/Select.svelte +95 -0
  110. package/dist/components/primitives/Select.svelte.d.ts +25 -0
  111. package/dist/components/primitives/Spinner.svelte +54 -0
  112. package/dist/components/primitives/Spinner.svelte.d.ts +4 -0
  113. package/dist/components/primitives/Switch.svelte +53 -0
  114. package/dist/components/primitives/Switch.svelte.d.ts +4 -0
  115. package/dist/components/primitives/Tag.svelte +64 -0
  116. package/dist/components/primitives/Tag.svelte.d.ts +4 -0
  117. package/dist/components/primitives/Textarea.svelte +92 -0
  118. package/dist/components/primitives/Textarea.svelte.d.ts +4 -0
  119. package/dist/components/primitives/ToggleCheckbox.svelte +36 -0
  120. package/dist/components/primitives/ToggleCheckbox.svelte.d.ts +4 -0
  121. package/dist/components/primitives/ToggleInput.svelte +36 -0
  122. package/dist/components/primitives/ToggleInput.svelte.d.ts +4 -0
  123. package/dist/components/primitives/ToggleRadio.svelte +36 -0
  124. package/dist/components/primitives/ToggleRadio.svelte.d.ts +4 -0
  125. package/dist/components/primitives/index.d.ts +26 -0
  126. package/dist/components/primitives/index.js +30 -0
  127. package/dist/components/scenes/ai/AiDocumentDownloadMenu.svelte +387 -0
  128. package/dist/components/scenes/ai/AiDocumentDownloadMenu.svelte.d.ts +38 -0
  129. package/dist/components/scenes/ai/AiDocumentEditor.svelte +2259 -0
  130. package/dist/components/scenes/ai/AiDocumentEditor.svelte.d.ts +4 -0
  131. package/dist/components/scenes/ai/MarkdownRenderer.svelte +460 -0
  132. package/dist/components/scenes/ai/MarkdownRenderer.svelte.d.ts +4 -0
  133. package/dist/components/scenes/ai/document-download.d.ts +40 -0
  134. package/dist/components/scenes/ai/document-download.js +300 -0
  135. package/dist/components/scenes/ai/document-parse.d.ts +28 -0
  136. package/dist/components/scenes/ai/document-parse.js +243 -0
  137. package/dist/components/scenes/ai/document-types.d.ts +164 -0
  138. package/dist/components/scenes/ai/document-types.js +1 -0
  139. package/dist/components/scenes/ai/index.d.ts +17 -0
  140. package/dist/components/scenes/ai/index.js +13 -0
  141. package/dist/components/scenes/ai/markdown-parse.d.ts +32 -0
  142. package/dist/components/scenes/ai/markdown-parse.js +161 -0
  143. package/dist/components/scenes/app/FeedbackModal.svelte +160 -0
  144. package/dist/components/scenes/app/FeedbackModal.svelte.d.ts +47 -0
  145. package/dist/components/scenes/app/LanguageSwitch.svelte +136 -0
  146. package/dist/components/scenes/app/LanguageSwitch.svelte.d.ts +50 -0
  147. package/dist/components/scenes/app/SettingsModal.svelte +174 -0
  148. package/dist/components/scenes/app/SettingsModal.svelte.d.ts +31 -0
  149. package/dist/components/scenes/app/ThemeSelector.svelte +176 -0
  150. package/dist/components/scenes/app/ThemeSelector.svelte.d.ts +31 -0
  151. package/dist/components/scenes/app/ThemeToggle.svelte +76 -0
  152. package/dist/components/scenes/app/ThemeToggle.svelte.d.ts +29 -0
  153. package/dist/components/scenes/app/index.d.ts +11 -0
  154. package/dist/components/scenes/app/index.js +11 -0
  155. package/dist/components/scenes/crud/CrudDeleteConfirm.svelte +39 -0
  156. package/dist/components/scenes/crud/CrudDeleteConfirm.svelte.d.ts +18 -0
  157. package/dist/components/scenes/crud/CrudDetailDrawer.svelte +155 -0
  158. package/dist/components/scenes/crud/CrudDetailDrawer.svelte.d.ts +38 -0
  159. package/dist/components/scenes/crud/CrudEditDrawer.svelte +222 -0
  160. package/dist/components/scenes/crud/CrudEditDrawer.svelte.d.ts +48 -0
  161. package/dist/components/scenes/crud/CrudFilterBar.svelte +96 -0
  162. package/dist/components/scenes/crud/CrudFilterBar.svelte.d.ts +31 -0
  163. package/dist/components/scenes/crud/CrudPage.svelte +499 -0
  164. package/dist/components/scenes/crud/CrudPage.svelte.d.ts +114 -0
  165. package/dist/components/scenes/crud/crud-types.d.ts +135 -0
  166. package/dist/components/scenes/crud/crud-types.js +7 -0
  167. package/dist/components/scenes/crud/index.d.ts +12 -0
  168. package/dist/components/scenes/crud/index.js +11 -0
  169. package/dist/components/scenes/crypto/EncryptedInput.svelte +156 -0
  170. package/dist/components/scenes/crypto/EncryptedInput.svelte.d.ts +32 -0
  171. package/dist/components/scenes/crypto/HashDisplay.svelte +97 -0
  172. package/dist/components/scenes/crypto/HashDisplay.svelte.d.ts +4 -0
  173. package/dist/components/scenes/crypto/SignatureDisplay.svelte +161 -0
  174. package/dist/components/scenes/crypto/SignatureDisplay.svelte.d.ts +4 -0
  175. package/dist/components/scenes/crypto/index.d.ts +9 -0
  176. package/dist/components/scenes/crypto/index.js +11 -0
  177. package/dist/components/scenes/iam/ChangePasswordForm.svelte +134 -0
  178. package/dist/components/scenes/iam/ChangePasswordForm.svelte.d.ts +8 -0
  179. package/dist/components/scenes/iam/ForgotPasswordForm.svelte +147 -0
  180. package/dist/components/scenes/iam/ForgotPasswordForm.svelte.d.ts +4 -0
  181. package/dist/components/scenes/iam/LoginForm.svelte +191 -0
  182. package/dist/components/scenes/iam/LoginForm.svelte.d.ts +8 -0
  183. package/dist/components/scenes/iam/PasswordInput.svelte +203 -0
  184. package/dist/components/scenes/iam/PasswordInput.svelte.d.ts +49 -0
  185. package/dist/components/scenes/iam/PermGuard.svelte +69 -0
  186. package/dist/components/scenes/iam/PermGuard.svelte.d.ts +16 -0
  187. package/dist/components/scenes/iam/RegisterForm.svelte +269 -0
  188. package/dist/components/scenes/iam/RegisterForm.svelte.d.ts +8 -0
  189. package/dist/components/scenes/iam/ResetPasswordForm.svelte +153 -0
  190. package/dist/components/scenes/iam/ResetPasswordForm.svelte.d.ts +8 -0
  191. package/dist/components/scenes/iam/UserProfile.svelte +251 -0
  192. package/dist/components/scenes/iam/UserProfile.svelte.d.ts +4 -0
  193. package/dist/components/scenes/iam/index.d.ts +16 -0
  194. package/dist/components/scenes/iam/index.js +18 -0
  195. package/dist/components/scenes/iam/password-utils.d.ts +15 -0
  196. package/dist/components/scenes/iam/password-utils.js +19 -0
  197. package/dist/components/scenes/iam/permission-context.svelte.d.ts +90 -0
  198. package/dist/components/scenes/iam/permission-context.svelte.js +114 -0
  199. package/dist/components/scenes/index.d.ts +13 -0
  200. package/dist/components/scenes/index.js +20 -0
  201. package/dist/components/scenes/storage/AvatarUpload.svelte +218 -0
  202. package/dist/components/scenes/storage/AvatarUpload.svelte.d.ts +4 -0
  203. package/dist/components/scenes/storage/FileList.svelte +260 -0
  204. package/dist/components/scenes/storage/FileList.svelte.d.ts +4 -0
  205. package/dist/components/scenes/storage/FileUpload.svelte +400 -0
  206. package/dist/components/scenes/storage/FileUpload.svelte.d.ts +7 -0
  207. package/dist/components/scenes/storage/ImageUpload.svelte +285 -0
  208. package/dist/components/scenes/storage/ImageUpload.svelte.d.ts +4 -0
  209. package/dist/components/scenes/storage/index.d.ts +10 -0
  210. package/dist/components/scenes/storage/index.js +12 -0
  211. package/dist/components/scenes/types.d.ts +583 -0
  212. package/dist/components/scenes/types.js +7 -0
  213. package/dist/i18n.svelte.d.ts +44 -0
  214. package/dist/i18n.svelte.js +108 -0
  215. package/dist/index.d.ts +13 -0
  216. package/dist/index.js +19 -0
  217. package/dist/messages/en-US.json +242 -0
  218. package/dist/messages/zh-CN.json +242 -0
  219. package/dist/messages.d.ts +29 -0
  220. package/dist/messages.js +42 -0
  221. package/dist/styles/design-tokens.css +58 -0
  222. package/dist/styles/global.css +218 -0
  223. package/dist/styles/mobile.css +105 -0
  224. package/dist/theme-config.d.ts +92 -0
  225. package/dist/theme-config.js +122 -0
  226. package/dist/toast.svelte.d.ts +45 -0
  227. package/dist/toast.svelte.js +77 -0
  228. package/dist/types.d.ts +944 -0
  229. package/dist/types.js +7 -0
  230. package/dist/utils/platform.svelte.d.ts +54 -0
  231. package/dist/utils/platform.svelte.js +87 -0
  232. package/dist/utils.d.ts +46 -0
  233. package/dist/utils.js +100 -0
  234. package/package.json +69 -0
@@ -0,0 +1,29 @@
1
+ interface Props {
2
+ currentTheme?: string;
3
+ onchange?: (theme: string) => void;
4
+ lightTheme?: string;
5
+ darkTheme?: string;
6
+ labels?: {
7
+ switchToLight?: string;
8
+ switchToDark?: string;
9
+ };
10
+ class?: string;
11
+ }
12
+ /**
13
+ * ThemeToggle
14
+ * 主题切换按钮组件,支持明/暗主题快速切换。
15
+ *
16
+ * @prop {string} currentTheme - 当前主题
17
+ * @prop {function} onchange - 主题变更回调
18
+ * @prop {string} lightTheme - 亮色主题名称
19
+ * @prop {string} darkTheme - 暗色主题名称
20
+ *
21
+ * @example
22
+ * <ThemeToggle
23
+ * currentTheme={$theme}
24
+ * onchange={(theme) => themeStore.set(theme)}
25
+ * />
26
+ */
27
+ declare const ThemeToggle: import("svelte").Component<Props, {}, "">;
28
+ type ThemeToggle = ReturnType<typeof ThemeToggle>;
29
+ export default ThemeToggle;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @h-ai/ui — 应用级场景组件 (App Scenes)
3
+ *
4
+ * 面向应用层的通用功能组件,包含设置、反馈、语言/主题切换等。 内置中英文翻译,自动响应全局 locale。
5
+ * @module index
6
+ */
7
+ export { default as FeedbackModal } from './FeedbackModal.svelte';
8
+ export { default as LanguageSwitch } from './LanguageSwitch.svelte';
9
+ export { default as SettingsModal } from './SettingsModal.svelte';
10
+ export { default as ThemeSelector } from './ThemeSelector.svelte';
11
+ export { default as ThemeToggle } from './ThemeToggle.svelte';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @h-ai/ui — 应用级场景组件 (App Scenes)
3
+ *
4
+ * 面向应用层的通用功能组件,包含设置、反馈、语言/主题切换等。 内置中英文翻译,自动响应全局 locale。
5
+ * @module index
6
+ */
7
+ export { default as FeedbackModal } from './FeedbackModal.svelte';
8
+ export { default as LanguageSwitch } from './LanguageSwitch.svelte';
9
+ export { default as SettingsModal } from './SettingsModal.svelte';
10
+ export { default as ThemeSelector } from './ThemeSelector.svelte';
11
+ export { default as ThemeToggle } from './ThemeToggle.svelte';
@@ -0,0 +1,39 @@
1
+ <!--
2
+ @component CrudDeleteConfirm
3
+ CRUD 删除确认对话框
4
+
5
+ 使用 Svelte 5 Runes ($props)
6
+ 使用 compounds 组件:Confirm
7
+ -->
8
+ <script lang='ts'>
9
+ import { uiM } from '../../../messages.js'
10
+ import Confirm from '../../compounds/Confirm.svelte'
11
+
12
+ let {
13
+ open = $bindable(false),
14
+ loading = false,
15
+ title = '',
16
+ message = '',
17
+ onconfirm,
18
+ oncancel,
19
+ }: {
20
+ open?: boolean
21
+ loading?: boolean
22
+ title?: string
23
+ message?: string
24
+ onconfirm?: () => Promise<void> | void
25
+ oncancel?: () => void
26
+ } = $props()
27
+ </script>
28
+
29
+ <Confirm
30
+ bind:open
31
+ title={title || uiM('crud_delete_title')}
32
+ message={message || uiM('crud_delete_message')}
33
+ confirmText={uiM('crud_delete')}
34
+ cancelText={uiM('crud_cancel')}
35
+ variant='error'
36
+ {loading}
37
+ {onconfirm}
38
+ {oncancel}
39
+ />
@@ -0,0 +1,18 @@
1
+ type $$ComponentProps = {
2
+ open?: boolean;
3
+ loading?: boolean;
4
+ title?: string;
5
+ message?: string;
6
+ onconfirm?: () => Promise<void> | void;
7
+ oncancel?: () => void;
8
+ };
9
+ /**
10
+ * CrudDeleteConfirm
11
+ * CRUD 删除确认对话框
12
+ *
13
+ * 使用 Svelte 5 Runes ($props)
14
+ * 使用 compounds 组件:Confirm
15
+ */
16
+ declare const CrudDeleteConfirm: import("svelte").Component<$$ComponentProps, {}, "open">;
17
+ type CrudDeleteConfirm = ReturnType<typeof CrudDeleteConfirm>;
18
+ export default CrudDeleteConfirm;
@@ -0,0 +1,155 @@
1
+ <!--
2
+ @component CrudDetailDrawer
3
+ CRUD 详情抽屉组件(只读展示)
4
+
5
+ 使用 Svelte 5 Runes ($props, $derived)
6
+ 使用 compounds 组件:Drawer, Badge
7
+ -->
8
+ <script lang='ts'>
9
+ import type { Snippet } from 'svelte'
10
+ import type { Size } from '../../../types.js'
11
+ import { uiM } from '../../../messages.js'
12
+ import Drawer from '../../compounds/Drawer.svelte'
13
+ import Badge from '../../primitives/Badge.svelte'
14
+ import Button from '../../primitives/Button.svelte'
15
+
16
+ type FieldDef = {
17
+ id: string
18
+ label: string | (() => string)
19
+ type: string
20
+ inDetail?: boolean
21
+ options?: Array<{ label: string, value: string | number | boolean }> | (() => Array<{ label: string, value: string | number | boolean }>)
22
+ render?: (value: unknown, item: Record<string, unknown>) => string
23
+ order?: number
24
+ }
25
+
26
+ let {
27
+ open = $bindable(false),
28
+ item = null,
29
+ fields = [],
30
+ title = '',
31
+ size = '2xl' as Size,
32
+ canEdit = false,
33
+ onedit,
34
+ onclose,
35
+ detailExtra,
36
+ }: {
37
+ open?: boolean
38
+ item?: Record<string, unknown> | null
39
+ fields?: FieldDef[]
40
+ title?: string
41
+ size?: Size
42
+ canEdit?: boolean
43
+ onedit?: () => void
44
+ onclose?: () => void
45
+ detailExtra?: Snippet<[Record<string, unknown>]>
46
+ } = $props()
47
+
48
+ function resolveText(text: string | (() => string)): string {
49
+ return typeof text === 'function' ? text() : text
50
+ }
51
+
52
+ function resolveOptions(options?: FieldDef['options']): Array<{ label: string, value: string | number | boolean }> {
53
+ if (!options)
54
+ return []
55
+ return typeof options === 'function' ? options() : options
56
+ }
57
+
58
+ function formatValue(field: FieldDef, value: unknown): string {
59
+ if (value === null || value === undefined)
60
+ return uiM('crud_detail_empty')
61
+
62
+ if (field.render && item) {
63
+ return field.render(value, item)
64
+ }
65
+
66
+ // 选项类型:将 value 映射为 label
67
+ if (field.type === 'select' || field.type === 'radio') {
68
+ const opts = resolveOptions(field.options)
69
+ const found = opts.find(o => String(o.value) === String(value))
70
+ return found?.label ?? String(value)
71
+ }
72
+
73
+ // 多选类型
74
+ if (field.type === 'multi-select' && Array.isArray(value)) {
75
+ const opts = resolveOptions(field.options)
76
+ return value.map((v) => {
77
+ const found = opts.find(o => String(o.value) === String(v))
78
+ return found?.label ?? String(v)
79
+ }).join(', ')
80
+ }
81
+
82
+ // 布尔
83
+ if (field.type === 'boolean' || field.type === 'checkbox') {
84
+ return value ? '✓' : '✗'
85
+ }
86
+
87
+ // 日期/时间
88
+ if (field.type === 'date' && (typeof value === 'string' || typeof value === 'number' || value instanceof Date)) {
89
+ return new Date(value as string | number).toLocaleDateString()
90
+ }
91
+ if (field.type === 'datetime' && (typeof value === 'string' || typeof value === 'number' || value instanceof Date)) {
92
+ return new Date(value as string | number).toLocaleString()
93
+ }
94
+
95
+ return String(value)
96
+ }
97
+
98
+ const detailFields = $derived(
99
+ [...fields]
100
+ .filter(f => f.inDetail !== false)
101
+ .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
102
+ )
103
+
104
+ function handleClose() {
105
+ open = false
106
+ onclose?.()
107
+ }
108
+ </script>
109
+
110
+ <Drawer bind:open {title} position='right' {size} onclose={handleClose}>
111
+ {#if item}
112
+ <div class='space-y-4 pb-20'>
113
+ {#each detailFields as field}
114
+ {@const value = item[field.id]}
115
+ <div>
116
+ <div class='text-xs font-medium text-base-content/50 mb-1'>
117
+ {resolveText(field.label)}
118
+ </div>
119
+ <div class='text-sm text-base-content'>
120
+ {#if field.type === 'multi-select' && Array.isArray(value) && value.length > 0}
121
+ <div class='flex flex-wrap gap-1'>
122
+ {#each value as v}
123
+ {@const opts = resolveOptions(field.options)}
124
+ {@const found = opts.find(o => String(o.value) === String(v))}
125
+ <Badge variant='ghost' size='sm'>{found?.label ?? String(v)}</Badge>
126
+ {/each}
127
+ </div>
128
+ {:else}
129
+ {formatValue(field, value)}
130
+ {/if}
131
+ </div>
132
+ </div>
133
+ {/each}
134
+
135
+ {#if detailExtra && item}
136
+ <div class='border-t border-base-content/5 pt-4'>
137
+ {@render detailExtra(item)}
138
+ </div>
139
+ {/if}
140
+ </div>
141
+
142
+ <!-- 底部操作栏 -->
143
+ <div class='absolute bottom-0 left-0 right-0 p-4 bg-base-200 border-t border-base-content/10 flex justify-end gap-2'>
144
+ {#if canEdit}
145
+ <Button variant='primary' onclick={onedit}>
146
+ <span class='icon-[tabler--edit] size-4 mr-1'></span>
147
+ {uiM('crud_edit')}
148
+ </Button>
149
+ {/if}
150
+ <Button variant='ghost' onclick={handleClose}>
151
+ {uiM('crud_close')}
152
+ </Button>
153
+ </div>
154
+ {/if}
155
+ </Drawer>
@@ -0,0 +1,38 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { Size } from '../../../types.js';
3
+ type FieldDef = {
4
+ id: string;
5
+ label: string | (() => string);
6
+ type: string;
7
+ inDetail?: boolean;
8
+ options?: Array<{
9
+ label: string;
10
+ value: string | number | boolean;
11
+ }> | (() => Array<{
12
+ label: string;
13
+ value: string | number | boolean;
14
+ }>);
15
+ render?: (value: unknown, item: Record<string, unknown>) => string;
16
+ order?: number;
17
+ };
18
+ type $$ComponentProps = {
19
+ open?: boolean;
20
+ item?: Record<string, unknown> | null;
21
+ fields?: FieldDef[];
22
+ title?: string;
23
+ size?: Size;
24
+ canEdit?: boolean;
25
+ onedit?: () => void;
26
+ onclose?: () => void;
27
+ detailExtra?: Snippet<[Record<string, unknown>]>;
28
+ };
29
+ /**
30
+ * CrudDetailDrawer
31
+ * CRUD 详情抽屉组件(只读展示)
32
+ *
33
+ * 使用 Svelte 5 Runes ($props, $derived)
34
+ * 使用 compounds 组件:Drawer, Badge
35
+ */
36
+ declare const CrudDetailDrawer: import("svelte").Component<$$ComponentProps, {}, "open">;
37
+ type CrudDetailDrawer = ReturnType<typeof CrudDetailDrawer>;
38
+ export default CrudDetailDrawer;
@@ -0,0 +1,222 @@
1
+ <!--
2
+ @component CrudEditDrawer
3
+ CRUD 编辑/新建抽屉组件
4
+
5
+ 使用 Svelte 5 Runes ($props, $state, $derived)
6
+ 使用 compounds/primitives 组件:Drawer, FormField, Input, Select, Textarea, Checkbox, Button
7
+ -->
8
+ <script lang='ts'>
9
+ import type { Snippet } from 'svelte'
10
+ import type { Size } from '../../../types.js'
11
+ import { uiM } from '../../../messages.js'
12
+ import Drawer from '../../compounds/Drawer.svelte'
13
+ import FormField from '../../compounds/FormField.svelte'
14
+ import Button from '../../primitives/Button.svelte'
15
+ import Checkbox from '../../primitives/Checkbox.svelte'
16
+ import Input from '../../primitives/Input.svelte'
17
+ import Select from '../../primitives/Select.svelte'
18
+ import Textarea from '../../primitives/Textarea.svelte'
19
+
20
+ type FieldDef = {
21
+ id: string
22
+ label: string | (() => string)
23
+ type: string
24
+ readonly?: boolean
25
+ options?: Array<{ label: string, value: string | number | boolean }> | (() => Array<{ label: string, value: string | number | boolean }>)
26
+ validation?: { required?: boolean, min?: number, max?: number, pattern?: string, message?: string }
27
+ placeholder?: string | (() => string)
28
+ order?: number
29
+ }
30
+
31
+ let {
32
+ open = $bindable(false),
33
+ mode = 'create' as 'create' | 'edit',
34
+ fields = [],
35
+ formData = $bindable<Record<string, unknown>>({}),
36
+ title = '',
37
+ size = '2xl' as Size,
38
+ submitting = false,
39
+ error = '',
40
+ onsubmit,
41
+ onclose,
42
+ editFormExtra,
43
+ editingItem = null,
44
+ }: {
45
+ open?: boolean
46
+ mode?: 'create' | 'edit'
47
+ fields?: FieldDef[]
48
+ formData?: Record<string, unknown>
49
+ title?: string
50
+ size?: Size
51
+ submitting?: boolean
52
+ error?: string
53
+ onsubmit?: (data: Record<string, unknown>) => Promise<void>
54
+ onclose?: () => void
55
+ editFormExtra?: Snippet<[Record<string, unknown> | null, 'create' | 'edit']>
56
+ editingItem?: Record<string, unknown> | null
57
+ } = $props()
58
+
59
+ function resolveText(text: string | (() => string)): string {
60
+ return typeof text === 'function' ? text() : text
61
+ }
62
+
63
+ function resolveOptions(options?: FieldDef['options']): Array<{ label: string, value: string | number | boolean }> {
64
+ if (!options)
65
+ return []
66
+ return typeof options === 'function' ? options() : options
67
+ }
68
+
69
+ const sortedFields = $derived(
70
+ [...fields].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
71
+ )
72
+
73
+ function handleClose() {
74
+ open = false
75
+ onclose?.()
76
+ }
77
+
78
+ async function handleSubmit(e: Event) {
79
+ e.preventDefault()
80
+ await onsubmit?.(formData)
81
+ }
82
+
83
+ function getInputType(fieldType: string): string {
84
+ switch (fieldType) {
85
+ case 'email': return 'email'
86
+ case 'url': return 'url'
87
+ case 'tel': return 'tel'
88
+ case 'password': return 'password'
89
+ case 'number': return 'number'
90
+ case 'date': return 'text'
91
+ case 'datetime': return 'text'
92
+ default: return 'text'
93
+ }
94
+ }
95
+
96
+ function updateField(fieldId: string, value: unknown) {
97
+ formData = { ...formData, [fieldId]: value }
98
+ }
99
+
100
+ function toggleMultiSelect(fieldId: string, optValue: string | number | boolean) {
101
+ const current = (formData[fieldId] as Array<string | number | boolean>) ?? []
102
+ const strVal = String(optValue)
103
+ if (current.map(String).includes(strVal)) {
104
+ formData = { ...formData, [fieldId]: current.filter(v => String(v) !== strVal) }
105
+ }
106
+ else {
107
+ formData = { ...formData, [fieldId]: [...current, optValue] }
108
+ }
109
+ }
110
+ </script>
111
+
112
+ <Drawer bind:open {title} position='right' {size} onclose={handleClose} closeOnBackdrop={false}>
113
+ <form onsubmit={handleSubmit} class='space-y-4 pb-20'>
114
+ {#if error}
115
+ <div class='p-3 bg-error/10 border border-error/20 rounded-lg text-sm text-error flex items-center gap-2'>
116
+ <span class='icon-[tabler--alert-circle] size-4 shrink-0'></span>
117
+ <span>{error}</span>
118
+ </div>
119
+ {/if}
120
+
121
+ {#each sortedFields as field}
122
+ {@const isReadonly = field.readonly && mode === 'edit'}
123
+ {@const isRequired = field.validation?.required}
124
+ {@const fieldValue = formData[field.id]}
125
+ {@const placeholder = field.placeholder ? resolveText(field.placeholder) : ''}
126
+
127
+ {#if field.type === 'textarea'}
128
+ <FormField label={resolveText(field.label)} required={isRequired}>
129
+ <Textarea
130
+ value={String(fieldValue ?? '')}
131
+ {placeholder}
132
+ disabled={submitting || isReadonly}
133
+ required={isRequired}
134
+ oninput={e => updateField(field.id, (e.target as HTMLTextAreaElement).value)}
135
+ />
136
+ </FormField>
137
+
138
+ {:else if field.type === 'select' || field.type === 'radio'}
139
+ {@const opts = resolveOptions(field.options)}
140
+ <FormField label={resolveText(field.label)} required={isRequired}>
141
+ <Select
142
+ value={String(fieldValue ?? '')}
143
+ disabled={submitting || isReadonly}
144
+ onchange={e => updateField(field.id, (e.target as HTMLSelectElement).value)}
145
+ >
146
+ <option value="">{placeholder || uiM('crud_filter_all')}</option>
147
+ {#each opts as opt}
148
+ <option value={String(opt.value)}>{opt.label}</option>
149
+ {/each}
150
+ </Select>
151
+ </FormField>
152
+
153
+ {:else if field.type === 'multi-select'}
154
+ {@const opts = resolveOptions(field.options)}
155
+ {@const selectedValues = ((fieldValue ?? []) as Array<string | number | boolean>).map(String)}
156
+ <FormField label={resolveText(field.label)} required={isRequired}>
157
+ <div class='flex flex-wrap gap-3 p-3 bg-base-200 rounded-lg max-h-48 overflow-y-auto'>
158
+ {#each opts as opt}
159
+ <label class='inline-flex items-center gap-2 cursor-pointer'>
160
+ <Checkbox
161
+ size='sm'
162
+ checked={selectedValues.includes(String(opt.value))}
163
+ onchange={() => toggleMultiSelect(field.id, opt.value)}
164
+ disabled={submitting || isReadonly}
165
+ />
166
+ <span class='text-sm text-base-content'>{opt.label}</span>
167
+ </label>
168
+ {/each}
169
+ {#if opts.length === 0}
170
+ <span class='text-sm text-base-content/60'>{uiM('crud_no_data')}</span>
171
+ {/if}
172
+ </div>
173
+ </FormField>
174
+
175
+ {:else if field.type === 'boolean' || field.type === 'checkbox'}
176
+ <FormField label={resolveText(field.label)}>
177
+ <Checkbox
178
+ checked={Boolean(fieldValue)}
179
+ onchange={checked => updateField(field.id, checked)}
180
+ disabled={submitting || isReadonly}
181
+ />
182
+ </FormField>
183
+
184
+ {:else}
185
+ <FormField label={resolveText(field.label)} required={isRequired}>
186
+ <Input
187
+ type={getInputType(field.type)}
188
+ value={String(fieldValue ?? '')}
189
+ {placeholder}
190
+ disabled={submitting || isReadonly}
191
+ required={isRequired}
192
+ pattern={field.validation?.pattern}
193
+ minlength={field.type !== 'number' ? field.validation?.min : undefined}
194
+ maxlength={field.type !== 'number' ? field.validation?.max : undefined}
195
+ min={field.type === 'number' ? field.validation?.min : undefined}
196
+ max={field.type === 'number' ? field.validation?.max : undefined}
197
+ oninput={e => updateField(field.id, field.type === 'number' ? Number((e.currentTarget as HTMLInputElement).value) : (e.currentTarget as HTMLInputElement).value)}
198
+ />
199
+ </FormField>
200
+ {/if}
201
+ {/each}
202
+
203
+ {#if editFormExtra}
204
+ <div class='border-t border-base-content/5 pt-4'>
205
+ {@render editFormExtra(editingItem, mode)}
206
+ </div>
207
+ {/if}
208
+ </form>
209
+
210
+ <!-- 底部操作栏 -->
211
+ <div class='absolute bottom-0 left-0 right-0 p-4 bg-base-200 border-t border-base-content/10 flex justify-end gap-2'>
212
+ <Button variant='ghost' onclick={handleClose} disabled={submitting}>
213
+ {uiM('crud_cancel')}
214
+ </Button>
215
+ <Button variant='primary' onclick={handleSubmit} disabled={submitting}>
216
+ {#if submitting}
217
+ <span class='loading loading-spinner loading-xs mr-2'></span>
218
+ {/if}
219
+ {mode === 'create' ? uiM('crud_create') : uiM('crud_save')}
220
+ </Button>
221
+ </div>
222
+ </Drawer>
@@ -0,0 +1,48 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { Size } from '../../../types.js';
3
+ type FieldDef = {
4
+ id: string;
5
+ label: string | (() => string);
6
+ type: string;
7
+ readonly?: boolean;
8
+ options?: Array<{
9
+ label: string;
10
+ value: string | number | boolean;
11
+ }> | (() => Array<{
12
+ label: string;
13
+ value: string | number | boolean;
14
+ }>);
15
+ validation?: {
16
+ required?: boolean;
17
+ min?: number;
18
+ max?: number;
19
+ pattern?: string;
20
+ message?: string;
21
+ };
22
+ placeholder?: string | (() => string);
23
+ order?: number;
24
+ };
25
+ type $$ComponentProps = {
26
+ open?: boolean;
27
+ mode?: 'create' | 'edit';
28
+ fields?: FieldDef[];
29
+ formData?: Record<string, unknown>;
30
+ title?: string;
31
+ size?: Size;
32
+ submitting?: boolean;
33
+ error?: string;
34
+ onsubmit?: (data: Record<string, unknown>) => Promise<void>;
35
+ onclose?: () => void;
36
+ editFormExtra?: Snippet<[Record<string, unknown> | null, 'create' | 'edit']>;
37
+ editingItem?: Record<string, unknown> | null;
38
+ };
39
+ /**
40
+ * CrudEditDrawer
41
+ * CRUD 编辑/新建抽屉组件
42
+ *
43
+ * 使用 Svelte 5 Runes ($props, $state, $derived)
44
+ * 使用 compounds/primitives 组件:Drawer, FormField, Input, Select, Textarea, Checkbox, Button
45
+ */
46
+ declare const CrudEditDrawer: import("svelte").Component<$$ComponentProps, {}, "open" | "formData">;
47
+ type CrudEditDrawer = ReturnType<typeof CrudEditDrawer>;
48
+ export default CrudEditDrawer;
@@ -0,0 +1,96 @@
1
+ <!--
2
+ @component CrudFilterBar
3
+ CRUD 搜索 + 过滤栏组件
4
+
5
+ 使用 Svelte 5 Runes ($props, $state)
6
+ -->
7
+ <script lang='ts'>
8
+ import { uiM } from '../../../messages.js'
9
+ import Input from '../../primitives/Input.svelte'
10
+ import Select from '../../primitives/Select.svelte'
11
+
12
+ type FilterFieldDef = {
13
+ id: string
14
+ label: string | (() => string)
15
+ type: string
16
+ options?: Array<{ label: string, value: string | number | boolean }> | (() => Array<{ label: string, value: string | number | boolean }>)
17
+ }
18
+
19
+ let {
20
+ searchable = true,
21
+ searchPlaceholder = '',
22
+ searchValue = $bindable(''),
23
+ filterFields = [],
24
+ filterValues = $bindable<Record<string, unknown>>({}),
25
+ total = 0,
26
+ onsearch,
27
+ onfilterchange,
28
+ }: {
29
+ searchable?: boolean
30
+ searchPlaceholder?: string
31
+ searchValue?: string
32
+ filterFields?: FilterFieldDef[]
33
+ filterValues?: Record<string, unknown>
34
+ total?: number
35
+ onsearch?: (search: string) => void
36
+ onfilterchange?: (filters: Record<string, unknown>) => void
37
+ } = $props()
38
+
39
+ let searchTimer: ReturnType<typeof setTimeout> | undefined
40
+
41
+ function resolveOptions(options?: FilterFieldDef['options']): Array<{ label: string, value: string | number | boolean }> {
42
+ if (!options)
43
+ return []
44
+ return typeof options === 'function' ? options() : options
45
+ }
46
+
47
+ function handleSearchInput() {
48
+ clearTimeout(searchTimer)
49
+ searchTimer = setTimeout(() => {
50
+ onsearch?.(searchValue)
51
+ }, 400)
52
+ }
53
+
54
+ function handleFilterChange(fieldId: string, value: unknown) {
55
+ filterValues = { ...filterValues, [fieldId]: value }
56
+ onfilterchange?.(filterValues)
57
+ }
58
+ </script>
59
+
60
+ <div class='p-4'>
61
+ <div class='flex flex-col sm:flex-row gap-3'>
62
+ {#if searchable}
63
+ <div class='flex-1 relative'>
64
+ <span class='icon-[tabler--search] size-4.5 absolute left-3 top-1/2 -translate-y-1/2 text-base-content/35 z-10'></span>
65
+ <Input
66
+ type='text'
67
+ placeholder={searchPlaceholder || uiM('crud_search_placeholder')}
68
+ class='pl-10'
69
+ bind:value={searchValue}
70
+ oninput={handleSearchInput}
71
+ autocomplete='off'
72
+ />
73
+ </div>
74
+ {/if}
75
+
76
+ {#each filterFields as field}
77
+ {@const opts = resolveOptions(field.options)}
78
+ {#if opts.length > 0}
79
+ <Select
80
+ class='min-w-32'
81
+ value={String(filterValues[field.id] ?? '')}
82
+ onchange={e => handleFilterChange(field.id, (e.target as HTMLSelectElement).value)}
83
+ >
84
+ <option value="">{uiM('crud_filter_all')}</option>
85
+ {#each opts as opt}
86
+ <option value={String(opt.value)}>{opt.label}</option>
87
+ {/each}
88
+ </Select>
89
+ {/if}
90
+ {/each}
91
+
92
+ <div class='text-sm text-base-content/50 self-center whitespace-nowrap'>
93
+ {uiM('crud_total', { count: total })}
94
+ </div>
95
+ </div>
96
+ </div>