@h-ai/ui 0.1.0-alpha.29 → 0.1.0-alpha.31

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 (194) hide show
  1. package/README.md +50 -3
  2. package/dist/components/compounds/Accordion.svelte +6 -3
  3. package/dist/components/compounds/Accordion.svelte.d.ts +3 -1
  4. package/dist/components/compounds/ActionSheet.svelte +11 -8
  5. package/dist/components/compounds/ActionSheet.svelte.d.ts +3 -1
  6. package/dist/components/compounds/Alert.svelte +6 -4
  7. package/dist/components/compounds/Alert.svelte.d.ts +3 -2
  8. package/dist/components/compounds/AppBar.svelte +13 -9
  9. package/dist/components/compounds/AppBar.svelte.d.ts +3 -1
  10. package/dist/components/compounds/BottomNav.svelte +13 -10
  11. package/dist/components/compounds/BottomNav.svelte.d.ts +3 -1
  12. package/dist/components/compounds/Breadcrumb.svelte +6 -4
  13. package/dist/components/compounds/Breadcrumb.svelte.d.ts +3 -2
  14. package/dist/components/compounds/Calendar.svelte +7 -2
  15. package/dist/components/compounds/Calendar.svelte.d.ts +3 -1
  16. package/dist/components/compounds/Card.svelte +6 -4
  17. package/dist/components/compounds/Card.svelte.d.ts +3 -2
  18. package/dist/components/compounds/Combobox.svelte +6 -2
  19. package/dist/components/compounds/Combobox.svelte.d.ts +3 -1
  20. package/dist/components/compounds/Confirm.svelte +9 -7
  21. package/dist/components/compounds/Confirm.svelte.d.ts +3 -2
  22. package/dist/components/compounds/DataTable.svelte +6 -2
  23. package/dist/components/compounds/DataTable.svelte.d.ts +2 -1
  24. package/dist/components/compounds/DatePicker.svelte +7 -2
  25. package/dist/components/compounds/DatePicker.svelte.d.ts +3 -1
  26. package/dist/components/compounds/Drawer.svelte +6 -4
  27. package/dist/components/compounds/Drawer.svelte.d.ts +3 -2
  28. package/dist/components/compounds/Dropdown.svelte +6 -4
  29. package/dist/components/compounds/Dropdown.svelte.d.ts +3 -2
  30. package/dist/components/compounds/Empty.svelte +6 -4
  31. package/dist/components/compounds/Empty.svelte.d.ts +3 -2
  32. package/dist/components/compounds/Form.svelte +11 -9
  33. package/dist/components/compounds/Form.svelte.d.ts +3 -2
  34. package/dist/components/compounds/FormField.svelte +6 -4
  35. package/dist/components/compounds/FormField.svelte.d.ts +3 -2
  36. package/dist/components/compounds/InfiniteScroll.svelte +10 -7
  37. package/dist/components/compounds/InfiniteScroll.svelte.d.ts +3 -1
  38. package/dist/components/compounds/MessageBoxContainer.svelte +6 -1
  39. package/dist/components/compounds/MessageBoxContainer.svelte.d.ts +3 -17
  40. package/dist/components/compounds/MessageBoxDialog.svelte +9 -7
  41. package/dist/components/compounds/MessageBoxDialog.svelte.d.ts +2 -1
  42. package/dist/components/compounds/MessageContainer.svelte +43 -66
  43. package/dist/components/compounds/MessageContainer.svelte.d.ts +2 -1
  44. package/dist/components/compounds/Modal.svelte +29 -43
  45. package/dist/components/compounds/Modal.svelte.d.ts +3 -2
  46. package/dist/components/compounds/PageHeader.svelte +6 -2
  47. package/dist/components/compounds/PageHeader.svelte.d.ts +3 -1
  48. package/dist/components/compounds/Pagination.svelte +6 -4
  49. package/dist/components/compounds/Pagination.svelte.d.ts +3 -2
  50. package/dist/components/compounds/Popover.svelte +10 -8
  51. package/dist/components/compounds/Popover.svelte.d.ts +3 -2
  52. package/dist/components/compounds/PullRefresh.svelte +11 -8
  53. package/dist/components/compounds/PullRefresh.svelte.d.ts +3 -1
  54. package/dist/components/compounds/Result.svelte +6 -4
  55. package/dist/components/compounds/Result.svelte.d.ts +3 -2
  56. package/dist/components/compounds/SafeArea.svelte +6 -3
  57. package/dist/components/compounds/SafeArea.svelte.d.ts +3 -1
  58. package/dist/components/compounds/Skeleton.svelte +6 -4
  59. package/dist/components/compounds/Skeleton.svelte.d.ts +3 -2
  60. package/dist/components/compounds/Steps.svelte +6 -4
  61. package/dist/components/compounds/Steps.svelte.d.ts +3 -2
  62. package/dist/components/compounds/SwipeCell.svelte +6 -3
  63. package/dist/components/compounds/SwipeCell.svelte.d.ts +3 -1
  64. package/dist/components/compounds/Tabs.svelte +6 -4
  65. package/dist/components/compounds/Tabs.svelte.d.ts +3 -2
  66. package/dist/components/compounds/TagInput.svelte +11 -9
  67. package/dist/components/compounds/TagInput.svelte.d.ts +3 -2
  68. package/dist/components/compounds/Timeline.svelte +6 -3
  69. package/dist/components/compounds/Timeline.svelte.d.ts +3 -1
  70. package/dist/components/compounds/ToastContainer.svelte +6 -2
  71. package/dist/components/compounds/ToastContainer.svelte.d.ts +2 -1
  72. package/dist/components/compounds/Tooltip.svelte +6 -4
  73. package/dist/components/compounds/Tooltip.svelte.d.ts +3 -2
  74. package/dist/components/primitives/Avatar.svelte +6 -4
  75. package/dist/components/primitives/Avatar.svelte.d.ts +3 -2
  76. package/dist/components/primitives/Badge.svelte +6 -4
  77. package/dist/components/primitives/Badge.svelte.d.ts +3 -2
  78. package/dist/components/primitives/BareButton.svelte +15 -12
  79. package/dist/components/primitives/BareButton.svelte.d.ts +3 -2
  80. package/dist/components/primitives/BareInput.svelte +37 -34
  81. package/dist/components/primitives/BareInput.svelte.d.ts +3 -2
  82. package/dist/components/primitives/Button.svelte +12 -10
  83. package/dist/components/primitives/Button.svelte.d.ts +3 -2
  84. package/dist/components/primitives/Checkbox.svelte +6 -4
  85. package/dist/components/primitives/Checkbox.svelte.d.ts +3 -2
  86. package/dist/components/primitives/IconButton.svelte +6 -4
  87. package/dist/components/primitives/IconButton.svelte.d.ts +3 -2
  88. package/dist/components/primitives/Input.svelte +6 -4
  89. package/dist/components/primitives/Input.svelte.d.ts +3 -2
  90. package/dist/components/primitives/Progress.svelte +6 -4
  91. package/dist/components/primitives/Progress.svelte.d.ts +3 -2
  92. package/dist/components/primitives/Radio.svelte +6 -4
  93. package/dist/components/primitives/Radio.svelte.d.ts +2 -2
  94. package/dist/components/primitives/Range.svelte +6 -3
  95. package/dist/components/primitives/Range.svelte.d.ts +3 -1
  96. package/dist/components/primitives/Rating.svelte +6 -3
  97. package/dist/components/primitives/Rating.svelte.d.ts +3 -1
  98. package/dist/components/primitives/Select.svelte +255 -48
  99. package/dist/components/primitives/Select.svelte.d.ts +2 -2
  100. package/dist/components/primitives/Spinner.svelte +6 -4
  101. package/dist/components/primitives/Spinner.svelte.d.ts +3 -2
  102. package/dist/components/primitives/Switch.svelte +6 -4
  103. package/dist/components/primitives/Switch.svelte.d.ts +3 -2
  104. package/dist/components/primitives/Tag.svelte +6 -4
  105. package/dist/components/primitives/Tag.svelte.d.ts +3 -2
  106. package/dist/components/primitives/Textarea.svelte +34 -19
  107. package/dist/components/primitives/Textarea.svelte.d.ts +3 -2
  108. package/dist/components/primitives/ToggleCheckbox.svelte +13 -10
  109. package/dist/components/primitives/ToggleCheckbox.svelte.d.ts +3 -2
  110. package/dist/components/primitives/ToggleInput.svelte +13 -10
  111. package/dist/components/primitives/ToggleInput.svelte.d.ts +3 -2
  112. package/dist/components/primitives/ToggleRadio.svelte +13 -10
  113. package/dist/components/primitives/ToggleRadio.svelte.d.ts +3 -2
  114. package/dist/components/scenes/ai/AiDocumentDownloadMenu.svelte +8 -5
  115. package/dist/components/scenes/ai/AiDocumentDownloadMenu.svelte.d.ts +3 -1
  116. package/dist/components/scenes/ai/AiDocumentEditor.svelte +40 -10
  117. package/dist/components/scenes/ai/AiDocumentEditor.svelte.d.ts +3 -1
  118. package/dist/components/scenes/ai/MarkdownRenderer.svelte +49 -7
  119. package/dist/components/scenes/ai/MarkdownRenderer.svelte.d.ts +3 -1
  120. package/dist/components/scenes/ai/document-parse.d.ts +2 -0
  121. package/dist/components/scenes/ai/document-parse.js +16 -17
  122. package/dist/components/scenes/ai/document-types.d.ts +6 -1
  123. package/dist/components/scenes/ai/markdown-parse.d.ts +2 -0
  124. package/dist/components/scenes/ai/markdown-parse.js +17 -20
  125. package/dist/components/scenes/ai/safe-html.d.ts +16 -0
  126. package/dist/components/scenes/ai/safe-html.js +284 -0
  127. package/dist/components/scenes/ai/table/AiTableEditor.svelte +6 -3
  128. package/dist/components/scenes/ai/table/AiTableEditor.svelte.d.ts +3 -1
  129. package/dist/components/scenes/ai/table/table-types.d.ts +2 -1
  130. package/dist/components/scenes/app/FeedbackModal.svelte +5 -2
  131. package/dist/components/scenes/app/FeedbackModal.svelte.d.ts +3 -1
  132. package/dist/components/scenes/app/LanguageSwitch.svelte +6 -2
  133. package/dist/components/scenes/app/LanguageSwitch.svelte.d.ts +3 -1
  134. package/dist/components/scenes/app/SettingsLayout.svelte +6 -3
  135. package/dist/components/scenes/app/SettingsLayout.svelte.d.ts +2 -1
  136. package/dist/components/scenes/app/SettingsModal.svelte +6 -3
  137. package/dist/components/scenes/app/SettingsModal.svelte.d.ts +3 -1
  138. package/dist/components/scenes/app/ThemeColorPicker.svelte +6 -3
  139. package/dist/components/scenes/app/ThemeColorPicker.svelte.d.ts +3 -1
  140. package/dist/components/scenes/app/ThemeSelector.svelte +6 -3
  141. package/dist/components/scenes/app/ThemeSelector.svelte.d.ts +3 -1
  142. package/dist/components/scenes/app/ThemeToggle.svelte +6 -2
  143. package/dist/components/scenes/app/ThemeToggle.svelte.d.ts +3 -1
  144. package/dist/components/scenes/crud/CrudDeleteConfirm.svelte +16 -11
  145. package/dist/components/scenes/crud/CrudDeleteConfirm.svelte.d.ts +2 -1
  146. package/dist/components/scenes/crud/CrudDetailDrawer.svelte +6 -3
  147. package/dist/components/scenes/crud/CrudDetailDrawer.svelte.d.ts +2 -2
  148. package/dist/components/scenes/crud/CrudDetailPanel.svelte +6 -3
  149. package/dist/components/scenes/crud/CrudDetailPanel.svelte.d.ts +2 -2
  150. package/dist/components/scenes/crud/CrudEditPanel.svelte +6 -3
  151. package/dist/components/scenes/crud/CrudEditPanel.svelte.d.ts +2 -2
  152. package/dist/components/scenes/crud/CrudFilterBar.svelte +6 -2
  153. package/dist/components/scenes/crud/CrudFilterBar.svelte.d.ts +2 -1
  154. package/dist/components/scenes/crud/CrudPage.svelte +6 -2
  155. package/dist/components/scenes/crud/CrudPage.svelte.d.ts +2 -1
  156. package/dist/components/scenes/crud/crud-types.d.ts +2 -2
  157. package/dist/components/scenes/crypto/EncryptedInput.svelte +6 -3
  158. package/dist/components/scenes/crypto/EncryptedInput.svelte.d.ts +3 -2
  159. package/dist/components/scenes/crypto/HashDisplay.svelte +6 -3
  160. package/dist/components/scenes/crypto/HashDisplay.svelte.d.ts +3 -1
  161. package/dist/components/scenes/crypto/SignatureDisplay.svelte +6 -3
  162. package/dist/components/scenes/crypto/SignatureDisplay.svelte.d.ts +3 -1
  163. package/dist/components/scenes/error/ErrorPage.svelte +6 -3
  164. package/dist/components/scenes/error/ErrorPage.svelte.d.ts +2 -1
  165. package/dist/components/scenes/iam/AuthShell.svelte +6 -3
  166. package/dist/components/scenes/iam/AuthShell.svelte.d.ts +2 -1
  167. package/dist/components/scenes/iam/ChangePasswordForm.svelte +6 -3
  168. package/dist/components/scenes/iam/ChangePasswordForm.svelte.d.ts +3 -1
  169. package/dist/components/scenes/iam/ForgotPasswordForm.svelte +6 -3
  170. package/dist/components/scenes/iam/ForgotPasswordForm.svelte.d.ts +3 -1
  171. package/dist/components/scenes/iam/LoginForm.svelte +6 -3
  172. package/dist/components/scenes/iam/LoginForm.svelte.d.ts +3 -1
  173. package/dist/components/scenes/iam/PasswordInput.svelte +6 -4
  174. package/dist/components/scenes/iam/PasswordInput.svelte.d.ts +3 -2
  175. package/dist/components/scenes/iam/RegisterForm.svelte +6 -4
  176. package/dist/components/scenes/iam/RegisterForm.svelte.d.ts +3 -1
  177. package/dist/components/scenes/iam/ResetPasswordForm.svelte +6 -3
  178. package/dist/components/scenes/iam/ResetPasswordForm.svelte.d.ts +3 -1
  179. package/dist/components/scenes/iam/UserProfile.svelte +6 -3
  180. package/dist/components/scenes/iam/UserProfile.svelte.d.ts +3 -1
  181. package/dist/components/scenes/storage/AvatarUpload.svelte +11 -8
  182. package/dist/components/scenes/storage/AvatarUpload.svelte.d.ts +3 -1
  183. package/dist/components/scenes/storage/FileList.svelte +6 -3
  184. package/dist/components/scenes/storage/FileList.svelte.d.ts +3 -1
  185. package/dist/components/scenes/storage/FileUpload.svelte +6 -3
  186. package/dist/components/scenes/storage/FileUpload.svelte.d.ts +3 -1
  187. package/dist/components/scenes/storage/ImageUpload.svelte +12 -9
  188. package/dist/components/scenes/storage/ImageUpload.svelte.d.ts +3 -1
  189. package/dist/components/scenes/types.d.ts +20 -16
  190. package/dist/styles/global.css +5 -0
  191. package/dist/types.d.ts +48 -41
  192. package/dist/utils.d.ts +6 -1
  193. package/dist/utils.js +16 -1
  194. package/package.json +2 -2
