@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,394 @@
1
+ <!-- SearchBar.vue -->
2
+ <script setup lang="ts">
3
+ import { MagnifierIcon } from "@umbra-ui/icons";
4
+ import { onMounted, ref, nextTick, onUnmounted, computed } from "vue";
5
+ import { gsap } from "gsap";
6
+ import {
7
+ offset,
8
+ flip,
9
+ shift,
10
+ size,
11
+ computePosition,
12
+ autoUpdate,
13
+ } from "@floating-ui/vue";
14
+ import SearchResults from "./SearchResults.vue";
15
+ import { type SearchResult } from "./types";
16
+ import "./theme.css";
17
+
18
+ // Props
19
+ interface Props {
20
+ type: string;
21
+ query: string;
22
+ items: SearchResult[];
23
+ placeholder?: string;
24
+ }
25
+
26
+ const props = withDefaults(defineProps<Props>(), {
27
+ type: "items",
28
+ query: "",
29
+ items: () => [],
30
+ placeholder: "Search",
31
+ });
32
+
33
+ // Emits
34
+ const emits = defineEmits<{
35
+ "update:query": [query: string];
36
+ onSelect: [result: SearchResult];
37
+ onFocus: [];
38
+ }>();
39
+
40
+ // Computed properties
41
+ const matches = computed(() => {
42
+ if (!props.query.trim()) {
43
+ return props.items;
44
+ }
45
+
46
+ const query = props.query.toLowerCase();
47
+ return props.items.filter((item) => {
48
+ const searchText =
49
+ `${item.title} ${item.caption} ${item.description} ${item.footnote}`.toLowerCase();
50
+ return searchText.includes(query);
51
+ });
52
+ });
53
+
54
+ const onSelect = (result: SearchResult) => {
55
+ endSearch();
56
+ emits("onSelect", result);
57
+ };
58
+
59
+ onMounted(() => {
60
+ // Initialize matches with all items
61
+ if (props.items.length > 0) {
62
+ // This will be handled by the computed property now
63
+ }
64
+ });
65
+
66
+ const onFocus = () => {
67
+ isFocused.value = true;
68
+ // Show picker on focus if there are items
69
+ if (props.items.length > 0) {
70
+ showPicker();
71
+ }
72
+ emits("onFocus");
73
+ };
74
+
75
+ const onInput = (e: Event) => {
76
+ const target = e.target as HTMLInputElement;
77
+ emits("update:query", target.value);
78
+
79
+ if (!isPopoverShowing.value) {
80
+ showPicker();
81
+ }
82
+ };
83
+
84
+ const onBlur = () => {
85
+ isFocused.value = false;
86
+ // Add a small delay to prevent the picker from being hidden immediately
87
+ setTimeout(() => {
88
+ if (!isPopoverShowing.value) {
89
+ endSearch();
90
+ }
91
+ }, 100);
92
+ };
93
+
94
+ const isFocused = ref<boolean>(false);
95
+ const isPopoverShowing = ref<boolean>(false);
96
+ const searchBar = ref<HTMLElement>();
97
+ const searchInput = ref<HTMLInputElement>();
98
+ const overlay = ref<HTMLElement>();
99
+ const picker = ref<HTMLElement>();
100
+
101
+ // Position tracking
102
+ let cleanupAutoUpdate: (() => void) | null = null;
103
+
104
+ onUnmounted(() => {
105
+ if (cleanupAutoUpdate) {
106
+ cleanupAutoUpdate();
107
+ }
108
+ });
109
+
110
+ const beginSearch = () => {
111
+ showPicker();
112
+ };
113
+
114
+ const endSearch = () => {
115
+ emits("update:query", "");
116
+ hidePicker();
117
+ if (searchInput.value) {
118
+ searchInput.value.blur();
119
+ }
120
+ };
121
+
122
+ // - Picker Management
123
+ const showPicker = async () => {
124
+ if (!searchBar.value) {
125
+ return;
126
+ }
127
+
128
+ isPopoverShowing.value = true;
129
+
130
+ await nextTick();
131
+
132
+ // Now the picker and overlay should be rendered, so we can access their refs
133
+ if (!picker.value || !overlay.value) {
134
+ return;
135
+ }
136
+
137
+ // Set up auto-update for positioning
138
+ if (cleanupAutoUpdate) {
139
+ cleanupAutoUpdate();
140
+ }
141
+
142
+ cleanupAutoUpdate = autoUpdate(searchBar.value, picker.value, () => {
143
+ computePosition(searchBar.value!, picker.value!, {
144
+ placement: "bottom-start",
145
+ middleware: [
146
+ offset(8),
147
+ flip(),
148
+ shift(),
149
+ size({
150
+ padding: 20,
151
+ apply({
152
+ availableWidth,
153
+ availableHeight,
154
+ elements,
155
+ }: {
156
+ availableWidth: number;
157
+ availableHeight: number;
158
+ elements: {
159
+ floating: {
160
+ style: {
161
+ maxWidth: string;
162
+ maxHeight: string;
163
+ };
164
+ };
165
+ };
166
+ }) {
167
+ Object.assign(elements.floating.style, {
168
+ maxWidth: `${availableWidth}px`,
169
+ maxHeight: `${availableHeight}px`,
170
+ });
171
+ },
172
+ }),
173
+ ],
174
+ }).then(({ x, y }) => {
175
+ if (picker.value) {
176
+ Object.assign(picker.value.style, {
177
+ left: `${x}px`,
178
+ top: `${y}px`,
179
+ });
180
+ }
181
+ });
182
+ });
183
+
184
+ // Animate the picker
185
+ if (picker.value) {
186
+ picker.value.style.display = "block";
187
+ gsap.fromTo(
188
+ picker.value,
189
+ {
190
+ opacity: 0,
191
+ scale: 0.95,
192
+ y: -8,
193
+ },
194
+ {
195
+ duration: 0.2,
196
+ opacity: 1,
197
+ scale: 1,
198
+ y: 0,
199
+ ease: "ease-out",
200
+ }
201
+ );
202
+ }
203
+
204
+ // Animate the overlay
205
+ if (overlay.value) {
206
+ overlay.value.style.display = "block";
207
+ gsap.to(overlay.value, {
208
+ duration: 0.2,
209
+ opacity: 0.7,
210
+ ease: "ease-out",
211
+ });
212
+ }
213
+ };
214
+
215
+ const hidePicker = () => {
216
+ if (!searchBar.value || !picker.value || !overlay.value) return;
217
+
218
+ isPopoverShowing.value = false;
219
+
220
+ // Clean up auto-update
221
+ if (cleanupAutoUpdate) {
222
+ cleanupAutoUpdate();
223
+ cleanupAutoUpdate = null;
224
+ }
225
+
226
+ // Animate the overlay
227
+ if (overlay.value) {
228
+ gsap
229
+ .to(overlay.value, {
230
+ duration: 0.2,
231
+ opacity: 0,
232
+ ease: "ease-out",
233
+ })
234
+ .then(() => {
235
+ if (overlay.value) {
236
+ overlay.value.style.display = "none";
237
+ }
238
+ });
239
+ }
240
+
241
+ // Animate the picker
242
+ if (picker.value) {
243
+ gsap
244
+ .to(picker.value, {
245
+ duration: 0.2,
246
+ opacity: 0,
247
+ scale: 0.95,
248
+ y: -8,
249
+ ease: "ease-out",
250
+ })
251
+ .then(() => {
252
+ if (picker.value) {
253
+ picker.value.style.display = "none";
254
+ }
255
+ });
256
+ }
257
+ };
258
+
259
+ const handleOverlayClick = () => {
260
+ endSearch();
261
+ };
262
+ </script>
263
+ //
264
+ ================================================================================================
265
+ // TEMPLATE //
266
+ ================================================================================================
267
+ <template>
268
+ <div
269
+ :class="[$style.container, { [$style.elevated]: isPopoverShowing }]"
270
+ ref="searchBar"
271
+ >
272
+ <div :class="$style.search">
273
+ <MagnifierIcon size="16" />
274
+ <input
275
+ type="text"
276
+ :placeholder="props.placeholder"
277
+ :class="[$style.field, 'callout']"
278
+ @focus="onFocus"
279
+ @blur="onBlur"
280
+ @keydown.escape="endSearch"
281
+ ref="searchInput"
282
+ :value="props.query"
283
+ @input="onInput"
284
+ />
285
+ </div>
286
+
287
+ <!-- Teleport the overlay and picker to body -->
288
+ <Teleport to="body">
289
+ <div
290
+ v-if="isPopoverShowing"
291
+ :class="$style.overlay"
292
+ ref="overlay"
293
+ @click="handleOverlayClick"
294
+ ></div>
295
+ <div v-if="isPopoverShowing" :class="$style.picker" ref="picker">
296
+ <SearchResults
297
+ :type="props.type"
298
+ :query="props.query"
299
+ :matches="matches"
300
+ @onSelect="onSelect"
301
+ />
302
+ </div>
303
+ </Teleport>
304
+ </div>
305
+ </template>
306
+ //
307
+ ================================================================================================
308
+ // STYLE //
309
+ ================================================================================================
310
+ <style module>
311
+ .container {
312
+ display: flex;
313
+ flex-direction: column;
314
+ gap: 0.706rem;
315
+ position: relative;
316
+ }
317
+
318
+ .elevated {
319
+ z-index: 1001;
320
+ }
321
+
322
+ .search {
323
+ background-color: var(--search-bar-bg);
324
+ min-height: 2rem;
325
+ border-radius: 0.471rem;
326
+ padding-left: 0.706rem;
327
+ padding-right: 0.706rem;
328
+ padding-top: 0.471rem;
329
+ padding-bottom: 0.471rem;
330
+ display: flex;
331
+ align-items: center;
332
+ justify-content: start;
333
+ gap: 0.471rem;
334
+ border: var(--search-bar-border);
335
+ transition: border 0.2s ease-out, background-color 0.2s ease-out;
336
+ box-shadow: 0px 1px 0px 0px var(--search-bar-shadow),
337
+ inset 0px 1px 0px 0px var(--search-bar-inset-shadow);
338
+ }
339
+ .search:has(.field:focus) {
340
+ border: 1px solid var(--search-bar-focus-border);
341
+ /* background-color: var(--text-2); */
342
+ }
343
+ .field {
344
+ background-color: transparent;
345
+ color: var(--search-bar-text);
346
+ border: none;
347
+ width: 100%;
348
+ }
349
+ .field::placeholder {
350
+ color: var(--search-bar-placeholder);
351
+ }
352
+ .search input:focus {
353
+ outline: none;
354
+ }
355
+ /*
356
+ .search:has(.field:focus) .field {
357
+ color: black;
358
+ }
359
+ .search:has(.field:focus) svg {
360
+ color: black;
361
+ }
362
+ */
363
+
364
+ .picker {
365
+ position: absolute;
366
+ top: 0;
367
+ left: 0;
368
+ display: none;
369
+ opacity: 0;
370
+ scale: 0.95;
371
+ z-index: 1000;
372
+ }
373
+
374
+ .overlay {
375
+ position: fixed;
376
+ top: 0;
377
+ left: 0;
378
+ width: 100%;
379
+ height: 100%;
380
+ background-color: var(--search-overlay-bg);
381
+ opacity: 0;
382
+ z-index: 999;
383
+ animation: overlayFadeIn 0.7s ease-out;
384
+ }
385
+
386
+ @keyframes overlayFadeIn {
387
+ from {
388
+ opacity: 0;
389
+ }
390
+ to {
391
+ opacity: 0.7;
392
+ }
393
+ }
394
+ </style>
@@ -0,0 +1,310 @@
1
+ <!-- SearchResults.vue -->
2
+ <script setup lang="ts">
3
+ import { SearchResult } from "./types";
4
+ import { CircleAnim32GlyphIcon, ChevronDownIcon } from "@umbra-ui/icons";
5
+ import { computed, ref, watch } from "vue";
6
+ import "./theme.css";
7
+
8
+ // Props
9
+ interface Props {
10
+ type: string;
11
+ query: string;
12
+ matches: SearchResult[];
13
+ }
14
+
15
+ const props = defineProps<Props>();
16
+
17
+ const emits = defineEmits<{
18
+ onSelect: [result: SearchResult];
19
+ }>();
20
+
21
+ const filtered = computed(() => {
22
+ return props.matches.slice(0, page.value);
23
+ });
24
+
25
+ const loading = computed(() => {
26
+ return props.matches.length === 0 ? true : false;
27
+ });
28
+
29
+ const pageValue: number = 10;
30
+ const page = ref<number>(pageValue);
31
+
32
+ const canLoadMore = computed(() => {
33
+ return props.matches.length > filtered.value.length ? true : false;
34
+ });
35
+
36
+ watch(
37
+ () => props.query,
38
+ (newValue) => {
39
+ page.value = pageValue;
40
+ }
41
+ );
42
+
43
+ const onLoadMore = () => {
44
+ page.value += pageValue;
45
+ };
46
+
47
+ const highlight = (description: string) => {
48
+ // make sure there is actually something to highlight first
49
+ if (description?.trim() === "" || description === undefined) {
50
+ return "";
51
+ }
52
+
53
+ // Escape special characters in the query for use in the regular expression
54
+ const escapedQuery = props.query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
55
+
56
+ // Create a regular expression with the escaped query and set it to be global and case-insensitive
57
+ const regex = new RegExp(`(${escapedQuery})`, "gi");
58
+
59
+ const filtered = filter(description);
60
+
61
+ // Replace occurrences of the query with highlighted spans
62
+ const highlightedDescription = filtered.replace(
63
+ regex,
64
+ '<span style="background-color: var(--search-highlight-bg);">$1</span>'
65
+ );
66
+
67
+ return highlightedDescription;
68
+ };
69
+
70
+ const onSelect = (result: SearchResult) => {
71
+ emits("onSelect", result);
72
+ };
73
+
74
+ const filter = (description: string) => {
75
+ const minLength = 300;
76
+ const distance = 280;
77
+ const query = props.query;
78
+ const firstIndex = description.indexOf(query);
79
+ const lastIndex = description.lastIndexOf(query);
80
+
81
+ if (description.length < minLength) {
82
+ return description;
83
+ } else {
84
+ const startIndex = Math.max(0, firstIndex - distance);
85
+ const endIndex = Math.min(
86
+ description.length,
87
+ lastIndex + query.length + distance
88
+ );
89
+ return description.substring(startIndex, endIndex);
90
+ }
91
+ };
92
+ </script>
93
+ //
94
+ ================================================================================================
95
+ // TEMPLATE //
96
+ ================================================================================================
97
+ <template>
98
+ <div :class="$style.container">
99
+ <div :class="$style.subheader">
100
+ <div :class="$style.query">
101
+ <p class="body">Searching all {{ props.type }} for</p>
102
+ <p class="body">"{{ props.query }}"</p>
103
+ </div>
104
+ <div :class="$style.right">
105
+ <p v-if="loading" class="subheadline">building search index</p>
106
+ <CircleAnim32GlyphIcon v-if="loading" size="16" />
107
+ <p v-if="!loading" class="body">
108
+ {{ props.matches.length }} results found
109
+ </p>
110
+ </div>
111
+ </div>
112
+ <div :class="$style.result_list">
113
+ <div
114
+ v-for="item in filtered"
115
+ :key="item.id"
116
+ :class="$style.result"
117
+ @click="onSelect(item)"
118
+ >
119
+ <div v-if="item.icon" :class="$style.icon">
120
+ <span
121
+ v-if="
122
+ item.icon.startsWith('http') ||
123
+ item.icon.startsWith('data:') ||
124
+ item.icon.endsWith('.svg') ||
125
+ item.icon.endsWith('.png') ||
126
+ item.icon.endsWith('.jpg') ||
127
+ item.icon.endsWith('.jpeg') ||
128
+ item.icon.endsWith('.gif')
129
+ "
130
+ >
131
+ <img :src="item.icon" alt="" />
132
+ </span>
133
+ <span v-else :class="$style.emoji">{{ item.icon }}</span>
134
+ </div>
135
+ <div :class="$style.labels">
136
+ <p
137
+ v-if="item.title"
138
+ :class="['headline', $style.title]"
139
+ v-html="highlight(item.title)"
140
+ ></p>
141
+ <p
142
+ v-if="item.description"
143
+ :class="['subheadline', $style.description]"
144
+ v-html="highlight(item.description)"
145
+ ></p>
146
+ <p
147
+ v-if="item.caption"
148
+ :class="['subheadline', $style.caption]"
149
+ v-html="highlight(item.caption)"
150
+ ></p>
151
+ <p
152
+ v-if="item.footnote"
153
+ :class="['caption', $style.footnote]"
154
+ v-html="highlight(item.footnote)"
155
+ ></p>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ <div v-if="props.matches.length === 0" :class="$style.nothing">
160
+ <p>Nothing Found</p>
161
+ </div>
162
+ <div v-if="canLoadMore" :class="$style.more" @click="onLoadMore">
163
+ <p class="headline">Load More Results</p>
164
+ <ChevronDownIcon size="12" />
165
+ </div>
166
+ </div>
167
+ </template>
168
+ //
169
+ ================================================================================================
170
+ // STYLE //
171
+ ================================================================================================
172
+ <style module>
173
+ .container {
174
+ background-color: var(--search-results-bg);
175
+ border-radius: 0.353rem;
176
+ width: 41.176rem;
177
+ max-height: 41.176rem;
178
+ overflow-y: auto;
179
+ z-index: 1000;
180
+ box-shadow: 0px 1px 0px 0px var(--search-results-shadow),
181
+ inset 0px 1px 0px 0px var(--search-results-inset-shadow);
182
+ border: var(--search-results-border);
183
+ }
184
+ .subheader {
185
+ background-color: var(--search-subheader-bg);
186
+ border-bottom: 1px solid var(--search-subheader-border);
187
+ padding-top: 0.412rem;
188
+ padding-bottom: 0.412rem;
189
+ padding-left: 0.882rem;
190
+ padding-right: 0.882rem;
191
+ display: flex;
192
+ align-items: center;
193
+ justify-content: space-between;
194
+ flex-wrap: wrap;
195
+ position: sticky;
196
+ top: 0;
197
+ z-index: 1;
198
+ color: var(--search-subheader-text);
199
+ }
200
+ .right {
201
+ display: flex;
202
+ align-items: center;
203
+ gap: 0.471rem;
204
+ }
205
+ .right p {
206
+ opacity: 0.5;
207
+ }
208
+ .query {
209
+ display: flex;
210
+ align-items: center;
211
+ justify-content: start;
212
+ gap: 0.471rem;
213
+ }
214
+ .query > :first-child {
215
+ opacity: 0.5;
216
+ }
217
+ .query > :last-child {
218
+ font-weight: bold;
219
+ }
220
+ .result_list {
221
+ display: flex;
222
+ flex-direction: column;
223
+ gap: 1px;
224
+ }
225
+ .result_list > :last-child {
226
+ border-bottom: 0;
227
+ }
228
+ .result {
229
+ display: flex;
230
+ align-items: center;
231
+ gap: 0.882rem;
232
+ padding: 0.882rem;
233
+ border-bottom: 1px solid var(--search-result-border);
234
+ background-color: var(--search-result-bg);
235
+ }
236
+ .result:hover {
237
+ background-color: var(--search-result-hover-bg);
238
+ }
239
+ .labels {
240
+ display: flex;
241
+ flex-direction: column;
242
+ align-items: start;
243
+ gap: 0.412rem;
244
+ color: var(--search-result-text);
245
+ }
246
+ .icon {
247
+ width: 4.1176rem;
248
+ height: 4.1176rem;
249
+ background-color: var(--search-icon-bg);
250
+ border-radius: 0.4706rem;
251
+ border: 1px solid var(--search-icon-border);
252
+ display: flex;
253
+ align-items: center;
254
+ justify-content: center;
255
+ }
256
+
257
+ .icon img {
258
+ width: 100%;
259
+ height: 100%;
260
+ object-fit: cover;
261
+ border-radius: 0.4706rem;
262
+ }
263
+
264
+ .emoji {
265
+ font-size: 2rem;
266
+ line-height: 1;
267
+ }
268
+ .title {
269
+ opacity: 1;
270
+ }
271
+ .description {
272
+ opacity: 0.5;
273
+ }
274
+ .caption {
275
+ opacity: 0.5;
276
+ }
277
+ .footnote {
278
+ padding-left: 0.588rem;
279
+ padding-right: 0.588rem;
280
+ padding-top: 0.235rem;
281
+ padding-bottom: 0.235rem;
282
+ background-color: var(--search-footnote-bg);
283
+ border-radius: 999px;
284
+ font-weight: 600;
285
+ }
286
+ .nothing {
287
+ min-height: 10.235rem;
288
+ display: flex;
289
+ align-items: center;
290
+ justify-content: center;
291
+ opacity: 0.5;
292
+ color: var(--search-nothing-text);
293
+ }
294
+ .more {
295
+ background-color: var(--search-load-more-bg);
296
+ color: var(--search-load-more-text);
297
+ padding: 0.882rem;
298
+ display: flex;
299
+ align-items: center;
300
+ justify-content: center;
301
+ gap: 0.471rem;
302
+ cursor: default;
303
+ }
304
+ .more img {
305
+ height: 1.176rem;
306
+ }
307
+ .more:hover {
308
+ background-color: var(--search-load-more-hover-bg);
309
+ }
310
+ </style>