@umbra.ui/core 0.1.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 (272) hide show
  1. package/dist/components/controls/Dropdown/types.d.ts +5 -0
  2. package/dist/components/controls/Dropdown/types.d.ts.map +1 -0
  3. package/dist/components/controls/Dropdown/types.js +1 -0
  4. package/dist/components/controls/SegmentedControl/types.d.ts +6 -0
  5. package/dist/components/controls/SegmentedControl/types.d.ts.map +1 -0
  6. package/dist/components/controls/SegmentedControl/types.js +1 -0
  7. package/dist/components/dialogs/Alert/types.d.ts +7 -0
  8. package/dist/components/dialogs/Alert/types.d.ts.map +1 -0
  9. package/dist/components/dialogs/Alert/types.js +1 -0
  10. package/dist/components/dialogs/Toast/types.d.ts +34 -0
  11. package/dist/components/dialogs/Toast/types.d.ts.map +1 -0
  12. package/dist/components/dialogs/Toast/types.js +10 -0
  13. package/dist/components/dialogs/Toast/useToast.d.ts +36 -0
  14. package/dist/components/dialogs/Toast/useToast.d.ts.map +1 -0
  15. package/dist/components/dialogs/Toast/useToast.js +90 -0
  16. package/dist/components/indicators/Tooltip/tooltip.d.ts +3 -0
  17. package/dist/components/indicators/Tooltip/tooltip.d.ts.map +1 -0
  18. package/dist/components/indicators/Tooltip/tooltip.js +33 -0
  19. package/dist/components/indicators/Tooltip/types.d.ts +14 -0
  20. package/dist/components/indicators/Tooltip/types.d.ts.map +1 -0
  21. package/dist/components/indicators/Tooltip/types.js +1 -0
  22. package/dist/components/indicators/Tooltip/useTooltip.d.ts +18 -0
  23. package/dist/components/indicators/Tooltip/useTooltip.d.ts.map +1 -0
  24. package/dist/components/indicators/Tooltip/useTooltip.js +57 -0
  25. package/dist/components/inputs/Tags/tag-bar-styles.d.ts +14 -0
  26. package/dist/components/inputs/Tags/tag-bar-styles.d.ts.map +1 -0
  27. package/dist/components/inputs/Tags/tag-bar-styles.js +313 -0
  28. package/dist/components/inputs/Tags/types.d.ts +93 -0
  29. package/dist/components/inputs/Tags/types.d.ts.map +1 -0
  30. package/dist/components/inputs/Tags/types.js +216 -0
  31. package/dist/components/inputs/search/types.d.ts +9 -0
  32. package/dist/components/inputs/search/types.d.ts.map +1 -0
  33. package/dist/components/inputs/search/types.js +1 -0
  34. package/dist/components/navigation/adaptive/types.d.ts +16 -0
  35. package/dist/components/navigation/adaptive/types.d.ts.map +1 -0
  36. package/dist/components/navigation/adaptive/types.js +1 -0
  37. package/dist/components/navigation/adaptive/useAdaptiveLayout.d.ts +27 -0
  38. package/dist/components/navigation/adaptive/useAdaptiveLayout.d.ts.map +1 -0
  39. package/dist/components/navigation/adaptive/useAdaptiveLayout.js +40 -0
  40. package/dist/components/navigation/adaptive/useBreakpoints.d.ts +6 -0
  41. package/dist/components/navigation/adaptive/useBreakpoints.d.ts.map +1 -0
  42. package/dist/components/navigation/adaptive/useBreakpoints.js +37 -0
  43. package/dist/components/navigation/adaptive/useContainerMonitor.d.ts +93 -0
  44. package/dist/components/navigation/adaptive/useContainerMonitor.d.ts.map +1 -0
  45. package/dist/components/navigation/adaptive/useContainerMonitor.js +145 -0
  46. package/dist/components/navigation/adaptive/useViewAnimation.d.ts +31 -0
  47. package/dist/components/navigation/adaptive/useViewAnimation.d.ts.map +1 -0
  48. package/dist/components/navigation/adaptive/useViewAnimation.js +591 -0
  49. package/dist/components/navigation/adaptive/useViewResize.d.ts +52 -0
  50. package/dist/components/navigation/adaptive/useViewResize.d.ts.map +1 -0
  51. package/dist/components/navigation/adaptive/useViewResize.js +146 -0
  52. package/dist/components/navigation/navstack/useNavigationStack.d.ts +25 -0
  53. package/dist/components/navigation/navstack/useNavigationStack.d.ts.map +1 -0
  54. package/dist/components/navigation/navstack/useNavigationStack.js +133 -0
  55. package/dist/components/navigation/slideover/useSlideoverController.d.ts +20 -0
  56. package/dist/components/navigation/slideover/useSlideoverController.d.ts.map +1 -0
  57. package/dist/components/navigation/slideover/useSlideoverController.js +267 -0
  58. package/dist/components/navigation/splitview/useSplitViewController.d.ts +20 -0
  59. package/dist/components/navigation/splitview/useSplitViewController.d.ts.map +1 -0
  60. package/dist/components/navigation/splitview/useSplitViewController.js +325 -0
  61. package/dist/components/navigation/tabcontroller/types.d.ts +21 -0
  62. package/dist/components/navigation/tabcontroller/types.d.ts.map +1 -0
  63. package/dist/components/navigation/tabcontroller/types.js +1 -0
  64. package/dist/components/navigation/tabcontroller/useTabController.d.ts +5 -0
  65. package/dist/components/navigation/tabcontroller/useTabController.d.ts.map +1 -0
  66. package/dist/components/navigation/tabcontroller/useTabController.js +10 -0
  67. package/dist/components/navigation/types.d.ts +8 -0
  68. package/dist/components/navigation/types.d.ts.map +1 -0
  69. package/dist/components/navigation/types.js +1 -0
  70. package/dist/components/pickers/CollectionPicker/types.d.ts +11 -0
  71. package/dist/components/pickers/CollectionPicker/types.d.ts.map +1 -0
  72. package/dist/components/pickers/CollectionPicker/types.js +1 -0
  73. package/dist/components/pickers/ColorPicker/colors.d.ts +13 -0
  74. package/dist/components/pickers/ColorPicker/colors.d.ts.map +1 -0
  75. package/dist/components/pickers/ColorPicker/colors.js +266 -0
  76. package/dist/components/pickers/FilePicker/types.d.ts +10 -0
  77. package/dist/components/pickers/FilePicker/types.d.ts.map +1 -0
  78. package/dist/components/pickers/FilePicker/types.js +1 -0
  79. package/dist/index.d.ts +91 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +196 -0
  82. package/dist/theme.d.ts +73 -0
  83. package/dist/theme.d.ts.map +1 -0
  84. package/dist/theme.js +279 -0
  85. package/dist/themes/blank.d.ts +7 -0
  86. package/dist/themes/blank.d.ts.map +1 -0
  87. package/dist/themes/blank.js +543 -0
  88. package/dist/themes/crimson-dark.d.ts +4 -0
  89. package/dist/themes/crimson-dark.d.ts.map +1 -0
  90. package/dist/themes/crimson-dark.js +552 -0
  91. package/dist/themes/cyan-light.d.ts +4 -0
  92. package/dist/themes/cyan-light.d.ts.map +1 -0
  93. package/dist/themes/cyan-light.js +552 -0
  94. package/dist/themes/dark.d.ts +4 -0
  95. package/dist/themes/dark.d.ts.map +1 -0
  96. package/dist/themes/dark.js +551 -0
  97. package/dist/themes/gold-dark.d.ts +4 -0
  98. package/dist/themes/gold-dark.d.ts.map +1 -0
  99. package/dist/themes/gold-dark.js +552 -0
  100. package/dist/themes/grass-dark.d.ts +4 -0
  101. package/dist/themes/grass-dark.d.ts.map +1 -0
  102. package/dist/themes/grass-dark.js +552 -0
  103. package/dist/themes/indigo.d.ts +4 -0
  104. package/dist/themes/indigo.d.ts.map +1 -0
  105. package/dist/themes/indigo.js +552 -0
  106. package/dist/themes/light.d.ts +4 -0
  107. package/dist/themes/light.d.ts.map +1 -0
  108. package/dist/themes/light.js +551 -0
  109. package/dist/themes/orange-dark.d.ts +4 -0
  110. package/dist/themes/orange-dark.d.ts.map +1 -0
  111. package/dist/themes/orange-dark.js +551 -0
  112. package/dist/themes/orange-light.d.ts +4 -0
  113. package/dist/themes/orange-light.d.ts.map +1 -0
  114. package/dist/themes/orange-light.js +551 -0
  115. package/package.json +62 -0
  116. package/src/components/controls/Button/Button.vue +417 -0
  117. package/src/components/controls/Button/README.md +348 -0
  118. package/src/components/controls/Button/theme.css +200 -0
  119. package/src/components/controls/Checkbox/Checkbox.vue +164 -0
  120. package/src/components/controls/Checkbox/README.md +441 -0
  121. package/src/components/controls/Checkbox/theme.css +36 -0
  122. package/src/components/controls/Dropdown/Dropdown.vue +476 -0
  123. package/src/components/controls/Dropdown/README.md +370 -0
  124. package/src/components/controls/Dropdown/theme.css +50 -0
  125. package/src/components/controls/Dropdown/types.ts +6 -0
  126. package/src/components/controls/IconButton/IconButton.vue +267 -0
  127. package/src/components/controls/IconButton/README.md +502 -0
  128. package/src/components/controls/IconButton/theme.css +89 -0
  129. package/src/components/controls/Radio/README.md +591 -0
  130. package/src/components/controls/Radio/Radio.vue +89 -0
  131. package/src/components/controls/Radio/theme.css +14 -0
  132. package/src/components/controls/RangeSlider/README.md +608 -0
  133. package/src/components/controls/RangeSlider/RangeSlider.vue +535 -0
  134. package/src/components/controls/RangeSlider/theme.css +80 -0
  135. package/src/components/controls/SegmentedControl/README.md +587 -0
  136. package/src/components/controls/SegmentedControl/SegmentedControl.vue +284 -0
  137. package/src/components/controls/SegmentedControl/theme.css +60 -0
  138. package/src/components/controls/SegmentedControl/types.ts +5 -0
  139. package/src/components/controls/Slider/README.md +627 -0
  140. package/src/components/controls/Slider/Slider.vue +260 -0
  141. package/src/components/controls/Slider/theme.css +74 -0
  142. package/src/components/controls/Stepper/README.md +601 -0
  143. package/src/components/controls/Stepper/Stepper.vue +103 -0
  144. package/src/components/controls/Stepper/theme.css +53 -0
  145. package/src/components/controls/Switch/README.md +667 -0
  146. package/src/components/controls/Switch/Switch.vue +127 -0
  147. package/src/components/controls/Switch/theme.css +42 -0
  148. package/src/components/dialogs/Alert/Alert.vue +218 -0
  149. package/src/components/dialogs/Alert/README.md +450 -0
  150. package/src/components/dialogs/Alert/theme.css +44 -0
  151. package/src/components/dialogs/Alert/types.ts +11 -0
  152. package/src/components/dialogs/Toast/README.md +522 -0
  153. package/src/components/dialogs/Toast/Toast.vue +296 -0
  154. package/src/components/dialogs/Toast/ToastContainer.vue +330 -0
  155. package/src/components/dialogs/Toast/theme.css +44 -0
  156. package/src/components/dialogs/Toast/types.ts +46 -0
  157. package/src/components/dialogs/Toast/useToast.ts +127 -0
  158. package/src/components/indicators/ProgressBar/ProgressBar.vue +98 -0
  159. package/src/components/indicators/ProgressBar/README.md +744 -0
  160. package/src/components/indicators/ProgressBar/theme.css +36 -0
  161. package/src/components/indicators/Tooltip/README.md +723 -0
  162. package/src/components/indicators/Tooltip/TooltipProvider.vue +142 -0
  163. package/src/components/indicators/Tooltip/theme.css +18 -0
  164. package/src/components/indicators/Tooltip/tooltip.ts +48 -0
  165. package/src/components/indicators/Tooltip/types.ts +15 -0
  166. package/src/components/indicators/Tooltip/useTooltip.ts +71 -0
  167. package/src/components/inputs/AutogrowTextView/AutogrowTextView.vue +110 -0
  168. package/src/components/inputs/AutogrowTextView/README.md +643 -0
  169. package/src/components/inputs/AutogrowTextView/theme.css +28 -0
  170. package/src/components/inputs/InputCard/InputCard.vue +600 -0
  171. package/src/components/inputs/InputCard/README.md +636 -0
  172. package/src/components/inputs/InputEmail/InputEmail.vue +698 -0
  173. package/src/components/inputs/InputEmail/README.md +764 -0
  174. package/src/components/inputs/InputNumber/InputNumber.vue +300 -0
  175. package/src/components/inputs/InputNumber/README.md +749 -0
  176. package/src/components/inputs/InputPhone/InputPhone.vue +645 -0
  177. package/src/components/inputs/InputPhone/README.md +636 -0
  178. package/src/components/inputs/InputSecure/InputSecure.vue +646 -0
  179. package/src/components/inputs/InputSecure/README.md +771 -0
  180. package/src/components/inputs/InputText/InputText.vue +225 -0
  181. package/src/components/inputs/InputText/README.md +844 -0
  182. package/src/components/inputs/OTP/OTP.vue +349 -0
  183. package/src/components/inputs/OTP/README.md +736 -0
  184. package/src/components/inputs/OTP/theme.css +50 -0
  185. package/src/components/inputs/StringCapture/README.md +718 -0
  186. package/src/components/inputs/StringCapture/StringCapture.vue +315 -0
  187. package/src/components/inputs/StringCapture/theme.css +86 -0
  188. package/src/components/inputs/Tags/README.md +897 -0
  189. package/src/components/inputs/Tags/TagBar.vue +793 -0
  190. package/src/components/inputs/Tags/TagCreation.vue +219 -0
  191. package/src/components/inputs/Tags/TagPicker.vue +380 -0
  192. package/src/components/inputs/Tags/tag-bar-styles.ts +354 -0
  193. package/src/components/inputs/Tags/theme.css +121 -0
  194. package/src/components/inputs/Tags/types.ts +346 -0
  195. package/src/components/inputs/search/README.md +759 -0
  196. package/src/components/inputs/search/SearchBar.vue +394 -0
  197. package/src/components/inputs/search/SearchResults.vue +310 -0
  198. package/src/components/inputs/search/theme.css +187 -0
  199. package/src/components/inputs/search/types.ts +8 -0
  200. package/src/components/inputs/theme.css +102 -0
  201. package/src/components/menus/ActionMenu/ActionMenu.vue +383 -0
  202. package/src/components/menus/ActionMenu/README.md +825 -0
  203. package/src/components/menus/ActionMenu/theme.css +93 -0
  204. package/src/components/models/Popover/Popover.vue +551 -0
  205. package/src/components/models/Popover/README.md +885 -0
  206. package/src/components/models/Popover/theme.css +52 -0
  207. package/src/components/models/Sheet/README.md +1159 -0
  208. package/src/components/models/Sheet/Sheet.vue +465 -0
  209. package/src/components/models/Sheet/theme.css +72 -0
  210. package/src/components/models/Sidebar/README.md +1228 -0
  211. package/src/components/models/Sidebar/Sidebar.vue +480 -0
  212. package/src/components/models/Sidebar/theme.css +90 -0
  213. package/src/components/navigation/adaptive/AdaptiveLayout.vue +779 -0
  214. package/src/components/navigation/adaptive/AdaptiveLayoutBreadcrumbs.vue +192 -0
  215. package/src/components/navigation/adaptive/AdaptiveLayoutMenuButton.vue +149 -0
  216. package/src/components/navigation/adaptive/README.md +768 -0
  217. package/src/components/navigation/adaptive/types.ts +19 -0
  218. package/src/components/navigation/adaptive/useAdaptiveLayout.ts +89 -0
  219. package/src/components/navigation/adaptive/useBreakpoints.ts +41 -0
  220. package/src/components/navigation/adaptive/useContainerMonitor.ts +214 -0
  221. package/src/components/navigation/adaptive/useViewAnimation.ts +721 -0
  222. package/src/components/navigation/adaptive/useViewResize.ts +211 -0
  223. package/src/components/navigation/navstack/NavigationStack.vue +180 -0
  224. package/src/components/navigation/navstack/README.md +994 -0
  225. package/src/components/navigation/navstack/useNavigationStack.ts +164 -0
  226. package/src/components/navigation/slideover/README.md +1275 -0
  227. package/src/components/navigation/slideover/SlideoverController.vue +287 -0
  228. package/src/components/navigation/slideover/useSlideoverController.ts +320 -0
  229. package/src/components/navigation/splitview/README.md +1115 -0
  230. package/src/components/navigation/splitview/SplitViewController.vue +176 -0
  231. package/src/components/navigation/splitview/useSplitViewController.ts +388 -0
  232. package/src/components/navigation/tabcontroller/README.md +919 -0
  233. package/src/components/navigation/tabcontroller/TabController.vue +307 -0
  234. package/src/components/navigation/tabcontroller/TabItem.vue +57 -0
  235. package/src/components/navigation/tabcontroller/types.ts +24 -0
  236. package/src/components/navigation/tabcontroller/useTabController.ts +18 -0
  237. package/src/components/navigation/theme.css +91 -0
  238. package/src/components/navigation/types.ts +7 -0
  239. package/src/components/pickers/CollectionPicker/CollectionPicker.vue +398 -0
  240. package/src/components/pickers/CollectionPicker/README.md +1115 -0
  241. package/src/components/pickers/CollectionPicker/theme.css +14 -0
  242. package/src/components/pickers/CollectionPicker/types.ts +11 -0
  243. package/src/components/pickers/ColorPicker/ColorPicker.vue +376 -0
  244. package/src/components/pickers/ColorPicker/README.md +1439 -0
  245. package/src/components/pickers/ColorPicker/colors.ts +299 -0
  246. package/src/components/pickers/ColorPicker/theme.css +32 -0
  247. package/src/components/pickers/DatePicker/DatePicker.vue +660 -0
  248. package/src/components/pickers/DatePicker/README.md +1195 -0
  249. package/src/components/pickers/DatePicker/theme.css +22 -0
  250. package/src/components/pickers/FilePicker/FilePicker.vue +534 -0
  251. package/src/components/pickers/FilePicker/README.md +1542 -0
  252. package/src/components/pickers/FilePicker/theme.css +48 -0
  253. package/src/components/pickers/FilePicker/types.ts +10 -0
  254. package/src/components/pickers/IconPicker/IconPicker.vue +327 -0
  255. package/src/components/pickers/IconPicker/README.md +1161 -0
  256. package/src/components/pickers/IconPicker/theme.css +28 -0
  257. package/src/components/pickers/theme.css +82 -0
  258. package/src/components/views/MarkdownViewer/MarkdownViewer.vue +442 -0
  259. package/src/components/views/MarkdownViewer/README.md +833 -0
  260. package/src/components/views/MarkdownViewer/theme.css +130 -0
  261. package/src/index.ts +263 -0
  262. package/src/theme.ts +378 -0
  263. package/src/themes/crimson-dark.ts +556 -0
  264. package/src/themes/cyan-light.ts +556 -0
  265. package/src/themes/dark.ts +557 -0
  266. package/src/themes/gold-dark.ts +556 -0
  267. package/src/themes/grass-dark.ts +556 -0
  268. package/src/themes/indigo.ts +556 -0
  269. package/src/themes/light.ts +557 -0
  270. package/src/themes/orange-dark.ts +557 -0
  271. package/src/themes/orange-light.ts +557 -0
  272. package/src/vue.d.ts +45 -0
