@placeholderco/placeholder-ui 1.0.12 → 2.0.0

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 (276) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +134 -130
  3. package/dist/app.css +178 -0
  4. package/dist/bootstrap.min.css +3600 -0
  5. package/dist/cards/GitCard.svelte +115 -0
  6. package/dist/cards/GitCard.svelte.d.ts +22 -0
  7. package/dist/display/Avatar.svelte +29 -17
  8. package/dist/display/Avatar.svelte.d.ts +12 -2
  9. package/dist/display/LinkCollection.svelte +40 -18
  10. package/dist/display/LinkCollection.svelte.d.ts +9 -2
  11. package/dist/display/Paper.svelte +31 -19
  12. package/dist/display/Paper.svelte.d.ts +12 -3
  13. package/dist/display/alert/Alert.svelte +129 -0
  14. package/dist/display/alert/Alert.svelte.d.ts +15 -0
  15. package/dist/display/alert/AlertVariant.d.ts +1 -0
  16. package/dist/display/alert/AlertVariant.js +1 -0
  17. package/dist/display/filetree/FileTree.svelte +259 -0
  18. package/dist/display/filetree/FileTree.svelte.d.ts +76 -0
  19. package/dist/display/filetree/FileTreeItem.svelte +396 -0
  20. package/dist/display/filetree/FileTreeItem.svelte.d.ts +40 -0
  21. package/dist/display/filetree/FileTreeNode.d.ts +46 -0
  22. package/dist/display/filetree/FileTreeNode.js +115 -0
  23. package/dist/display/filetree/index.d.ts +5 -0
  24. package/dist/display/filetree/index.js +3 -0
  25. package/dist/editors/CodeMirror.svelte +306 -0
  26. package/dist/editors/CodeMirror.svelte.d.ts +31 -0
  27. package/dist/editors/tiptap/LinkActionMenu.svelte +307 -0
  28. package/dist/editors/tiptap/LinkActionMenu.svelte.d.ts +8 -0
  29. package/dist/editors/tiptap/TipTap.svelte +856 -0
  30. package/dist/editors/tiptap/TipTap.svelte.d.ts +54 -0
  31. package/dist/editors/tiptap/TipTapBubble.svelte +790 -0
  32. package/dist/editors/tiptap/TipTapBubble.svelte.d.ts +44 -0
  33. package/dist/editors/tiptap/collaboration.d.ts +55 -0
  34. package/dist/editors/tiptap/collaboration.js +1 -0
  35. package/dist/editors/tiptap/extensions/Alert.d.ts +18 -0
  36. package/dist/editors/tiptap/extensions/Alert.js +72 -0
  37. package/dist/editors/tiptap/extensions/CodeBlockEditor.d.ts +21 -0
  38. package/dist/editors/tiptap/extensions/CodeBlockEditor.js +326 -0
  39. package/dist/editors/tiptap/extensions/Div.d.ts +19 -0
  40. package/dist/editors/tiptap/extensions/Div.js +89 -0
  41. package/dist/editors/tiptap/extensions/Span.d.ts +5 -0
  42. package/dist/editors/tiptap/extensions/Span.js +56 -0
  43. package/dist/editors/tiptap/extensions/codeBlockEditor.css +236 -0
  44. package/dist/editors/tiptap/extensions/codeBlockLanguages.d.ts +17 -0
  45. package/dist/editors/tiptap/extensions/codeBlockLanguages.js +124 -0
  46. package/dist/editors/tiptap/extensions/index.d.ts +7 -0
  47. package/dist/editors/tiptap/extensions/index.js +5 -0
  48. package/dist/editors/tiptap/toolbar/AlertButton.svelte +261 -0
  49. package/dist/editors/tiptap/toolbar/AlertButton.svelte.d.ts +7 -0
  50. package/dist/editors/tiptap/toolbar/AlignCenterButton.svelte +18 -0
  51. package/dist/editors/tiptap/toolbar/AlignCenterButton.svelte.d.ts +7 -0
  52. package/dist/editors/tiptap/toolbar/AlignJustifyButton.svelte +18 -0
  53. package/dist/editors/tiptap/toolbar/AlignJustifyButton.svelte.d.ts +7 -0
  54. package/dist/editors/tiptap/toolbar/AlignLeftButton.svelte +18 -0
  55. package/dist/editors/tiptap/toolbar/AlignLeftButton.svelte.d.ts +7 -0
  56. package/dist/editors/tiptap/toolbar/AlignRightButton.svelte +18 -0
  57. package/dist/editors/tiptap/toolbar/AlignRightButton.svelte.d.ts +7 -0
  58. package/dist/editors/tiptap/toolbar/BoldButton.svelte +18 -0
  59. package/dist/editors/tiptap/toolbar/BoldButton.svelte.d.ts +7 -0
  60. package/dist/editors/tiptap/toolbar/BulletListButton.svelte +18 -0
  61. package/dist/editors/tiptap/toolbar/BulletListButton.svelte.d.ts +7 -0
  62. package/dist/editors/tiptap/toolbar/CodeBlockButton.svelte +18 -0
  63. package/dist/editors/tiptap/toolbar/CodeBlockButton.svelte.d.ts +7 -0
  64. package/dist/editors/tiptap/toolbar/CodeButton.svelte +18 -0
  65. package/dist/editors/tiptap/toolbar/CodeButton.svelte.d.ts +7 -0
  66. package/dist/editors/tiptap/toolbar/ColorButton.svelte +299 -0
  67. package/dist/editors/tiptap/toolbar/ColorButton.svelte.d.ts +20 -0
  68. package/dist/editors/tiptap/toolbar/DocumentButton.svelte +58 -0
  69. package/dist/editors/tiptap/toolbar/DocumentButton.svelte.d.ts +9 -0
  70. package/dist/editors/tiptap/toolbar/HeadingButton.svelte +30 -0
  71. package/dist/editors/tiptap/toolbar/HeadingButton.svelte.d.ts +8 -0
  72. package/dist/editors/tiptap/toolbar/HtmlButton.svelte +68 -0
  73. package/dist/editors/tiptap/toolbar/HtmlButton.svelte.d.ts +7 -0
  74. package/dist/editors/tiptap/toolbar/ImageButton.svelte +54 -0
  75. package/dist/editors/tiptap/toolbar/ImageButton.svelte.d.ts +8 -0
  76. package/dist/editors/tiptap/toolbar/ItalicButton.svelte +18 -0
  77. package/dist/editors/tiptap/toolbar/ItalicButton.svelte.d.ts +7 -0
  78. package/dist/editors/tiptap/toolbar/LinkButton.svelte +257 -0
  79. package/dist/editors/tiptap/toolbar/LinkButton.svelte.d.ts +7 -0
  80. package/dist/editors/tiptap/toolbar/OrderedListButton.svelte +18 -0
  81. package/dist/editors/tiptap/toolbar/OrderedListButton.svelte.d.ts +7 -0
  82. package/dist/editors/tiptap/toolbar/RedoButton.svelte +18 -0
  83. package/dist/editors/tiptap/toolbar/RedoButton.svelte.d.ts +7 -0
  84. package/dist/editors/tiptap/toolbar/StrikethroughButton.svelte +18 -0
  85. package/dist/editors/tiptap/toolbar/StrikethroughButton.svelte.d.ts +7 -0
  86. package/dist/editors/tiptap/toolbar/SubscriptButton.svelte +18 -0
  87. package/dist/editors/tiptap/toolbar/SubscriptButton.svelte.d.ts +7 -0
  88. package/dist/editors/tiptap/toolbar/SuperscriptButton.svelte +18 -0
  89. package/dist/editors/tiptap/toolbar/SuperscriptButton.svelte.d.ts +7 -0
  90. package/dist/editors/tiptap/toolbar/TableButton.svelte +258 -0
  91. package/dist/editors/tiptap/toolbar/TableButton.svelte.d.ts +7 -0
  92. package/dist/editors/tiptap/toolbar/ToolbarButton.svelte +84 -0
  93. package/dist/editors/tiptap/toolbar/ToolbarButton.svelte.d.ts +10 -0
  94. package/dist/editors/tiptap/toolbar/UnderlineButton.svelte +18 -0
  95. package/dist/editors/tiptap/toolbar/UnderlineButton.svelte.d.ts +7 -0
  96. package/dist/editors/tiptap/toolbar/UndoButton.svelte +18 -0
  97. package/dist/editors/tiptap/toolbar/UndoButton.svelte.d.ts +7 -0
  98. package/dist/form/Autocomplete.svelte +86 -135
  99. package/dist/form/Autocomplete.svelte.d.ts +6 -8
  100. package/dist/form/AutocompleteMulti.svelte +385 -227
  101. package/dist/form/AutocompleteMulti.svelte.d.ts +59 -10
  102. package/dist/form/Checkbox.svelte +59 -54
  103. package/dist/form/Checkbox.svelte.d.ts +14 -5
  104. package/dist/form/Chips.svelte +29 -13
  105. package/dist/form/Chips.svelte.d.ts +16 -2
  106. package/dist/form/ComboBox.svelte +30 -20
  107. package/dist/form/ComboBox.svelte.d.ts +1 -1
  108. package/dist/form/ComboBoxItemBuilder.svelte +161 -188
  109. package/dist/form/ComboBoxItemBuilder.svelte.d.ts +19 -2
  110. package/dist/form/ComboBoxMulti.svelte +38 -24
  111. package/dist/form/ComboBoxMulti.svelte.d.ts +1 -1
  112. package/dist/form/CronBuilder.svelte +84 -67
  113. package/dist/form/CronBuilder.svelte.d.ts +16 -3
  114. package/dist/form/DatePicker.svelte +148 -195
  115. package/dist/form/DateRangePicker.svelte +101 -98
  116. package/dist/form/DateTimePicker.svelte +167 -208
  117. package/dist/form/DateTimePicker.svelte.d.ts +10 -2
  118. package/dist/form/FormGroup.svelte +33 -10
  119. package/dist/form/FormGroup.svelte.d.ts +12 -2
  120. package/dist/form/Number.svelte +70 -47
  121. package/dist/form/Number.svelte.d.ts +36 -6
  122. package/dist/form/Radio.svelte +18 -18
  123. package/dist/form/RadioGroup.svelte +23 -12
  124. package/dist/form/RadioGroup.svelte.d.ts +13 -2
  125. package/dist/form/SegmentedControl.svelte +76 -56
  126. package/dist/form/SegmentedControl.svelte.d.ts +9 -4
  127. package/dist/form/Select.svelte +150 -233
  128. package/dist/form/Select.svelte.d.ts +29 -5
  129. package/dist/form/SelectMulti.svelte +163 -277
  130. package/dist/form/SelectMulti.svelte.d.ts +36 -7
  131. package/dist/form/Slider.svelte +183 -269
  132. package/dist/form/Slider.svelte.d.ts +32 -17
  133. package/dist/form/StringArrayBuilder.svelte +249 -0
  134. package/dist/form/StringArrayBuilder.svelte.d.ts +15 -0
  135. package/dist/form/Switch.svelte +164 -106
  136. package/dist/form/Switch.svelte.d.ts +22 -3
  137. package/dist/form/TextArea.svelte +49 -29
  138. package/dist/form/TextArea.svelte.d.ts +23 -3
  139. package/dist/form/Textbox.svelte +69 -68
  140. package/dist/form/Textbox.svelte.d.ts +32 -4
  141. package/dist/form/TimePicker.svelte +42 -47
  142. package/dist/form/TimePicker.svelte.d.ts +12 -2
  143. package/dist/form/comboBoxCore.svelte.d.ts +99 -0
  144. package/dist/form/comboBoxCore.svelte.js +243 -0
  145. package/dist/icon/Icon.svelte +8 -3
  146. package/dist/icon/Icon.svelte.d.ts +8 -3
  147. package/dist/icon/file-type-bmp.svg +1 -0
  148. package/dist/icon/file-type-csv.svg +1 -0
  149. package/dist/icon/file-type-doc.svg +1 -0
  150. package/dist/icon/file-type-docx.svg +1 -0
  151. package/dist/icon/file-type-jpg.svg +1 -0
  152. package/dist/icon/file-type-pdf.svg +1 -0
  153. package/dist/icon/file-type-png.svg +1 -0
  154. package/dist/icon/file-type-ppt.svg +1 -0
  155. package/dist/icon/file-type-svg.svg +1 -0
  156. package/dist/icon/file-type-txt.svg +1 -0
  157. package/dist/icon/file-type-xls.svg +1 -0
  158. package/dist/icon/file-type-zip.svg +1 -0
  159. package/dist/icon/index.d.ts +166 -151
  160. package/dist/icon/index.js +166 -152
  161. package/dist/icon/logo-placeholder.svg +6 -0
  162. package/dist/icon/source-code.svg +1 -0
  163. package/dist/icon/sun-moon.svg +1 -0
  164. package/dist/icon/table.svg +6 -0
  165. package/dist/index.d.ts +87 -26
  166. package/dist/index.js +25 -28
  167. package/dist/layout/AppShell.svelte +12 -162
  168. package/dist/layout/AppShell.svelte.d.ts +6 -27
  169. package/dist/layout/CustomNavbar.svelte +9 -7
  170. package/dist/layout/CustomNavbar.svelte.d.ts +4 -2
  171. package/dist/layout/Navbar.svelte +161 -129
  172. package/dist/layout/Navbar.svelte.d.ts +29 -15
  173. package/dist/layout/NavbarItemDisplay.svelte +2 -2
  174. package/dist/layout/NavbarItemView.svelte +182 -0
  175. package/dist/layout/NavbarItemView.svelte.d.ts +10 -0
  176. package/dist/layout/Sidenav.svelte +280 -341
  177. package/dist/layout/Sidenav.svelte.d.ts +36 -4
  178. package/dist/models/ComboBoxItem.d.ts +2 -0
  179. package/dist/models/Employee.d.ts +53 -0
  180. package/dist/models/Employee.js +23 -0
  181. package/dist/models/Hyperlink.d.ts +3 -1
  182. package/dist/models/LinkCollectionItem.d.ts +3 -1
  183. package/dist/models/NavbarItem.d.ts +6 -2
  184. package/dist/theme.svelte.d.ts +0 -1
  185. package/dist/theme.svelte.js +46 -58
  186. package/dist/ui/Accordion.svelte +81 -242
  187. package/dist/ui/Accordion.svelte.d.ts +37 -14
  188. package/dist/ui/AccordionItem.svelte +303 -0
  189. package/dist/ui/AccordionItem.svelte.d.ts +21 -0
  190. package/dist/ui/ActionIcon.svelte +25 -21
  191. package/dist/ui/ActionIcon.svelte.d.ts +15 -2
  192. package/dist/ui/Badge.svelte +163 -171
  193. package/dist/ui/Badge.svelte.d.ts +13 -5
  194. package/dist/ui/Button.svelte +178 -205
  195. package/dist/ui/Button.svelte.d.ts +19 -2
  196. package/dist/ui/ButtonVariant.d.ts +15 -1
  197. package/dist/ui/ContextMenu.svelte +146 -0
  198. package/dist/ui/ContextMenu.svelte.d.ts +37 -0
  199. package/dist/ui/ContextMenuList.svelte +188 -0
  200. package/dist/ui/ContextMenuList.svelte.d.ts +12 -0
  201. package/dist/ui/Dialog.svelte +183 -111
  202. package/dist/ui/Dialog.svelte.d.ts +21 -4
  203. package/dist/ui/Drawer.svelte +237 -184
  204. package/dist/ui/Drawer.svelte.d.ts +20 -2
  205. package/dist/ui/Dropdown.svelte +24 -14
  206. package/dist/ui/Dropdown.svelte.d.ts +11 -2
  207. package/dist/ui/Dropzone.svelte +159 -57
  208. package/dist/ui/Dropzone.svelte.d.ts +29 -2
  209. package/dist/ui/Link.svelte +12 -4
  210. package/dist/ui/Link.svelte.d.ts +8 -2
  211. package/dist/ui/Loader.svelte +23 -18
  212. package/dist/ui/Loader.svelte.d.ts +5 -2
  213. package/dist/ui/Logo.svelte +37 -0
  214. package/dist/ui/Logo.svelte.d.ts +16 -0
  215. package/dist/ui/MultiSortable.svelte +287 -0
  216. package/dist/ui/MultiSortable.svelte.d.ts +50 -0
  217. package/dist/ui/Pagination.svelte +15 -9
  218. package/dist/ui/Pagination.svelte.d.ts +6 -2
  219. package/dist/ui/Popover.svelte +43 -65
  220. package/dist/ui/Popover.svelte.d.ts +14 -5
  221. package/dist/ui/ProgressBar.svelte +28 -30
  222. package/dist/ui/ProgressBarVariant.d.ts +7 -1
  223. package/dist/ui/SegmentedControl.svelte +0 -0
  224. package/dist/ui/SegmentedControl.svelte.d.ts +26 -0
  225. package/dist/ui/Sortable.svelte +163 -0
  226. package/dist/ui/Sortable.svelte.d.ts +47 -0
  227. package/dist/ui/Table.svelte +383 -80
  228. package/dist/ui/Table.svelte.d.ts +43 -2
  229. package/dist/ui/Tabs.svelte +22 -18
  230. package/dist/ui/Tabs.svelte.d.ts +6 -2
  231. package/dist/ui/ThemeSwitcher.svelte +26 -7
  232. package/dist/ui/ThemeSwitcher.svelte.d.ts +4 -2
  233. package/dist/ui/Tooltip.svelte +312 -180
  234. package/dist/ui/Tooltip.svelte.d.ts +4 -3
  235. package/dist/uiTheme.svelte.d.ts +86 -0
  236. package/dist/uiTheme.svelte.js +214 -0
  237. package/dist/util/DateFunctions.js +1 -1
  238. package/dist/util/DragDrop.d.ts +50 -0
  239. package/dist/util/DragDrop.js +154 -0
  240. package/dist/util/NavigateTo.js +2 -2
  241. package/dist/util/dayjs.d.ts +4 -0
  242. package/dist/util/dayjs.js +15 -0
  243. package/package.json +66 -26
  244. package/dist/display/Alert.svelte +0 -179
  245. package/dist/display/Alert.svelte.d.ts +0 -13
  246. package/dist/form/FileInput.svelte +0 -235
  247. package/dist/form/FileInput.svelte.d.ts +0 -23
  248. package/dist/form/PasswordInput.svelte +0 -253
  249. package/dist/form/PasswordInput.svelte.d.ts +0 -34
  250. package/dist/form/Rating.svelte +0 -235
  251. package/dist/form/Rating.svelte.d.ts +0 -21
  252. package/dist/styles/components.css +0 -199
  253. package/dist/styles/dark.css +0 -146
  254. package/dist/styles/index.css +0 -116
  255. package/dist/styles/reset.css +0 -110
  256. package/dist/styles/semantic.css +0 -86
  257. package/dist/styles/tokens.css +0 -203
  258. package/dist/styles/utilities.css +0 -523
  259. package/dist/ui/Breadcrumbs.svelte +0 -131
  260. package/dist/ui/Breadcrumbs.svelte.d.ts +0 -16
  261. package/dist/ui/LoadingOverlay.svelte +0 -53
  262. package/dist/ui/LoadingOverlay.svelte.d.ts +0 -11
  263. package/dist/ui/Progress.svelte +0 -191
  264. package/dist/ui/Progress.svelte.d.ts +0 -15
  265. package/dist/ui/RingProgress.svelte +0 -141
  266. package/dist/ui/RingProgress.svelte.d.ts +0 -18
  267. package/dist/ui/Skeleton.svelte +0 -85
  268. package/dist/ui/Skeleton.svelte.d.ts +0 -12
  269. package/dist/ui/Stepper.svelte +0 -355
  270. package/dist/ui/Stepper.svelte.d.ts +0 -24
  271. package/dist/ui/Timeline.svelte +0 -225
  272. package/dist/ui/Timeline.svelte.d.ts +0 -23
  273. /package/dist/{actions → util}/ClickOutside.d.ts +0 -0
  274. /package/dist/{actions → util}/ClickOutside.js +0 -0
  275. /package/dist/{transitions → util}/Transitions.d.ts +0 -0
  276. /package/dist/{transitions → util}/Transitions.js +0 -0