@@ -9,7 +9,8 @@
9
9
  =============================================================================
10
10
  -->
11
11
  <script lang='ts'>
12
- import { cn } from '../../utils.js'
12
+ import type { DataAttributes } from '../../types.js'
13
+ import { cn, getDataAttributes } from '../../utils.js'
13
14
  import BareInput from './BareInput.svelte'
14
15
 
15
16
  interface Props {
@@ -49,8 +50,10 @@
49
50
  onchange,
50
51
  oninput,
51
52
  class: className = '',
52
- }: Props = $props()
53
+ ...restProps
54
+ }: Props & DataAttributes = $props()
53
55
 
56
+ const dataAttributes = $derived(getDataAttributes(restProps))
54
57
  const sizeClass = $derived({
55
58
  xs: 'range-xs',
56
59
  sm: 'range-sm',
@@ -94,7 +97,7 @@
94
97
  }
95
98
  </script>
96
99
 
97
- <div class='w-full'>
100
+ <div {...dataAttributes} class='w-full'>
98
101
  <BareInput
99
102
  type='range'
100
103
  class={rangeClass}
@@ -1,3 +1,4 @@
1
+ import type { DataAttributes } from '../../types.js';
1
2
  interface Props {
2
3
  /** 当前值 */
3
4
  value?: number;
@@ -22,6 +23,7 @@ interface Props {
22
23
  /** 自定义类名 */
23
24
  class?: string;
24
25
  }
25
- declare const Range: import("svelte").Component<Props, {}, "value">;
26
+ type $$ComponentProps = Props & DataAttributes;
27
+ declare const Range: import("svelte").Component<$$ComponentProps, {}, "value">;
26
28
  type Range = ReturnType<typeof Range>;
27
29
  export default Range;
@@ -9,8 +9,9 @@
9
9
  =============================================================================
10
10
  -->
11
11
  <script lang='ts'>
12
+ import type { DataAttributes } from '../../types.js'
12
13
  import { uiM } from '../../messages.js'
13
- import { cn } from '../../utils.js'
14
+ import { cn, getDataAttributes } from '../../utils.js'
14
15
 
15
16
  interface Props {
16
17
  /** 当前评分值 (0 - max) */
@@ -49,8 +50,10 @@
49
50
  name = `rating-${Math.random().toString(36).slice(2, 9)}`,
50
51
  onchange,
51
52
  class: className = '',
52
- }: Props = $props()
53
+ ...restProps
54
+ }: Props & DataAttributes = $props()
53
55
 
56
+ const dataAttributes = $derived(getDataAttributes(restProps))
54
57
  const sizeClass = $derived({
55
58
  xs: 'rating-xs',
56
59
  sm: 'rating-sm',
@@ -110,7 +113,7 @@
110
113
 
111
114
  {#if readonly}
112
115
  <!-- 只读模式 - 使用 div 而非 input -->
113
- <div class={cn('rating', sizeClass, className)}>
116
+ <div {...dataAttributes} class={cn('rating', sizeClass, className)}>
114
117
  {#each Array.from({ length: max }) as _, i (i)}
115
118
  <div
116
119
  class={cn(
@@ -1,3 +1,4 @@
1
+ import type { DataAttributes } from '../../types.js';
1
2
  interface Props {
2
3
  /** 当前评分值 (0 - max) */
3
4
  value?: number;
@@ -22,6 +23,7 @@ interface Props {
22
23
  /** 自定义类名 */
23
24
  class?: string;
24
25
  }
25
- declare const Rating: import("svelte").Component<Props, {}, "value">;
26
+ type $$ComponentProps = Props & DataAttributes;
27
+ declare const Rating: import("svelte").Component<$$ComponentProps, {}, "value">;
26
28
  type Rating = ReturnType<typeof Rating>;
27
29
  export default Rating;
@@ -2,15 +2,15 @@
2
2
  =============================================================================
3
3
  @h-ai/ui - Select 组件
4
4
  =============================================================================
5
- 下拉选择框组件
5
+ 下拉选择框组件(全自定义实现,统一弹出层样式)
6
6
 
7
7
  使用 Svelte 5 Runes ($props, $derived, $bindable)
8
- 支持自定义验证消息(validationMessage)覆盖浏览器原生提示
8
+ 支持 clearable(可清空)和 filterable(可筛选)属性
9
9
  =============================================================================
10
10
  -->
11
11
  <script lang='ts' generics="T = string">
12
- import type { SelectProps } from '../../types.js'
13
- import { cn, getSizeClass } from '../../utils.js'
12
+ import type { DataAttributes, SelectProps } from '../../types.js'
13
+ import { cn, getDataAttributes } from '../../utils.js'
14
14
 
15
15
  let {
16
16
  value = $bindable<T>(),
@@ -19,76 +19,283 @@
19
19
  size = 'md',
20
20
  disabled = false,
21
21
  required = false,
22
+ clearable = false,
23
+ filterable = false,
22
24
  error = '',
23
- validationMessage = '',
24
25
  class: className = '',
25
26
  id,
26
27
  onchange,
27
- children,
28
- }: SelectProps<T> = $props()
28
+ ...restProps
29
+ }: SelectProps<T> & DataAttributes = $props()
29
30
 
30
- let selectRef: HTMLSelectElement | undefined = $state()
31
+ const dataAttributes = $derived(getDataAttributes(restProps))
32
+ let inputRef: HTMLInputElement | undefined = $state()
33
+ let dropdownRef: HTMLDivElement | undefined = $state()
34
+ let isDropdownOpen = $state(false)
35
+ let filterText = $state('')
36
+ let isHovered = $state(false)
31
37
 
32
- const selectClass = $derived(
38
+ // 尺寸映射
39
+ const sizeClasses: Record<string, string> = {
40
+ xs: 'h-7 text-xs px-2',
41
+ sm: 'h-9 text-sm px-3',
42
+ md: 'h-10 text-sm px-3',
43
+ lg: 'h-12 text-base px-4',
44
+ }
45
+
46
+ // 选项文字尺寸
47
+ const optionTextClass = $derived(
48
+ size === 'xs'
49
+ ? 'text-xs'
50
+ : size === 'lg'
51
+ ? 'text-base'
52
+ : 'text-sm',
53
+ )
54
+
55
+ // 获取当前选中的标签
56
+ const selectedLabel = $derived(() => {
57
+ if (!options || !value)
58
+ return ''
59
+ const selected = options.find(opt => opt.value === value)
60
+ return selected ? selected.label : ''
61
+ })
62
+
63
+ // 过滤后的选项
64
+ const filteredOptions = $derived(() => {
65
+ if (!options)
66
+ return []
67
+ if (!filterable || !filterText)
68
+ return options
69
+ return options.filter(opt =>
70
+ opt.label.toLowerCase().includes(filterText.toLowerCase()),
71
+ )
72
+ })
73
+
74
+ const wrapperClass = $derived(
33
75
  cn(
34
- 'select w-full',
35
- getSizeClass(size, 'input'),
36
- error && 'select-error',
76
+ 'relative flex w-full items-center rounded-lg border bg-base-100 cursor-pointer',
77
+ sizeClasses[size] || sizeClasses.md,
78
+ error
79
+ ? 'border-error/60 focus-within:ring-2 focus-within:ring-error/15'
80
+ : isDropdownOpen
81
+ ? 'border-primary/50 ring-2 ring-primary/10'
82
+ : 'border-base-content/15 hover:border-base-content/25 focus-within:border-primary/50 focus-within:ring-2 focus-within:ring-primary/10',
83
+ 'transition-[border-color,box-shadow] duration-150',
84
+ disabled && 'opacity-50 cursor-not-allowed',
85
+ ),
86
+ )
87
+
88
+ const inputClass = $derived(
89
+ cn(
90
+ 'flex-1 min-w-0 bg-transparent outline-none border-none',
91
+ 'focus:outline-none focus:ring-0 focus:shadow-none',
92
+ 'placeholder:text-base-content/40',
93
+ error && 'text-error',
37
94
  className,
38
95
  ),
39
96
  )
40
97
 
41
- // 当 validationMessage 变化时更新自定义验证消息
98
+ // 点击外部关闭下拉框
42
99
  $effect(() => {
43
- if (selectRef) {
44
- selectRef.setCustomValidity(validationMessage)
100
+ function handleClickOutside(e: MouseEvent) {
101
+ if (dropdownRef && !dropdownRef.contains(e.target as Node)) {
102
+ isDropdownOpen = false
103
+ filterText = ''
104
+ }
105
+ }
106
+
107
+ if (isDropdownOpen) {
108
+ document.addEventListener('click', handleClickOutside)
109
+ return () => document.removeEventListener('click', handleClickOutside)
45
110
  }
46
111
  })
47
112
 
48
- function handleChange(e: Event & { currentTarget: HTMLSelectElement }) {
49
- const selectedValue = e.currentTarget.value as T
50
- value = selectedValue
51
- // 选择后重置自定义验证
52
- if (validationMessage) {
53
- e.currentTarget.setCustomValidity(validationMessage)
113
+ function handleWrapperClick() {
114
+ if (disabled)
115
+ return
116
+
117
+ if (!isDropdownOpen) {
118
+ isDropdownOpen = true
54
119
  }
55
- onchange?.(selectedValue)
120
+
121
+ if (filterable) {
122
+ // filterable 模式下点击后始终聚焦输入框,保持下拉可筛状态
123
+ setTimeout(() => inputRef?.focus(), 0)
124
+ }
125
+ }
126
+
127
+ function handleOptionSelect(optionValue: T, optionDisabled?: boolean) {
128
+ if (optionDisabled)
129
+ return
130
+ value = optionValue
131
+ isDropdownOpen = false
132
+ filterText = ''
133
+ onchange?.(optionValue)
56
134
  }
57
135
 
58
- function handleInvalid(e: Event & { currentTarget: HTMLSelectElement }) {
59
- if (validationMessage) {
60
- e.currentTarget.setCustomValidity(validationMessage)
136
+ function handleFilterInput(e: Event & { currentTarget: HTMLInputElement }) {
137
+ filterText = e.currentTarget.value
138
+ if (!isDropdownOpen) {
139
+ isDropdownOpen = true
61
140
  }
62
141
  }
142
+
143
+ function handleInputKeydown(e: KeyboardEvent) {
144
+ if (e.key === 'Escape') {
145
+ isDropdownOpen = false
146
+ filterText = ''
147
+ inputRef?.blur()
148
+ }
149
+ else if (e.key === 'Enter') {
150
+ // 回车选中第一个匹配项
151
+ const opts = filteredOptions()
152
+ if (opts.length > 0 && !opts[0].disabled) {
153
+ handleOptionSelect(opts[0].value)
154
+ }
155
+ }
156
+ else if (e.key === 'Backspace' && filterText === '' && clearable && value) {
157
+ value = undefined as T
158
+ onchange?.(undefined as T)
159
+ }
160
+ }
161
+
162
+ function handleClear(e: MouseEvent) {
163
+ e.stopPropagation()
164
+ e.preventDefault()
165
+ value = undefined as T
166
+ filterText = ''
167
+ onchange?.(undefined as T)
168
+ }
169
+
170
+ // 判断是否显示清除按钮
171
+ const showClear = $derived(clearable && value !== undefined && value !== null && value !== '' && isHovered)
63
172
  </script>
64
173
 
65
- <div class='fieldset w-full'>
66
- <select
67
- bind:this={selectRef}
68
- {id}
69
- class={selectClass}
70
- {disabled}
71
- {required}
72
- onchange={handleChange}
73
- oninvalid={handleInvalid}
174
+ <div
175
+ {...dataAttributes}
176
+ class='fieldset w-full'
177
+ bind:this={dropdownRef}
178
+ role='combobox'
179
+ aria-expanded={isDropdownOpen}
180
+ aria-haspopup='listbox'
181
+ onmouseenter={() => isHovered = true}
182
+ onmouseleave={() => isHovered = false}
183
+ >
184
+ <div
185
+ class={wrapperClass}
186
+ onclick={handleWrapperClick}
187
+ onkeydown={(e) => {
188
+ if (e.key === 'Enter' || e.key === ' ') {
189
+ e.preventDefault()
190
+ handleWrapperClick()
191
+ }
192
+ }}
193
+ role='presentation'
74
194
  >
75
- {#if placeholder}
76
- <option value="" disabled selected={!value}>{placeholder}</option>
195
+ {#if filterable}
196
+ <!-- 可筛选模式:可输入的 input -->
197
+ <input
198
+ bind:this={inputRef}
199
+ {id}
200
+ type='text'
201
+ class={inputClass}
202
+ {disabled}
203
+ {required}
204
+ {placeholder}
205
+ value={isDropdownOpen ? filterText : selectedLabel()}
206
+ oninput={handleFilterInput}
207
+ onkeydown={handleInputKeydown}
208
+ onfocus={() => {
209
+ if (!disabled && !isDropdownOpen)
210
+ isDropdownOpen = true
211
+ }}
212
+ role='combobox'
213
+ aria-autocomplete='list'
214
+ aria-controls='{id}-listbox'
215
+ aria-expanded={isDropdownOpen}
216
+ />
217
+ {:else}
218
+ <!-- 普通模式:只读 input 显示选中值 -->
219
+ <input
220
+ {id}
221
+ type='text'
222
+ class={inputClass}
223
+ readonly
224
+ {disabled}
225
+ {required}
226
+ {placeholder}
227
+ value={selectedLabel()}
228
+ tabindex='-1'
229
+ />
77
230
  {/if}
78
- {#if children}
79
- {@render children()}
80
- {:else if options}
81
- {#each options as option (String(option.value))}
82
- <option
83
- value={option.value as string}
84
- disabled={option.disabled}
85
- selected={value === option.value}
231
+
232
+ <!-- 右侧图标区域 -->
233
+ <div class='flex items-center pr-2 shrink-0'>
234
+ {#if showClear}
235
+ <button
236
+ type='button'
237
+ class='flex items-center justify-center w-5 h-5 rounded-full hover:bg-base-content/10 transition-colors'
238
+ onclick={handleClear}
239
+ tabindex='-1'
240
+ aria-label='清空选择'
241
+ >
242
+ <svg xmlns='http://www.w3.org/2000/svg' class='h-3.5 w-3.5' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
243
+ <path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M6 18L18 6M6 6l12 12' />
244
+ </svg>
245
+ </button>
246
+ {:else}
247
+ <svg
248
+ class={cn('h-4 w-4 text-base-content/40 transition-transform duration-200', isDropdownOpen && 'rotate-180')}
249
+ xmlns='http://www.w3.org/2000/svg'
250
+ viewBox='0 0 20 20'
251
+ fill='currentColor'
86
252
  >
87
- {option.label}
88
- </option>
89
- {/each}
253
+ <path fill-rule='evenodd' d='M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z' clip-rule='evenodd' />
254
+ </svg>
255
+ {/if}
256
+ </div>
257
+
258
+ <!-- 下拉选项列表 -->
259
+ {#if isDropdownOpen}
260
+ <ul
261
+ id='{id}-listbox'
262
+ class='absolute left-0 z-50 mt-2 w-full max-h-60 overflow-auto rounded-xl border border-base-content/10 bg-base-100 p-1.5 shadow-xl shadow-base-content/10'
263
+ role='listbox'
264
+ style='top: 100%;'
265
+ >
266
+ {#each filteredOptions() as option (String(option.value))}
267
+ <li
268
+ class={cn(
269
+ 'flex cursor-pointer items-center rounded-lg px-3 py-2.5 transition-all duration-150',
270
+ optionTextClass,
271
+ option.disabled ? 'opacity-40 cursor-not-allowed' : 'hover:bg-base-content/5 active:bg-base-content/8',
272
+ value === option.value && 'bg-primary/10 text-primary font-semibold',
273
+ )}
274
+ role='option'
275
+ aria-selected={value === option.value}
276
+ aria-disabled={option.disabled}
277
+ onclick={(e) => {
278
+ e.stopPropagation()
279
+ handleOptionSelect(option.value, option.disabled)
280
+ }}
281
+ onkeydown={(e) => {
282
+ if (e.key === 'Enter' || e.key === ' ') {
283
+ e.preventDefault()
284
+ handleOptionSelect(option.value, option.disabled)
285
+ }
286
+ }}
287
+ tabindex={option.disabled ? -1 : 0}
288
+ >
289
+ {option.label}
290
+ </li>
291
+ {:else}
292
+ <li class={cn('px-3 py-3 text-base-content/40 text-center', optionTextClass)}>
293
+ 无匹配选项
294
+ </li>
295
+ {/each}
296
+ </ul>
90
297
  {/if}
91
- </select>
298
+ </div>
92
299
  {#if error}
93
300
  <span class='fieldset-label text-error'>{error}</span>
94
301
  {/if}
@@ -1,6 +1,6 @@
1
- import type { SelectProps } from '../../types.js';
1
+ import type { DataAttributes, SelectProps } from '../../types.js';
2
2
  declare function $$render<T = string>(): {
3
- props: SelectProps<T>;
3
+ props: SelectProps<T> & DataAttributes;
4
4
  exports: {};
5
5
  bindings: "value";
6
6
  slots: {};
@@ -8,15 +8,17 @@
8
8
  =============================================================================
9
9
  -->
10
10
  <script lang='ts'>
11
- import type { SpinnerProps } from '../../types.js'
12
- import { cn } from '../../utils.js'
11
+ import type { DataAttributes, SpinnerProps } from '../../types.js'
12
+ import { cn, getDataAttributes } from '../../utils.js'
13
13
 
14
14
  const {
15
15
  size = 'md',
16
16
  variant = 'primary',
17
17
  class: className = '',
18
- }: SpinnerProps = $props()
18
+ ...restProps
19
+ }: SpinnerProps & DataAttributes = $props()
19
20
 
21
+ const dataAttributes = $derived(getDataAttributes(restProps))
20
22
  const sizeMap = {
21
23
  'xs': 'loading-xs',
22
24
  'sm': 'loading-sm',
@@ -51,4 +53,4 @@
51
53
  )
52
54
  </script>
53
55
 
54
- <span class={spinnerClass}></span>
56
+ <span {...dataAttributes} class={spinnerClass}></span>
@@ -1,4 +1,5 @@
1
- import type { SpinnerProps } from '../../types.js';
2
- declare const Spinner: import("svelte").Component<SpinnerProps, {}, "">;
1
+ import type { DataAttributes, SpinnerProps } from '../../types.js';
2
+ type $$ComponentProps = SpinnerProps & DataAttributes;
3
+ declare const Spinner: import("svelte").Component<$$ComponentProps, {}, "">;
3
4
  type Spinner = ReturnType<typeof Spinner>;
4
5
  export default Spinner;
@@ -8,8 +8,8 @@
8
8
  =============================================================================
9
9
  -->
10
10
  <script lang='ts'>
11
- import type { SwitchProps } from '../../types.js'
12
- import { cn, generateId, getSizeClass } from '../../utils.js'
11
+ import type { DataAttributes, SwitchProps } from '../../types.js'
12
+ import { cn, generateId, getDataAttributes, getSizeClass } from '../../utils.js'
13
13
 
14
14
  let {
15
15
  checked = $bindable(false),
@@ -18,8 +18,10 @@
18
18
  disabled = false,
19
19
  class: className = '',
20
20
  onchange,
21
- }: SwitchProps = $props()
21
+ ...restProps
22
+ }: SwitchProps & DataAttributes = $props()
22
23
 
24
+ const dataAttributes = $derived(getDataAttributes(restProps))
23
25
  const id = generateId('switch')
24
26
 
25
27
  const hasToggleColorVariant = $derived(
@@ -41,7 +43,7 @@
41
43
  }
42
44
  </script>
43
45
 
44
- <div class='inline-flex'>
46
+ <div {...dataAttributes} class='inline-flex'>
45
47
  <label class='label cursor-pointer gap-2' for={id}>
46
48
  <input
47
49
  {id}
@@ -1,4 +1,5 @@
1
- import type { SwitchProps } from '../../types.js';
2
- declare const Switch: import("svelte").Component<SwitchProps, {}, "checked">;
1
+ import type { DataAttributes, SwitchProps } from '../../types.js';
2
+ type $$ComponentProps = SwitchProps & DataAttributes;
3
+ declare const Switch: import("svelte").Component<$$ComponentProps, {}, "checked">;
3
4
  type Switch = ReturnType<typeof Switch>;
4
5
  export default Switch;
@@ -8,8 +8,8 @@
8
8
  =============================================================================
9
9
  -->
10
10
  <script lang='ts'>
11
- import type { TagProps } from '../../types.js'
12
- import { cn, getSizeClass, getVariantClass } from '../../utils.js'
11
+ import type { DataAttributes, TagProps } from '../../types.js'
12
+ import { cn, getDataAttributes, getSizeClass, getVariantClass } from '../../utils.js'
13
13
  import IconButton from './IconButton.svelte'
14
14
 
15
15
  const {
@@ -23,8 +23,10 @@
23
23
  class: className = '',
24
24
  onclose,
25
25
  children,
26
- }: TagProps = $props()
26
+ ...restProps
27
+ }: TagProps & DataAttributes = $props()
27
28
 
29
+ const dataAttributes = $derived(getDataAttributes(restProps))
28
30
  const tagClass = $derived(
29
31
  cn(
30
32
  'badge gap-1',
@@ -41,7 +43,7 @@
41
43
  }
42
44
  </script>
43
45
 
44
- <span class={tagClass}>
46
+ <span {...dataAttributes} class={tagClass}>
45
47
  {#if children}
46
48
  {@render children()}
47
49
  {:else}
@@ -1,4 +1,5 @@
1
- import type { TagProps } from '../../types.js';
2
- declare const Tag: import("svelte").Component<TagProps, {}, "">;
1
+ import type { DataAttributes, TagProps } from '../../types.js';
2
+ type $$ComponentProps = TagProps & DataAttributes;
3
+ declare const Tag: import("svelte").Component<$$ComponentProps, {}, "">;
3
4
  type Tag = ReturnType<typeof Tag>;
4
5
  export default Tag;
@@ -9,8 +9,8 @@
9
9
  =============================================================================
10
10
  -->
11
11
  <script lang='ts'>
12
- import type { TextareaProps } from '../../types.js'
13
- import { cn, getInputSizeClass } from '../../utils.js'
12
+ import type { DataAttributes, TextareaProps } from '../../types.js'
13
+ import { cn, getDataAttributes, getInputSizeClass } from '../../utils.js'
14
14
 
15
15
  let {
16
16
  value = $bindable(''),
@@ -27,13 +27,26 @@
27
27
  id,
28
28
  name,
29
29
  oninput,
30
- }: TextareaProps = $props()
30
+ ...restProps
31
+ }: TextareaProps & DataAttributes = $props()
31
32
 
33
+ const dataAttributes = $derived(getDataAttributes(restProps))
32
34
  let textareaRef: HTMLTextAreaElement | undefined = $state()
33
35
 
36
+ const wrapperClass = $derived(
37
+ cn(
38
+ 'flex w-full rounded-lg border bg-base-100',
39
+ error
40
+ ? 'border-error/60 focus-within:ring-2 focus-within:ring-error/15'
41
+ : 'border-base-content/15 focus-within:border-primary/50 focus-within:ring-2 focus-within:ring-primary/10',
42
+ 'transition-[border-color,box-shadow] duration-150',
43
+ disabled && 'opacity-50 cursor-not-allowed',
44
+ ),
45
+ )
46
+
34
47
  const textareaClass = $derived(
35
48
  cn(
36
- 'textarea w-full',
49
+ 'textarea w-full bg-transparent border-none outline-none',
37
50
  getInputSizeClass(size),
38
51
  error && 'textarea-error',
39
52
  className,
@@ -71,21 +84,23 @@
71
84
  }
72
85
  </script>
73
86
 
74
- <div class='fieldset w-full'>
75
- <textarea
76
- bind:this={textareaRef}
77
- {id}
78
- {name}
79
- {placeholder}
80
- {rows}
81
- {disabled}
82
- {readonly}
83
- {required}
84
- class={textareaClass}
85
- bind:value
86
- oninput={handleInput}
87
- oninvalid={handleInvalid}
88
- ></textarea>
87
+ <div {...dataAttributes} class='fieldset w-full'>
88
+ <div class={wrapperClass}>
89
+ <textarea
90
+ bind:this={textareaRef}
91
+ {id}
92
+ {name}
93
+ {placeholder}
94
+ {rows}
95
+ {disabled}
96
+ {readonly}
97
+ {required}
98
+ class={textareaClass}
99
+ bind:value
100
+ oninput={handleInput}
101
+ oninvalid={handleInvalid}
102
+ ></textarea>
103
+ </div>
89
104
  {#if error}
90
105
  <span class='fieldset-label text-error'>{error}</span>
91
106
  {/if}
@@ -1,4 +1,5 @@
1
- import type { TextareaProps } from '../../types.js';
2
- declare const Textarea: import("svelte").Component<TextareaProps, {}, "value">;
1
+ import type { DataAttributes, TextareaProps } from '../../types.js';
2
+ type $$ComponentProps = TextareaProps & DataAttributes;
3
+ declare const Textarea: import("svelte").Component<$$ComponentProps, {}, "value">;
3
4
  type Textarea = ReturnType<typeof Textarea>;
4
5
  export default Textarea;