@@ -0,0 +1,793 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ computed,
4
+ ref,
5
+ onMounted,
6
+ nextTick,
7
+ onUnmounted,
8
+ Teleport,
9
+ watch,
10
+ } from "vue";
11
+ import { useTheme } from "../../../theme";
12
+ import { TagItem, generateTagStyle } from "./types";
13
+ import { IconButton } from "@umbra-ui/core";
14
+ import { gsap } from "gsap";
15
+ import TagPicker from "./TagPicker.vue";
16
+ import {
17
+ offset,
18
+ flip,
19
+ shift,
20
+ size,
21
+ computePosition,
22
+ autoUpdate,
23
+ } from "@floating-ui/vue";
24
+ import "./theme.css";
25
+
26
+ // - Props (Pure Data)
27
+ export interface Props {
28
+ allTags: TagItem[];
29
+ selectedTags: TagItem[];
30
+ query: string;
31
+ matchIndex: number;
32
+ placeholder?: string;
33
+ overflow?: "full" | "fixed" | "list";
34
+ allowEditing?: boolean;
35
+ }
36
+
37
+ const props = withDefaults(defineProps<Props>(), {
38
+ allTags: () => [],
39
+ selectedTags: () => [],
40
+ query: "",
41
+ matchIndex: -1,
42
+ placeholder: "ADD TAGS",
43
+ overflow: "fixed",
44
+ allowEditing: true,
45
+ });
46
+
47
+ // - Emits (Pure Actions)
48
+ const emits = defineEmits<{
49
+ "update:query": [query: string];
50
+ "update:matchIndex": [index: number];
51
+ "tag-create": [title: string];
52
+ "tag-select": [tag: TagItem];
53
+ "tag-remove": [tag: TagItem];
54
+ "tag-reorder": [tags: TagItem[]];
55
+ }>();
56
+
57
+ const availableTags = computed(() => {
58
+ return props.allTags.filter(
59
+ (tag) =>
60
+ !props.selectedTags.some(
61
+ (selectedTag) =>
62
+ selectedTag.title.toLowerCase() === tag.title.toLowerCase()
63
+ )
64
+ );
65
+ });
66
+
67
+ const matches = computed(() => {
68
+ const lowercaseQuery = props.query.toLowerCase().trim();
69
+ return availableTags.value
70
+ .filter((tag) => tag.title.toLowerCase().includes(lowercaseQuery))
71
+ .sort((a, b) =>
72
+ a.title.localeCompare(b.title, "en", { sensitivity: "base" })
73
+ );
74
+ });
75
+
76
+ const queryAlreadyAdded = computed(() => {
77
+ return props.selectedTags.some(
78
+ (tag) => tag.title.toLowerCase().trim() === props.query.toLowerCase().trim()
79
+ );
80
+ });
81
+
82
+ const canCreateTag = computed(() => {
83
+ return (
84
+ props.query.trim() !== "" &&
85
+ !queryAlreadyAdded.value &&
86
+ !matches.value.some(
87
+ (tag) => tag.title.toLowerCase() === props.query.toLowerCase().trim()
88
+ )
89
+ );
90
+ });
91
+
92
+ // Calculate total navigable items (matches + create option if applicable)
93
+ const totalNavigableItems = computed(() => {
94
+ return matches.value.length + (canCreateTag.value ? 1 : 0);
95
+ });
96
+
97
+ // Check if we should show the picker (when query is empty, show all available tags)
98
+ const shouldShowPicker = computed(() => {
99
+ return (
100
+ showPopover.value &&
101
+ (props.query.trim() !== "" || availableTags.value.length > 0)
102
+ );
103
+ });
104
+
105
+ // Helper function to get tag style
106
+ const getTagStyle = (tag: TagItem) => {
107
+ if (tag.style) {
108
+ return generateTagStyle(tag.style);
109
+ }
110
+ // Default style if none provided
111
+ return generateTagStyle();
112
+ };
113
+
114
+ const addTag = async (title: string) => {
115
+ if (queryAlreadyAdded.value) {
116
+ console.log("already added tag");
117
+ return;
118
+ }
119
+
120
+ // Try to find existing tag first
121
+ const existingTag = matches.value.find(
122
+ (tag) => tag.title.toLowerCase() === props.query.toLowerCase().trim()
123
+ );
124
+
125
+ if (existingTag) {
126
+ emits("tag-select", existingTag);
127
+ } else {
128
+ // Request creation of new tag
129
+ emits("tag-create", title);
130
+ }
131
+ };
132
+
133
+ const removeTag = (tag: TagItem) => {
134
+ emits("tag-remove", tag);
135
+ emits("update:query", "");
136
+ emits("update:matchIndex", -1);
137
+ };
138
+
139
+ const onTagClick = (tag: TagItem) => {
140
+ removeTag(tag);
141
+ };
142
+
143
+ // - Element References
144
+ const field = ref<HTMLInputElement | null>(null);
145
+ const button = ref<HTMLElement | null>(null);
146
+ const label = ref<HTMLElement | null>(null);
147
+ const overlay = ref<HTMLElement | null>(null);
148
+ const container = ref<HTMLElement | null>(null);
149
+ const picker = ref<HTMLElement | null>(null);
150
+ const controlContainer = ref<HTMLElement | null>(null);
151
+
152
+ // - State Management
153
+ const showPopover = ref<boolean>(false);
154
+ const fieldWidth = ref<number>(20);
155
+ const componentWidth = ref<number>(0);
156
+ const componentHeight = ref<number>(0);
157
+ const controlWidth = ref<number>(0);
158
+
159
+ // - Position tracking
160
+ let cleanupAutoUpdate: (() => void) | null = null;
161
+
162
+ // - ResizeObserver for tracking control width
163
+ const resizeObserverControl = ref<ResizeObserver | null>(null);
164
+ const resizeObserverContainer = ref<ResizeObserver | null>(null);
165
+
166
+ // - Debounce timer for ResizeObserver
167
+ let resizeDebounceTimer: number | null = null;
168
+ let resizeContainerDebounceTimer: number | null = null;
169
+
170
+ // - Lifecycle
171
+ onMounted(() => {
172
+ windowResize();
173
+ controlContainerResize(); // Initial measurement before ResizeObserver kicks in
174
+
175
+ // Set up ResizeObserver for the controls container
176
+ if (controlContainer.value) {
177
+ resizeObserverControl.value = new ResizeObserver((entries) => {
178
+ // Clear any existing timer
179
+ if (resizeDebounceTimer) {
180
+ clearTimeout(resizeDebounceTimer);
181
+ }
182
+
183
+ // Debounce the resize callback
184
+ resizeDebounceTimer = window.setTimeout(() => {
185
+ for (const entry of entries) {
186
+ controlWidth.value = entry.contentRect.width;
187
+ }
188
+ }, 50); // 50ms debounce - adjust as needed
189
+ });
190
+
191
+ resizeObserverControl.value.observe(controlContainer.value);
192
+ }
193
+
194
+ // Set up resizeObserver for the container
195
+ if (container.value) {
196
+ resizeObserverContainer.value = new ResizeObserver((entries) => {
197
+ // Clear any existing timer
198
+ if (resizeContainerDebounceTimer) {
199
+ clearTimeout(resizeContainerDebounceTimer);
200
+ }
201
+
202
+ // Debounce the resize callback
203
+ resizeContainerDebounceTimer = window.setTimeout(() => {
204
+ for (const entry of entries) {
205
+ componentHeight.value = entry.contentRect.height;
206
+ }
207
+ }, 50); // 50ms debounce - adjust as needed
208
+ });
209
+
210
+ resizeObserverContainer.value.observe(container.value);
211
+ }
212
+ });
213
+
214
+ onUnmounted(() => {
215
+ if (cleanupAutoUpdate) {
216
+ cleanupAutoUpdate();
217
+ }
218
+
219
+ // Clean up ResizeObserver
220
+ if (resizeObserverControl.value) {
221
+ resizeObserverControl.value.disconnect();
222
+ }
223
+
224
+ if (resizeObserverContainer.value) {
225
+ resizeObserverContainer.value.disconnect();
226
+ }
227
+
228
+ // Clear debounce timer
229
+ if (resizeDebounceTimer) {
230
+ clearTimeout(resizeDebounceTimer);
231
+ }
232
+ });
233
+
234
+ const onInput = (e: Event) => {
235
+ const target = e.target as HTMLInputElement;
236
+ emits("update:query", target.value);
237
+
238
+ if (!field.value) return;
239
+
240
+ fieldWidth.value = field.value.scrollWidth;
241
+ emits("update:matchIndex", -1);
242
+
243
+ // Show popover if not already visible and there are tags to show
244
+ if (!showPopover.value && availableTags.value.length > 0) {
245
+ togglePopover();
246
+ }
247
+ };
248
+
249
+ const onKeydown = (e: KeyboardEvent) => {
250
+ if (e.key === "Backspace" || e.key === "Delete") {
251
+ fieldWidth.value -= 5.5;
252
+ if (fieldWidth.value <= 35) {
253
+ fieldWidth.value = 35;
254
+ }
255
+ }
256
+
257
+ // Only handle navigation keys if the popover is showing
258
+ if (!showPopover.value) return;
259
+
260
+ if (e.key === "ArrowUp") {
261
+ e.preventDefault();
262
+ navigateUp();
263
+ } else if (e.key === "ArrowDown") {
264
+ e.preventDefault();
265
+ navigateDown();
266
+ } else if (e.key === "Tab") {
267
+ // Tab should cycle through options
268
+ e.preventDefault();
269
+ if (e.shiftKey) {
270
+ navigateUp();
271
+ } else {
272
+ navigateDown();
273
+ }
274
+ }
275
+ };
276
+
277
+ const navigateUp = () => {
278
+ if (totalNavigableItems.value === 0) return;
279
+
280
+ let newIndex = props.matchIndex - 1;
281
+
282
+ // Wrap around to bottom
283
+ if (newIndex < -1) {
284
+ newIndex = totalNavigableItems.value - 1;
285
+ }
286
+
287
+ emits("update:matchIndex", newIndex);
288
+ };
289
+
290
+ const navigateDown = () => {
291
+ if (totalNavigableItems.value === 0) return;
292
+
293
+ let newIndex = props.matchIndex + 1;
294
+
295
+ // Wrap around to top
296
+ if (newIndex >= totalNavigableItems.value) {
297
+ newIndex = -1;
298
+ }
299
+
300
+ emits("update:matchIndex", newIndex);
301
+ };
302
+
303
+ const onInputReturn = () => {
304
+ // Check if user is selecting the "create" option
305
+ if (canCreateTag.value && props.matchIndex === matches.value.length) {
306
+ addTag(props.query);
307
+ emits("update:query", "");
308
+ emits("update:matchIndex", -1);
309
+ return;
310
+ }
311
+
312
+ // Check if user was paging through tags
313
+ if (
314
+ props.matchIndex !== -1 &&
315
+ props.matchIndex < matches.value.length &&
316
+ matches.value[props.matchIndex]
317
+ ) {
318
+ const tag = matches.value[props.matchIndex];
319
+ emits("tag-select", tag);
320
+ emits("update:query", "");
321
+ emits("update:matchIndex", -1);
322
+ return;
323
+ }
324
+
325
+ // Stop adding tags if user pressed return twice
326
+ if (props.query.trim() === "") {
327
+ endEditing();
328
+ return;
329
+ }
330
+
331
+ // Add tag and clear for next tag
332
+ addTag(props.query);
333
+ emits("update:query", "");
334
+ fieldWidth.value = 35;
335
+ };
336
+
337
+ const beginEditing = () => {
338
+ if (!field.value) return;
339
+
340
+ field.value.focus();
341
+ emits("update:query", "");
342
+ emits("update:matchIndex", -1);
343
+ gsap.to(label.value, { duration: 0.3, opacity: 0 });
344
+ gsap.to(button.value, { duration: 0.3, opacity: 0 });
345
+ gsap
346
+ .to(field.value, {
347
+ duration: 0.3,
348
+ width: 35,
349
+ opacity: 1,
350
+ })
351
+ .then(() => {
352
+ fieldWidth.value = 35;
353
+ // Show popover after animation if there are available tags
354
+ if (availableTags.value.length > 0 && !showPopover.value) {
355
+ togglePopover();
356
+ }
357
+ });
358
+ };
359
+
360
+ const endEditing = () => {
361
+ if (!field.value) return;
362
+
363
+ field.value.blur();
364
+ emits("update:query", "");
365
+ emits("update:matchIndex", -1);
366
+ gsap.to(label.value, { duration: 0.3, opacity: 1 });
367
+ gsap.to(button.value, { duration: 0.3, opacity: 1 });
368
+ gsap
369
+ .to(field.value, {
370
+ duration: 0.3,
371
+ width: 20,
372
+ opacity: 0,
373
+ })
374
+ .then(() => {
375
+ fieldWidth.value = 20;
376
+ });
377
+ if (showPopover.value) {
378
+ togglePopover();
379
+ }
380
+ };
381
+
382
+ // - Popover Management
383
+ const togglePopover = () => {
384
+ showPopover.value = !showPopover.value;
385
+ if (showPopover.value) {
386
+ nextTick(() => {
387
+ updatePopoverPosition();
388
+ });
389
+ } else {
390
+ if (cleanupAutoUpdate) {
391
+ cleanupAutoUpdate();
392
+ cleanupAutoUpdate = null;
393
+ }
394
+ }
395
+ };
396
+
397
+ const updatePopoverPosition = async () => {
398
+ if (!button.value || !picker.value) return;
399
+
400
+ await nextTick();
401
+
402
+ if (cleanupAutoUpdate) {
403
+ cleanupAutoUpdate();
404
+ }
405
+
406
+ cleanupAutoUpdate = autoUpdate(button.value, picker.value, () => {
407
+ computePosition(button.value!, picker.value!, {
408
+ placement: "bottom-start",
409
+ middleware: [
410
+ offset(8),
411
+ flip(),
412
+ shift(),
413
+ size({
414
+ padding: 20,
415
+ apply({
416
+ availableWidth,
417
+ availableHeight,
418
+ elements,
419
+ }: {
420
+ availableWidth: number;
421
+ availableHeight: number;
422
+ elements: {
423
+ floating: {
424
+ style: {
425
+ maxWidth: string;
426
+ maxHeight: string;
427
+ };
428
+ };
429
+ };
430
+ }) {
431
+ Object.assign(elements.floating.style, {
432
+ maxWidth: `${availableWidth}px`,
433
+ maxHeight: `${availableHeight}px`,
434
+ });
435
+ },
436
+ }),
437
+ ],
438
+ }).then(({ x, y }) => {
439
+ if (picker.value) {
440
+ Object.assign(picker.value.style, {
441
+ left: `${x}px`,
442
+ top: `${y}px`,
443
+ });
444
+ }
445
+ });
446
+ });
447
+ };
448
+
449
+ const handleOverlayClick = () => {
450
+ endEditing();
451
+ };
452
+
453
+ // Handle query updates from v-model
454
+ const updateQuery = (value: string) => {
455
+ emits("update:query", value);
456
+ };
457
+
458
+ // End editing if the user tries to resize the window
459
+ const windowResize = function () {
460
+ if (showPopover.value) {
461
+ endEditing();
462
+ }
463
+ if (container.value) {
464
+ componentWidth.value = container.value.offsetWidth;
465
+ componentHeight.value = container.value.offsetHeight;
466
+ }
467
+ };
468
+
469
+ const controlContainerResize = () => {
470
+ if (controlContainer.value) {
471
+ controlWidth.value = controlContainer.value.offsetWidth;
472
+ }
473
+ };
474
+
475
+ window.addEventListener("resize", windowResize);
476
+
477
+ // Watch for external changes to showPopover to handle edge cases
478
+ watch(showPopover, (newVal) => {
479
+ if (!newVal) {
480
+ emits("update:matchIndex", -1);
481
+ }
482
+ });
483
+
484
+ const getBorderRadius = computed(() => {
485
+ if (props.overflow === "full") {
486
+ return "0.471rem";
487
+ }
488
+ if (componentHeight.value > 50) {
489
+ return "0.471rem";
490
+ }
491
+ return "0.471rem";
492
+ });
493
+
494
+ const themeConfig = computed(() => {
495
+ if (useTheme()) {
496
+ return useTheme();
497
+ }
498
+ return { customThemeColorMode: "light" };
499
+ });
500
+ </script>
501
+
502
+ <template>
503
+ <div
504
+ :class="[
505
+ $style.container,
506
+ {
507
+ [$style.container_full]: overflow === 'full',
508
+ [$style.container_fixed]: overflow === 'fixed',
509
+ [$style.container_list]: overflow === 'list',
510
+ [$style.container_raised]: showPopover,
511
+ },
512
+ ]"
513
+ ref="container"
514
+ >
515
+ <div v-if="allowEditing" :class="$style.controls" ref="controlContainer">
516
+ <div :class="$style.add_button" ref="button">
517
+ <IconButton
518
+ iconName="plus"
519
+ buttonType="square"
520
+ buttonStyle="secondary"
521
+ :buttonSize="10"
522
+ @click="beginEditing"
523
+ />
524
+ <p
525
+ v-if="selectedTags.length === 0"
526
+ :class="[$style.add_label, 'caption']"
527
+ @click="beginEditing"
528
+ ref="label"
529
+ >
530
+ {{ placeholder }}
531
+ </p>
532
+ </div>
533
+ <input
534
+ :class="[$style.field, 'callout']"
535
+ type="text"
536
+ ref="field"
537
+ :style="{ width: `${fieldWidth}px` }"
538
+ :value="query"
539
+ @input="onInput"
540
+ @keydown="onKeydown"
541
+ @keydown.enter="onInputReturn"
542
+ @keydown.escape="endEditing"
543
+ />
544
+ </div>
545
+ <div
546
+ :class="[
547
+ $style.tag_list,
548
+ {
549
+ [$style.tag_list_full]: overflow === 'full',
550
+ [$style.tag_list_fixed]: overflow === 'fixed',
551
+ [$style.tag_list_list]: overflow === 'list',
552
+ },
553
+ ]"
554
+ v-if="selectedTags.length > 0"
555
+ >
556
+ <div
557
+ v-for="tag in selectedTags"
558
+ :key="tag.title"
559
+ :id="tag.title"
560
+ :class="$style.tag"
561
+ :style="{
562
+ ...getTagStyle(tag).container,
563
+ pointerEvents: allowEditing ? 'auto' : 'none',
564
+ }"
565
+ @click="onTagClick(tag)"
566
+ >
567
+ <p class="callout" :style="{ color: getTagStyle(tag).container.color }">
568
+ {{ tag.title }}
569
+ </p>
570
+ </div>
571
+ </div>
572
+ </div>
573
+
574
+ <!-- Teleport the overlay and picker to body -->
575
+ <Teleport to="body">
576
+ <div
577
+ v-if="showPopover"
578
+ :class="$style.overlay"
579
+ ref="overlay"
580
+ @click="handleOverlayClick"
581
+ ></div>
582
+ <div v-if="showPopover" :class="$style.picker" ref="picker">
583
+ <TagPicker
584
+ :all-tags="allTags"
585
+ :selected-tags="selectedTags"
586
+ :query="query"
587
+ :match-index="matchIndex"
588
+ :can-create-tag="canCreateTag"
589
+ @tag-select="(tag) => emits('tag-select', tag)"
590
+ @tag-create="(title) => emits('tag-create', title)"
591
+ @update:query="(value) => emits('update:query', value)"
592
+ @update:match-index="(index) => emits('update:matchIndex', index)"
593
+ @quit="endEditing"
594
+ />
595
+ </div>
596
+ </Teleport>
597
+ </template>
598
+
599
+ <style module>
600
+ /* Container - use CSS Grid for better control */
601
+ .container {
602
+ display: grid;
603
+ align-items: center;
604
+ position: relative;
605
+ overflow: hidden;
606
+ background-color: var(--tagbar-bg);
607
+ border-radius: 0.471rem;
608
+ border: var(--tagbar-border);
609
+ }
610
+
611
+ .container_raised {
612
+ z-index: 1001;
613
+ }
614
+
615
+ /* Full overflow mode - use grid with minmax */
616
+ .container_full {
617
+ width: 100%;
618
+ /* Grid template: controls take their content size, tag list takes remaining space */
619
+ grid-template-columns: auto minmax(0, 1fr);
620
+ grid-template-rows: 1fr;
621
+ }
622
+
623
+ /* Fixed overflow mode - container uses assigned width */
624
+ .container_fixed {
625
+ width: 100%;
626
+ display: flex;
627
+ flex-wrap: wrap;
628
+ align-items: flex-start;
629
+ }
630
+
631
+ /* List overflow mode - container uses assigned width */
632
+ .container_list {
633
+ width: 100%;
634
+ display: flex;
635
+ flex-wrap: wrap;
636
+ align-items: flex-start;
637
+ }
638
+
639
+ /* Controls */
640
+ .controls {
641
+ display: grid;
642
+ grid-template-columns: 1fr;
643
+ grid-template-rows: 1fr;
644
+ grid-template-areas: "content";
645
+ align-items: center;
646
+ padding-top: 0.471rem;
647
+ padding-bottom: 0.471rem;
648
+ padding-left: 0.471rem;
649
+ /* No flex-shrink needed in grid context */
650
+ }
651
+
652
+ /* Fixed overflow mode - controls take full width */
653
+ .container_fixed .controls {
654
+ width: 100%;
655
+ flex-basis: 100%;
656
+ order: 2;
657
+ }
658
+
659
+ /* List overflow mode - controls take full width */
660
+ .container_list .controls {
661
+ width: 100%;
662
+ flex-basis: 100%;
663
+ order: 2;
664
+ }
665
+
666
+ .add_button {
667
+ grid-area: content;
668
+ display: flex;
669
+ align-items: center;
670
+ gap: 0.588rem;
671
+ justify-content: start;
672
+ }
673
+
674
+ .add_label {
675
+ user-select: none;
676
+ color: var(--tagbar-text);
677
+ }
678
+
679
+ .field {
680
+ background-color: var(--tagbar-field-bg);
681
+ color: var(--tagbar-field-text);
682
+ border: none;
683
+ border-radius: 999px;
684
+ min-height: 1.412rem;
685
+ padding-left: 0.471rem;
686
+ padding-right: 0.588rem;
687
+ grid-area: content;
688
+ opacity: 0;
689
+ user-select: none;
690
+ pointer-events: none;
691
+ }
692
+
693
+ .tag_list {
694
+ display: flex;
695
+ align-items: center;
696
+ justify-content: start;
697
+ gap: 0.588rem;
698
+ padding: 0.294rem 0;
699
+ }
700
+
701
+ /* Full overflow mode - horizontal scroll with grid */
702
+ .tag_list_full {
703
+ /* minmax(0, 1fr) in the grid parent ensures this won't grow beyond available space */
704
+ overflow-x: auto;
705
+ overflow-y: hidden;
706
+ padding-right: 0.588rem;
707
+ padding-left: 0.588rem;
708
+
709
+ /* Ensure the flex container inside doesn't wrap */
710
+ flex-wrap: nowrap;
711
+
712
+ /* Hide scrollbar if desired */
713
+ scrollbar-width: none; /* Firefox */
714
+ -ms-overflow-style: none; /* IE and Edge */
715
+ }
716
+
717
+ .tag_list_full::-webkit-scrollbar {
718
+ display: none; /* Chrome, Safari, Opera */
719
+ }
720
+
721
+ /* Fixed overflow mode - wrap tags */
722
+ .tag_list_fixed {
723
+ flex-wrap: wrap;
724
+ overflow: visible;
725
+ padding-left: 0.471rem;
726
+ padding-top: 0.471rem;
727
+ padding-right: 0.471rem;
728
+ width: 100%;
729
+ flex-basis: 100%;
730
+ order: 1;
731
+ }
732
+
733
+ /* List overflow mode - each tag on its own row */
734
+ .tag_list_list {
735
+ flex-direction: column;
736
+ align-items: flex-start;
737
+ gap: 0.588rem;
738
+ padding: 0.471rem;
739
+ width: 100%;
740
+ flex-basis: 100%;
741
+ order: 1;
742
+ border-bottom: 1px solid var(--tagbar-list-border);
743
+ }
744
+
745
+ .tag {
746
+ cursor: default;
747
+ user-select: none;
748
+ transition: scale 0.15s ease;
749
+ transform-origin: center;
750
+ flex-shrink: 0; /* Prevent tags from shrinking */
751
+ }
752
+
753
+ .tag_list_list .tag {
754
+ width: 100%;
755
+ border-bottom: 1px solid var(--tagbar-list-border);
756
+ }
757
+
758
+ .tag:hover {
759
+ scale: 1.05;
760
+ }
761
+
762
+ .tag_list_list .tag:hover {
763
+ scale: 1.01;
764
+ }
765
+
766
+ .tag p {
767
+ white-space: nowrap;
768
+ }
769
+
770
+ .overlay {
771
+ position: fixed;
772
+ top: 0;
773
+ left: 0;
774
+ width: 100%;
775
+ height: 100%;
776
+ background-color: var(--search-overlay-bg);
777
+ opacity: 0;
778
+ z-index: 999;
779
+ }
780
+
781
+ .picker {
782
+ position: absolute;
783
+ top: 0;
784
+ left: 0;
785
+ background-color: var(--tagpicker-container-bg);
786
+ border-radius: 0.353rem;
787
+ min-width: 18.824rem;
788
+ overflow: hidden;
789
+ z-index: 1000;
790
+ box-shadow: 0px 1px 0px 0px var(--tagpicker-container-shadow),
791
+ inset 0px 1px 0px 0px var(--tagpicker-container-inset-shadow);
792
+ }
793
+ </style>