@@ -1,240 +1,331 @@
1
- <script lang="ts">
2
- import { clickOutside } from "../actions/ClickOutside.js";
3
- import type { NotifyModel } from "../models/NotifyModel.js";
1
+ <script module lang="ts">
4
2
  import type {
3
+ ComboBoxGroup,
5
4
  ComboBoxItem,
6
5
  FetchFunctionType,
7
- SearchFunctionType,
8
- } from "../models/ComboBoxItem.js";
9
- import ComboBoxMulti from "./ComboBoxMulti.svelte";
10
- import FormGroup from "./FormGroup.svelte";
11
- import Badge from "../ui/Badge.svelte";
12
- import { onMount, tick, type Snippet } from "svelte";
13
-
14
- interface Props {
6
+ RetrieveLabelFunctionType,
7
+ SearchFunctionType
8
+ } from '../models/ComboBoxItem.js';
9
+ import type { Snippet } from 'svelte';
10
+
11
+ export interface AutocompleteMultiProps {
12
+ /** HTML name attribute for the input */
13
+ name?: string;
14
+ /** Label text displayed above the input */
15
15
  label?: string;
16
+ /** Array of selected values (bindable) */
16
17
  values?: string[];
18
+ /**
19
+ * Array of full selected item objects (bindable). Can be passed initially to preset
20
+ * the selection with known labels — no `values` or retrieveLabelFunction needed.
21
+ */
17
22
  rawValues?: ComboBoxItem[];
23
+ /** Placeholder text when nothing is selected */
18
24
  placeholder?: string;
25
+ /** Mark field as required (shows asterisk) */
26
+ required?: boolean;
27
+ /** CSS classes for the control element */
19
28
  class?: string;
29
+ /** CSS classes for the container element */
30
+ containerClass?: string;
31
+ /** CSS classes for the FormGroup wrapper */
32
+ groupClass?: string;
33
+ /** Disable the input */
20
34
  disabled?: boolean;
21
- required?: boolean;
35
+ /** Show error state styling */
22
36
  showError?: boolean;
37
+ /** Error message to display */
23
38
  errorText?: string;
39
+ /** Auto-focus the input on mount */
24
40
  autofocus?: boolean;
25
- showNoResultsMessage?: boolean;
41
+ /** Auto-focus on every dialog open */
42
+ autofocusDialog?: boolean;
43
+ /** Hide "no results" message when search finds nothing */
44
+ hideNoResults?: boolean;
45
+ /** Close the dropdown after selecting an option */
46
+ closeOnSelection?: boolean;
47
+ /**
48
+ * Offer to add the typed text as a new chip when it doesn't match an existing option.
49
+ * On by default — set to `false` to restrict selection to the provided options.
50
+ */
51
+ allowAdd?: boolean;
52
+ /** Builds the label shown for the "add new value" suggestion (default: `Add "<value>"`) */
53
+ addItemLabel?: (value: string) => string;
54
+ /** Text shown while loading options */
26
55
  loadingText?: string;
56
+ /** SVG icon displayed on the left side */
57
+ leftIconSvg?: string;
58
+ /** Static list of autocomplete options */
27
59
  options?: ComboBoxItem[];
28
- fetchFunction?: FetchFunctionType;
29
- searchFunction?: SearchFunctionType;
30
- tooltipLocation?: "top" | "bottom" | "left" | "right";
60
+ /** Options organized into groups */
61
+ groupedOptions?: ComboBoxGroup[];
62
+ /** Position of the tooltip */
63
+ tooltipLocation?: 'top' | 'bottom' | 'left' | 'right';
64
+ /** Async function to fetch all options */
65
+ fetchFunction?: FetchFunctionType | null;
66
+ /** Async function to search/filter options based on input */
67
+ searchFunction?: SearchFunctionType | null;
68
+ /** Async function to retrieve labels for values */
69
+ retrieveLabelFunction?: RetrieveLabelFunctionType | null;
70
+ /** Rich tooltip content using a Svelte snippet */
31
71
  tooltipContent?: Snippet;
72
+ /** Simple tooltip text */
32
73
  tooltipText?: string;
74
+ /** Callback when Enter is pressed with the current filter text */
75
+ enterPressed?: (e: string) => void;
76
+ /** Callback when selection changes (returns value strings) */
33
77
  onchange?: (e: string[]) => void;
78
+ /** Callback when selection changes (returns full item objects) */
34
79
  onchangeRaw?: (e: ComboBoxItem[]) => void;
35
- onCreate?: (value: string) => void;
80
+ /** Callback when the user chooses to add the typed value as a new item */
81
+ onItemAdded?: (value: string) => void;
36
82
  }
83
+ </script>
84
+
85
+ <script lang="ts">
86
+ import ComboBoxMulti from './ComboBoxMulti.svelte';
87
+ import FormGroup from './FormGroup.svelte';
88
+ import { clickOutside } from '../util/ClickOutside.js';
89
+ import { ComboBoxMultiCore } from './comboBoxCore.svelte.js';
90
+ import Icon from '../icon/Icon.svelte';
91
+ import Loader from '../ui/Loader.svelte';
92
+ import Badge from '../ui/Badge.svelte';
93
+ import ActionIcon from '../ui/ActionIcon.svelte';
94
+ import { iconX } from '../icon/index.js';
95
+ import { tick } from 'svelte';
96
+ import { useDialogEvents } from '../ui/DialogEvents.svelte.js';
97
+
98
+ const autoName = `autocompletemulti-${crypto.randomUUID()}`;
37
99
 
38
100
  let {
39
- label = "",
101
+ name,
102
+ label = '',
40
103
  values = $bindable([]),
41
104
  rawValues = $bindable([]),
42
- placeholder = "Start typing to search...",
43
- class: classes = "",
44
- disabled = false,
105
+ placeholder = 'Start typing to search...',
45
106
  required = false,
107
+ class: classes = '',
108
+ containerClass = '',
109
+ groupClass = '',
110
+ disabled = false,
46
111
  showError = false,
47
- errorText = "",
112
+ errorText = '',
48
113
  autofocus = false,
49
- showNoResultsMessage = false,
50
- loadingText = "",
114
+ autofocusDialog = false,
115
+ hideNoResults = false,
116
+ closeOnSelection = false,
117
+ allowAdd = true,
118
+ addItemLabel = (v: string) => `Add "${v}"`,
119
+ loadingText = '',
120
+ leftIconSvg,
51
121
  options = [],
52
- fetchFunction,
53
- searchFunction,
54
- tooltipLocation = "top",
122
+ groupedOptions = [],
123
+ tooltipLocation = 'top',
124
+ fetchFunction = null,
125
+ searchFunction = null,
126
+ retrieveLabelFunction = null,
55
127
  tooltipContent,
56
128
  tooltipText,
57
- onchange,
58
- onchangeRaw,
59
- onCreate,
60
- }: Props = $props();
61
-
62
- const CREATE_PREFIX = "__create__";
129
+ enterPressed = undefined,
130
+ onchange = undefined,
131
+ onchangeRaw = undefined,
132
+ onItemAdded = undefined
133
+ }: AutocompleteMultiProps = $props();
63
134
 
64
- let filterString = $state("");
65
- let preloading = $state(false);
66
- let searching = $state(false);
67
- let timeout: number;
135
+ if (values === null || values === undefined) values = [];
136
+ if (rawValues === null || rawValues === undefined) rawValues = [];
137
+ // Callers may preset the selection by passing only rawValues — adopt their values
138
+ if (values.length === 0 && rawValues.length > 0) values = rawValues.map((r) => r.value);
68
139
 
69
- let allOptions: ComboBoxItem[] = [];
70
- let filteredOptions: ComboBoxItem[] = $state([]);
140
+ let resolvedName = $derived((name || label || autoName).replace(/[^a-zA-Z0-9_\-:.]/g, '_'));
71
141
 
142
+ let internalValues: string[] = $state([]);
143
+ let filterValue = $state('');
72
144
  let open = $state(false);
73
145
 
74
- if (values === null || values === undefined) values = [];
75
-
76
- onMount(() => {
77
- if (fetchFunction) {
78
- preloading = true;
79
- fetchFunction().then((response: NotifyModel<ComboBoxItem[]>) => {
80
- convertOptions(response.object!);
81
- preloadValues();
82
- preloading = false;
83
- });
84
- } else {
85
- if (options.length) convertOptions(options);
86
- preloadValues();
87
- }
88
-
89
- if (autofocus) {
90
- tick().then(() => textboxElement?.focus());
146
+ const core = new ComboBoxMultiCore({
147
+ getOptions: () => options,
148
+ getGroupedOptions: () => groupedOptions,
149
+ getFetchFunction: () => fetchFunction,
150
+ getSearchFunction: () => searchFunction,
151
+ getRetrieveLabelFunction: () => retrieveLabelFunction,
152
+ getValues: () => values,
153
+ setValues: (v) => (values = v),
154
+ setRawValues: (r) => (rawValues = r),
155
+ initialRawValues: [...rawValues],
156
+ clearUnknownStatic: false,
157
+ onRawValuesResolved: (raw) => onchangeRaw?.(raw),
158
+ onOptionsChanged: () => {
159
+ if (filterValue) core.filterLocal(filterValue);
91
160
  }
92
161
  });
93
162
 
94
- function preloadValues() {
95
- if (!values || values.length === 0) {
96
- rawValues = [];
97
- return;
98
- }
163
+ let isEmpty: boolean = $derived(rawValues?.length === 0);
99
164
 
100
- if (rawValues.length > 0) return;
101
-
102
- let newRawValues: ComboBoxItem[] = [];
103
-
104
- for (let value of values) {
105
- const foundOption = allOptions.find((x) => x.value === value);
106
- if (foundOption) {
107
- foundOption.selected = true;
108
- newRawValues.push({ ...foundOption });
109
- } else if (value != null) {
110
- newRawValues.push({
111
- label: String(value),
112
- value: value,
113
- selected: true,
114
- });
115
- }
165
+ /** Synthetic "add new value" suggestion shown when the typed text matches no option */
166
+ let addOption: ComboBoxItem | undefined = $derived.by(() => {
167
+ const trimmed = filterValue?.trim();
168
+ if (!allowAdd || !trimmed) return undefined;
169
+
170
+ const exists = core.allGroups
171
+ .flatMap((x) => x.items)
172
+ .some(
173
+ (x) =>
174
+ x.label.toLowerCase() === trimmed.toLowerCase() ||
175
+ x.value.toLowerCase() === trimmed.toLowerCase()
176
+ );
177
+ const alreadySelected = values.some((v) => v.toLowerCase() === trimmed.toLowerCase());
178
+ if (exists || alreadySelected) return undefined;
179
+
180
+ return { label: addItemLabel(trimmed), value: trimmed, isAddNew: true };
181
+ });
182
+
183
+ /** Groups passed to the dropdown, including the add suggestion when applicable */
184
+ let displayGroups: ComboBoxGroup[] = $derived(
185
+ addOption
186
+ ? [...core.filteredGroups, { label: '', items: [addOption], showGroupName: false }]
187
+ : core.filteredGroups
188
+ );
189
+
190
+ useDialogEvents({
191
+ onFirstOpen: () => {
192
+ if (autofocus) setTimeout(() => doFocus(), 150);
193
+ },
194
+ onOpen: () => {
195
+ if (autofocusDialog) setTimeout(() => doFocus(), 150);
116
196
  }
197
+ });
117
198
 
118
- rawValues = newRawValues;
199
+ function doFocus() {
200
+ open = true;
201
+ tick().then(() => textboxElement?.focus());
119
202
  }
120
203
 
121
- function convertOptions(opts: ComboBoxItem[]) {
122
- allOptions = opts.map((option) => ({
123
- ...option,
124
- selected: values && values.some((x) => x === option.value),
125
- }));
126
- filteredOptions = [...allOptions];
127
- }
204
+ $effect(() => {
205
+ const currentValues = values || [];
206
+ const currentInternalValues = internalValues || [];
128
207
 
129
- function appendCreateOption(opts: ComboBoxItem[], query: string): ComboBoxItem[] {
130
- if (!query.trim()) return opts;
131
- const normalizedQuery = query.trim().toLowerCase();
132
- const exactMatch = opts.some((o) => o.label.toLowerCase() === normalizedQuery);
133
- if (exactMatch) return opts;
134
- // Don't offer create for values already selected
135
- if (values.some((v) => v.toLowerCase() === normalizedQuery)) return opts;
136
- return [...opts, { label: `Create "${query.trim()}"`, value: CREATE_PREFIX + query.trim() }];
137
- }
208
+ const arraysAreDifferent =
209
+ currentValues.length !== currentInternalValues.length ||
210
+ currentValues.some((value, index) => value !== currentInternalValues[index]);
138
211
 
139
- function onSelection(selectedValue: ComboBoxItem) {
140
- if (selectedValue.value.startsWith(CREATE_PREFIX)) {
141
- const newValue = selectedValue.value.slice(CREATE_PREFIX.length);
142
- const newItem: ComboBoxItem = { label: newValue, value: newValue, selected: true };
143
-
144
- // Add to allOptions so it's available going forward
145
- allOptions = [...allOptions, newItem];
146
- if (options) {
147
- options.push({ label: newValue, value: newValue });
212
+ if (arraysAreDifferent) {
213
+ internalValues = [...currentValues];
214
+ const selfChanged = core.preloadValues();
215
+ // preloadValues fires its own callbacks when it drops invalid values; don't double-notify
216
+ if (!selfChanged) {
217
+ onchangeRaw?.(rawValues);
148
218
  }
219
+ }
220
+ });
149
221
 
150
- values = [...values, newValue];
151
- rawValues = [...rawValues, newItem];
222
+ let selectionJustMade = false;
152
223
 
153
- onchange?.(values);
154
- onchangeRaw?.(rawValues);
155
- onCreate?.(newValue);
224
+ function onSelection(selectedValue: ComboBoxItem) {
225
+ selectionJustMade = true;
156
226
 
157
- filterString = "";
158
- filteredOptions = appendCreateOption(
159
- allOptions.filter((o) => !values.includes(o.value) || o.value === newValue),
160
- "",
161
- );
162
- filteredOptions = [...allOptions];
227
+ if (selectedValue.isAddNew) {
228
+ addItem(selectedValue.value);
163
229
  return;
164
230
  }
165
231
 
166
- // Toggle selection
167
- const alreadySelected = values.includes(selectedValue.value);
232
+ const items = core.allGroups.flatMap((x) => x.items);
233
+ const index = items.findIndex((opt) => opt.value === selectedValue.value);
234
+ const actualOption = index !== -1 ? items[index] : selectedValue;
168
235
 
169
- if (alreadySelected) {
170
- values = values.filter((v) => v !== selectedValue.value);
171
- rawValues = rawValues.filter((rv) => rv.value !== selectedValue.value);
172
- const opt = allOptions.find((o) => o.value === selectedValue.value);
173
- if (opt) opt.selected = false;
174
- } else {
175
- const opt = allOptions.find((o) => o.value === selectedValue.value);
176
- if (opt) opt.selected = true;
177
- values = [...values, selectedValue.value];
178
- rawValues = [...rawValues, opt ?? selectedValue];
236
+ actualOption.selected = !actualOption.selected;
237
+
238
+ if (actualOption.selected && !values.includes(actualOption.value)) {
239
+ values = [...values, actualOption.value];
240
+ rawValues = [...rawValues, actualOption];
241
+ } else if (!actualOption.selected) {
242
+ values = values.filter((value) => value !== actualOption.value);
243
+ rawValues = rawValues.filter((rawValue) => rawValue.value !== actualOption.value);
179
244
  }
180
245
 
246
+ internalValues = [...values];
181
247
  onchange?.(values);
182
248
  onchangeRaw?.(rawValues);
183
249
 
184
- filterString = "";
185
- filteredOptions = [...allOptions];
250
+ if (closeOnSelection) open = false;
251
+ tick().then(() => textboxElement?.focus());
186
252
  }
187
253
 
188
- function removeValue(item: ComboBoxItem) {
189
- values = values.filter((v) => v !== item.value);
190
- rawValues = rawValues.filter((rv) => rv.value !== item.value);
191
- const opt = allOptions.find((o) => o.value === item.value);
192
- if (opt) opt.selected = false;
254
+ function addItem(newValue: string) {
255
+ selectionJustMade = true;
256
+ const trimmed = newValue.trim();
257
+ filterValue = '';
258
+ core.filterLocal('');
259
+
260
+ if (!trimmed || values.some((v) => v.toLowerCase() === trimmed.toLowerCase())) return;
261
+
262
+ const newItem: ComboBoxItem = { label: trimmed, value: trimmed, selected: true };
263
+ values = [...values, trimmed];
264
+ rawValues = [...rawValues, newItem];
265
+ internalValues = [...values];
266
+
267
+ onItemAdded?.(trimmed);
193
268
  onchange?.(values);
194
269
  onchangeRaw?.(rawValues);
270
+
271
+ if (closeOnSelection) open = false;
272
+ tick().then(() => textboxElement?.focus());
195
273
  }
196
274
 
197
- function onFilterChange(value: string) {
198
- if (searchFunction && value) {
199
- clearTimeout(timeout);
200
- searching = true;
201
- timeout = window.setTimeout(() => {
202
- searchFunction(value).then(
203
- (response: NotifyModel<ComboBoxItem[]>) => {
204
- convertOptions(response.object!);
205
- filteredOptions = appendCreateOption(filteredOptions, value);
206
- searching = false;
207
- open = true;
208
- },
209
- );
210
- }, 300);
211
- } else if (allOptions) {
212
- filteredOptions = allOptions.filter((option) =>
213
- option.label.toLowerCase().includes(value.toLowerCase()),
214
- );
215
- filteredOptions = appendCreateOption(filteredOptions, value);
275
+ function removeItem(item: ComboBoxItem) {
276
+ values = values.filter((value) => value !== item.value);
277
+ rawValues = rawValues.filter((rawValue) => rawValue.value !== item.value);
278
+ internalValues = [...values];
279
+
280
+ const match = core.allGroups.flatMap((x) => x.items).find((x) => x.value === item.value);
281
+ if (match) match.selected = false;
282
+
283
+ onchange?.(values);
284
+ onchangeRaw?.(rawValues);
285
+ }
286
+
287
+ function clearAll() {
288
+ values = [];
289
+ rawValues = [];
290
+ internalValues = [];
291
+ core.allGroups.flatMap((x) => x.items).forEach((x) => (x.selected = false));
292
+
293
+ onchange?.(values);
294
+ onchangeRaw?.(rawValues);
295
+ }
296
+
297
+ function onFilterChange(text: string) {
298
+ if (searchFunction && text) {
299
+ core.search(text, () => (open = true));
300
+ } else {
301
+ core.filterLocal(text);
216
302
  open = true;
217
303
  }
218
304
  }
219
305
 
306
+ let textboxElement: HTMLInputElement | undefined = $state(undefined);
220
307
  let comboBoxEl: ReturnType<typeof ComboBoxMulti> | undefined = $state(undefined);
221
- let textboxElement: HTMLElement | undefined = $state(undefined);
222
308
 
223
309
  function onKeyDown(e: KeyboardEvent) {
224
- if (e.key === "Backspace" && filterString === "" && rawValues.length > 0) {
225
- // Remove last selected value on backspace when input is empty
226
- removeValue(rawValues[rawValues.length - 1]);
310
+ if (e.key === 'Backspace' && filterValue.length === 0 && rawValues.length > 0) {
311
+ removeItem(rawValues[rawValues.length - 1]);
227
312
  return;
228
313
  }
229
314
 
230
- if (!open) return;
315
+ if (!open) {
316
+ if (e.key === 'Enter') enterPressed?.(filterValue);
317
+ return;
318
+ }
231
319
 
232
- if (e.key === "Escape") {
320
+ if (e.key === 'Escape') {
233
321
  e.preventDefault();
234
322
  open = false;
235
- }
323
+ } else {
324
+ selectionJustMade = false;
325
+ comboBoxEl?.handleKeyDown?.(e);
236
326
 
237
- comboBoxEl?.handleKeyDown?.(e);
327
+ if (!selectionJustMade && e.key === 'Enter') enterPressed?.(filterValue);
328
+ }
238
329
  }
239
330
 
240
331
  function onFocus() {
@@ -242,54 +333,102 @@
242
333
  }
243
334
 
244
335
  function closePopover(e: MouseEvent) {
245
- if (textboxElement?.contains(e.target as HTMLElement)) return;
246
-
336
+ if (controlElement?.contains(e.target as HTMLElement)) return;
247
337
  open = false;
248
338
  }
249
339
 
250
- // Build grouped options for ComboBoxMulti
251
- let groupedOptions = $derived([{ label: "", items: filteredOptions }]);
340
+ let controlElement: HTMLElement | undefined = $state(undefined);
341
+
342
+ const extraClasses: string[] = $derived.by(() => {
343
+ const list: string[] = [];
344
+ if (open) list.push('open');
345
+ if (disabled) list.push('disabled');
346
+ if (showError) list.push('show-error');
347
+ return list;
348
+ });
252
349
  </script>
253
350
 
254
- <div class="autocomplete-multi-container" class:show-error={showError}>
255
- <FormGroup {label} {required} {tooltipContent} {tooltipLocation} {tooltipText}>
256
- <div class="autocomplete-multi-input {classes}" class:focused={open} class:disabled use:clickOutside={closePopover}>
257
- {#each rawValues as item}
258
- {#if disabled}
259
- <Badge>{item.label}</Badge>
260
- {:else}
261
- <Badge onDelete={() => removeValue(item)}>{item.label}</Badge>
262
- {/if}
263
- {/each}
264
- <input
265
- class="inline-input"
266
- bind:this={textboxElement}
267
- bind:value={filterString}
268
- {placeholder}
269
- {disabled}
270
- autocomplete="off"
271
- onfocus={onFocus}
272
- oninput={() => onFilterChange(filterString)}
273
- onkeydown={onKeyDown}
274
- />
351
+ <div class="autocomplete-multi-container {containerClass}">
352
+ <FormGroup
353
+ {label}
354
+ {required}
355
+ {showError}
356
+ {errorText}
357
+ {tooltipContent}
358
+ {tooltipText}
359
+ {tooltipLocation}
360
+ class={groupClass}
361
+ >
362
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
363
+ <div
364
+ bind:this={controlElement}
365
+ class="ac-control {extraClasses.join(' ')} {classes}"
366
+ onclick={() => {
367
+ if (!disabled) tick().then(() => textboxElement?.focus());
368
+ }}
369
+ onkeydown={() => {}}
370
+ >
371
+ {#if leftIconSvg}
372
+ <span class="left-icon"><Icon svg={leftIconSvg} size="1rem" /></span>
373
+ {/if}
374
+
375
+ <div class="ac-chips">
376
+ {#each rawValues as item (item.value)}
377
+ {#if disabled}
378
+ <Badge>{item.label}</Badge>
379
+ {:else}
380
+ <Badge ondelete={() => removeItem(item)}>{item.label}</Badge>
381
+ {/if}
382
+ {/each}
383
+
384
+ <input
385
+ bind:this={textboxElement}
386
+ bind:value={filterValue}
387
+ name={resolvedName}
388
+ type="search"
389
+ class="ac-input"
390
+ {disabled}
391
+ placeholder={core.preloading ? 'Loading...' : isEmpty ? placeholder : ''}
392
+ autocomplete="off"
393
+ oninput={() => onFilterChange(filterValue)}
394
+ onfocus={onFocus}
395
+ onkeydown={onKeyDown}
396
+ />
397
+ </div>
398
+
399
+ {#if core.preloading || core.searching}
400
+ <span class="control-action"><Loader sizeOverride="1.125rem" /></span>
401
+ {:else if !isEmpty && !disabled}
402
+ <span class="control-action">
403
+ <ActionIcon
404
+ variant="secondary-subtle"
405
+ svg={iconX}
406
+ size="0.75rem"
407
+ onclick={(e: MouseEvent) => {
408
+ clearAll();
409
+ e.stopPropagation();
410
+ }}
411
+ />
412
+ </span>
413
+ {/if}
275
414
  </div>
276
- <div class="relative w-full">
415
+ </FormGroup>
416
+
417
+ <div class="autocomplete-panel {open ? '' : 'hidden'}">
418
+ <div class="autocomplete-panel-inner" use:clickOutside={closePopover}>
277
419
  <ComboBoxMulti
278
420
  bind:this={comboBoxEl}
279
- {filterString}
421
+ filterString={filterValue}
280
422
  values={rawValues}
281
- onSelection={onSelection}
282
- {groupedOptions}
423
+ {onSelection}
424
+ groupedOptions={displayGroups}
283
425
  {open}
284
- loading={searching || preloading}
285
- {showNoResultsMessage}
426
+ loading={core.searching}
427
+ {hideNoResults}
286
428
  {loadingText}
287
429
  />
288
430
  </div>
289
- </FormGroup>
290
- {#if showError && errorText}
291
- <div class="text-error">{errorText}</div>
292
- {/if}
431
+ </div>
293
432
  </div>
294
433
 
295
434
  <style>
@@ -297,60 +436,79 @@
297
436
  position: relative;
298
437
  }
299
438
 
300
- .autocomplete-multi-input {
439
+ .ac-control {
301
440
  display: flex;
302
- flex-wrap: wrap;
303
441
  align-items: center;
304
- gap: var(--pui-spacing-1);
305
- padding: var(--pui-spacing-1) var(--pui-spacing-2);
306
- border: 1px solid var(--pui-input-border);
307
- background-color: var(--pui-input-bg);
308
- color: var(--pui-input-text);
309
- border-radius: var(--pui-radius-base);
310
- cursor: text;
442
+ gap: 0.25rem;
443
+ width: 100%;
311
444
  min-height: 2.25rem;
445
+ padding: 0.25rem 0.5rem;
446
+ border-radius: 0.25rem;
447
+ background-color: var(--input-bg-color);
448
+ color: var(--text-color);
449
+ border: 1px solid var(--border-color);
450
+ cursor: text;
312
451
  }
313
452
 
314
- .autocomplete-multi-input.focused {
315
- border-color: var(--pui-input-border-focus);
453
+ .ac-control.open {
454
+ border-color: var(--accent-color);
316
455
  }
317
456
 
318
- .autocomplete-multi-input.disabled {
457
+ .ac-control.show-error {
458
+ border-color: var(--danger-text);
459
+ }
460
+
461
+ .ac-control.disabled {
319
462
  opacity: 0.5;
320
463
  cursor: not-allowed;
321
- background-color: var(--pui-bg-disabled);
464
+ background-color: var(--border-color);
322
465
  }
323
466
 
324
- .inline-input {
325
- flex: 1;
326
- min-width: 4rem;
467
+ .left-icon {
468
+ display: flex;
469
+ align-items: center;
470
+ pointer-events: none;
471
+ }
472
+
473
+ .ac-chips {
474
+ display: flex;
475
+ flex-wrap: wrap;
476
+ align-items: center;
477
+ gap: 0.25rem;
478
+ flex: 1 1 auto;
479
+ min-width: 0;
480
+ }
481
+
482
+ .ac-input {
483
+ flex: 1 1 6rem;
484
+ min-width: 6rem;
327
485
  border: none;
328
486
  outline: none;
329
487
  background: transparent;
330
- color: var(--pui-input-text);
331
- font-size: var(--pui-font-size-md);
332
- line-height: var(--pui-line-height-normal);
333
- padding: 0;
334
- }
335
-
336
- .inline-input::placeholder {
337
- color: var(--pui-text-placeholder);
488
+ color: var(--text-color);
489
+ font-size: 1rem;
490
+ padding: 0.25rem 0;
338
491
  }
339
492
 
340
- .relative {
341
- position: relative;
493
+ .control-action {
494
+ display: flex;
495
+ align-items: center;
496
+ line-height: 0;
342
497
  }
343
498
 
344
- .w-full {
499
+ .autocomplete-panel {
500
+ position: absolute;
345
501
  width: 100%;
502
+ left: 0;
503
+ z-index: 70;
346
504
  }
347
505
 
348
- .show-error .autocomplete-multi-input {
349
- border-color: var(--pui-text-danger);
506
+ .autocomplete-panel-inner {
507
+ position: relative;
508
+ width: 100%;
350
509
  }
351
510
 
352
- .text-error {
353
- color: var(--pui-text-danger);
354
- font-size: var(--pui-font-size-sm);
511
+ .hidden {
512
+ display: none;
355
513
  }
356
514
  </style>