@sabrenski/spire-ui 0.0.1

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 (237) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/dist/index.d.ts +4981 -0
  4. package/dist/spire-ui.css +1 -0
  5. package/dist/spire-ui.es.js +18403 -0
  6. package/dist/spire-ui.umd.js +45 -0
  7. package/package.json +83 -0
  8. package/src/components/Accordion/Accordion.test.ts +218 -0
  9. package/src/components/Accordion/AccordionContent.vue +112 -0
  10. package/src/components/Accordion/AccordionItem.vue +87 -0
  11. package/src/components/Accordion/AccordionRoot.vue +111 -0
  12. package/src/components/Accordion/AccordionTrigger.vue +125 -0
  13. package/src/components/Accordion/index.ts +11 -0
  14. package/src/components/Accordion/keys.ts +23 -0
  15. package/src/components/Avatar/Avatar.test.ts +181 -0
  16. package/src/components/Avatar/Avatar.vue +150 -0
  17. package/src/components/Avatar/index.ts +2 -0
  18. package/src/components/Badge/Badge.test.ts +141 -0
  19. package/src/components/Badge/Badge.vue +133 -0
  20. package/src/components/Badge/index.ts +2 -0
  21. package/src/components/BadgeContainer/BadgeContainer.test.ts +150 -0
  22. package/src/components/BadgeContainer/BadgeContainer.vue +90 -0
  23. package/src/components/BadgeContainer/index.ts +2 -0
  24. package/src/components/Breadcrumb/Breadcrumb.test.ts +342 -0
  25. package/src/components/Breadcrumb/BreadcrumbEllipsis.vue +96 -0
  26. package/src/components/Breadcrumb/BreadcrumbItem.vue +16 -0
  27. package/src/components/Breadcrumb/BreadcrumbLink.vue +67 -0
  28. package/src/components/Breadcrumb/BreadcrumbList.vue +20 -0
  29. package/src/components/Breadcrumb/BreadcrumbPage.vue +25 -0
  30. package/src/components/Breadcrumb/BreadcrumbRoot.vue +41 -0
  31. package/src/components/Breadcrumb/BreadcrumbSeparator.vue +63 -0
  32. package/src/components/Breadcrumb/index.ts +13 -0
  33. package/src/components/Breadcrumb/keys.ts +7 -0
  34. package/src/components/Button/Button.test.ts +231 -0
  35. package/src/components/Button/Button.vue +349 -0
  36. package/src/components/Button/index.ts +2 -0
  37. package/src/components/Callout/Callout.test.ts +260 -0
  38. package/src/components/Callout/Callout.vue +341 -0
  39. package/src/components/Callout/index.ts +2 -0
  40. package/src/components/Card/Card.test.ts +565 -0
  41. package/src/components/Card/Card.vue +209 -0
  42. package/src/components/Card/CardContent.vue +57 -0
  43. package/src/components/Card/CardFooter.vue +72 -0
  44. package/src/components/Card/CardHeader.vue +111 -0
  45. package/src/components/Card/CardImage.vue +124 -0
  46. package/src/components/Card/index.ts +14 -0
  47. package/src/components/Chart/BarChart.vue +208 -0
  48. package/src/components/Chart/BaseChart.vue +444 -0
  49. package/src/components/Chart/Chart.test.ts +359 -0
  50. package/src/components/Chart/DonutChart.vue +283 -0
  51. package/src/components/Chart/LineChart.vue +211 -0
  52. package/src/components/Chart/index.ts +20 -0
  53. package/src/components/Chart/useChartTheme.ts +192 -0
  54. package/src/components/Checkbox/Checkbox.test.ts +209 -0
  55. package/src/components/Checkbox/Checkbox.vue +285 -0
  56. package/src/components/Checkbox/index.ts +2 -0
  57. package/src/components/ChoiceChip/ChoiceChip.test.ts +142 -0
  58. package/src/components/ChoiceChip/ChoiceChip.vue +218 -0
  59. package/src/components/ChoiceChip/index.ts +2 -0
  60. package/src/components/ChoiceChipGroup/ChoiceChipGroup.test.ts +151 -0
  61. package/src/components/ChoiceChipGroup/ChoiceChipGroup.vue +70 -0
  62. package/src/components/ChoiceChipGroup/index.ts +2 -0
  63. package/src/components/ColorPicker/ColorArea.vue +159 -0
  64. package/src/components/ColorPicker/ColorPicker.test.ts +250 -0
  65. package/src/components/ColorPicker/ColorPicker.vue +339 -0
  66. package/src/components/ColorPicker/ColorSlider.vue +191 -0
  67. package/src/components/ColorPicker/index.ts +7 -0
  68. package/src/components/Combobox/Combobox.test.ts +891 -0
  69. package/src/components/Combobox/Combobox.vue +934 -0
  70. package/src/components/Combobox/index.ts +2 -0
  71. package/src/components/DataTable/DataTable.test.ts +1221 -0
  72. package/src/components/DataTable/DataTable.vue +1415 -0
  73. package/src/components/DataTable/index.ts +10 -0
  74. package/src/components/DatePicker/DatePicker.test.ts +625 -0
  75. package/src/components/DatePicker/DatePicker.vue +1586 -0
  76. package/src/components/DatePicker/index.ts +2 -0
  77. package/src/components/Drawer/Drawer.test.ts +336 -0
  78. package/src/components/Drawer/Drawer.vue +466 -0
  79. package/src/components/Drawer/index.ts +2 -0
  80. package/src/components/Dropdown/Dropdown.test.ts +607 -0
  81. package/src/components/Dropdown/Dropdown.vue +807 -0
  82. package/src/components/Dropdown/DropdownItem.vue +227 -0
  83. package/src/components/Dropdown/DropdownSeparator.vue +14 -0
  84. package/src/components/Dropdown/DropdownSub.vue +104 -0
  85. package/src/components/Dropdown/DropdownSubContent.vue +187 -0
  86. package/src/components/Dropdown/DropdownSubTrigger.vue +151 -0
  87. package/src/components/Dropdown/index.ts +14 -0
  88. package/src/components/EmptyState/EmptyState.test.ts +180 -0
  89. package/src/components/EmptyState/EmptyState.vue +137 -0
  90. package/src/components/EmptyState/index.ts +2 -0
  91. package/src/components/FileUpload/FileUpload.test.ts +1151 -0
  92. package/src/components/FileUpload/FileUpload.vue +1042 -0
  93. package/src/components/FileUpload/index.ts +2 -0
  94. package/src/components/Heading/Heading.test.ts +107 -0
  95. package/src/components/Heading/Heading.vue +67 -0
  96. package/src/components/Heading/index.ts +2 -0
  97. package/src/components/Icon/Icon.test.ts +157 -0
  98. package/src/components/Icon/Icon.vue +86 -0
  99. package/src/components/Icon/index.ts +2 -0
  100. package/src/components/Input/Input.test.ts +273 -0
  101. package/src/components/Input/Input.vue +388 -0
  102. package/src/components/Input/index.ts +2 -0
  103. package/src/components/Layout/Container.vue +67 -0
  104. package/src/components/Layout/Grid.vue +159 -0
  105. package/src/components/Layout/GridItem.vue +154 -0
  106. package/src/components/Layout/Layout.test.ts +202 -0
  107. package/src/components/Layout/Stack.vue +128 -0
  108. package/src/components/Layout/index.ts +9 -0
  109. package/src/components/Layout/keys.ts +7 -0
  110. package/src/components/Modal/Modal.test.ts +311 -0
  111. package/src/components/Modal/Modal.vue +336 -0
  112. package/src/components/Modal/index.ts +2 -0
  113. package/src/components/Pagination/Pagination.test.ts +303 -0
  114. package/src/components/Pagination/Pagination.vue +212 -0
  115. package/src/components/Pagination/index.ts +3 -0
  116. package/src/components/Pagination/utils.ts +86 -0
  117. package/src/components/Popover/Popover.test.ts +285 -0
  118. package/src/components/Popover/Popover.vue +441 -0
  119. package/src/components/Popover/index.ts +2 -0
  120. package/src/components/Progress/Progress.test.ts +361 -0
  121. package/src/components/Progress/Progress.vue +363 -0
  122. package/src/components/Progress/index.ts +7 -0
  123. package/src/components/Radio/Radio.test.ts +216 -0
  124. package/src/components/Radio/Radio.vue +214 -0
  125. package/src/components/Radio/index.ts +2 -0
  126. package/src/components/Rating/Rating.test.ts +319 -0
  127. package/src/components/Rating/Rating.vue +247 -0
  128. package/src/components/Rating/index.ts +2 -0
  129. package/src/components/SegmentedControl/SegmentedControl.test.ts +292 -0
  130. package/src/components/SegmentedControl/SegmentedControl.vue +288 -0
  131. package/src/components/SegmentedControl/index.ts +2 -0
  132. package/src/components/Select/Select.test.ts +589 -0
  133. package/src/components/Select/Select.vue +666 -0
  134. package/src/components/Select/index.ts +2 -0
  135. package/src/components/Sidebar/Sidebar.test.ts +301 -0
  136. package/src/components/Sidebar/SidebarGroup.vue +103 -0
  137. package/src/components/Sidebar/SidebarItem.vue +196 -0
  138. package/src/components/Sidebar/SidebarLayout.vue +42 -0
  139. package/src/components/Sidebar/SidebarRoot.vue +122 -0
  140. package/src/components/Sidebar/index.ts +11 -0
  141. package/src/components/Sidebar/keys.ts +14 -0
  142. package/src/components/Skeleton/Skeleton.test.ts +130 -0
  143. package/src/components/Skeleton/Skeleton.vue +104 -0
  144. package/src/components/Skeleton/index.ts +2 -0
  145. package/src/components/Slider/Slider.test.ts +416 -0
  146. package/src/components/Slider/Slider.vue +435 -0
  147. package/src/components/Slider/index.ts +2 -0
  148. package/src/components/Slider/utils.ts +91 -0
  149. package/src/components/Spinner/Spinner.test.ts +79 -0
  150. package/src/components/Spinner/Spinner.vue +159 -0
  151. package/src/components/Spinner/index.ts +2 -0
  152. package/src/components/SpireProvider/SpireProvider.vue +71 -0
  153. package/src/components/SpireProvider/index.ts +11 -0
  154. package/src/components/Stepper/Stepper.test.ts +221 -0
  155. package/src/components/Stepper/StepperContent.vue +51 -0
  156. package/src/components/Stepper/StepperItem.vue +89 -0
  157. package/src/components/Stepper/StepperRoot.vue +101 -0
  158. package/src/components/Stepper/StepperSeparator.vue +52 -0
  159. package/src/components/Stepper/StepperTrigger.vue +144 -0
  160. package/src/components/Stepper/index.ts +11 -0
  161. package/src/components/Stepper/keys.ts +27 -0
  162. package/src/components/Switch/Switch.test.ts +214 -0
  163. package/src/components/Switch/Switch.vue +235 -0
  164. package/src/components/Switch/index.ts +2 -0
  165. package/src/components/Tabs/Tabs.test.ts +363 -0
  166. package/src/components/Tabs/Tabs.vue +318 -0
  167. package/src/components/Tabs/index.ts +2 -0
  168. package/src/components/Text/Text.test.ts +154 -0
  169. package/src/components/Text/Text.vue +100 -0
  170. package/src/components/Text/index.ts +2 -0
  171. package/src/components/Textarea/Textarea.test.ts +432 -0
  172. package/src/components/Textarea/Textarea.vue +411 -0
  173. package/src/components/Textarea/index.ts +2 -0
  174. package/src/components/TimePicker/TimePicker.test.ts +352 -0
  175. package/src/components/TimePicker/TimePicker.vue +569 -0
  176. package/src/components/TimePicker/index.ts +2 -0
  177. package/src/components/Timeline/Timeline.test.ts +193 -0
  178. package/src/components/Timeline/Timeline.vue +111 -0
  179. package/src/components/Timeline/TimelineItem.vue +167 -0
  180. package/src/components/Timeline/index.ts +13 -0
  181. package/src/components/Timeline/keys.ts +21 -0
  182. package/src/components/Toast/ToastItem.test.ts +289 -0
  183. package/src/components/Toast/ToastItem.vue +370 -0
  184. package/src/components/Toast/ToastProvider.test.ts +158 -0
  185. package/src/components/Toast/ToastProvider.vue +181 -0
  186. package/src/components/Toast/index.ts +83 -0
  187. package/src/components/Toast/toastState.test.ts +165 -0
  188. package/src/components/Toast/toastState.ts +161 -0
  189. package/src/components/ToggleButton/ToggleButton.test.ts +166 -0
  190. package/src/components/ToggleButton/ToggleButton.vue +197 -0
  191. package/src/components/ToggleButton/index.ts +2 -0
  192. package/src/components/ToggleGroup/ToggleGroup.test.ts +181 -0
  193. package/src/components/ToggleGroup/ToggleGroup.vue +130 -0
  194. package/src/components/ToggleGroup/index.ts +2 -0
  195. package/src/components/Tooltip/Tooltip.test.ts +238 -0
  196. package/src/components/Tooltip/Tooltip.vue +217 -0
  197. package/src/components/Tooltip/index.ts +2 -0
  198. package/src/components/TreeView/TreeView.test.ts +357 -0
  199. package/src/components/TreeView/TreeView.vue +251 -0
  200. package/src/components/TreeView/TreeViewItem.vue +288 -0
  201. package/src/components/TreeView/index.ts +11 -0
  202. package/src/components/TreeView/keys.ts +35 -0
  203. package/src/composables/index.ts +12 -0
  204. package/src/composables/useClickOutside.ts +36 -0
  205. package/src/composables/useClipboard.ts +35 -0
  206. package/src/composables/useEventListener.ts +48 -0
  207. package/src/composables/useFocusTrap.ts +58 -0
  208. package/src/composables/useHoverReveal.ts +98 -0
  209. package/src/composables/useId.ts +10 -0
  210. package/src/composables/useMagnetic.ts +171 -0
  211. package/src/composables/useRelativePosition.ts +127 -0
  212. package/src/composables/useRipple.ts +146 -0
  213. package/src/composables/useScrollLock.ts +25 -0
  214. package/src/composables/useSpireConfig.ts +27 -0
  215. package/src/composables/useStagger.ts +224 -0
  216. package/src/config/icons.test.ts +115 -0
  217. package/src/config/icons.ts +170 -0
  218. package/src/index.ts +361 -0
  219. package/src/styles/depth.css +129 -0
  220. package/src/styles/effects.css +169 -0
  221. package/src/styles/fallback.css +152 -0
  222. package/src/styles/main.css +25 -0
  223. package/src/styles/mood.css +211 -0
  224. package/src/styles/motion.css +159 -0
  225. package/src/styles/reset.css +97 -0
  226. package/src/styles/theme.css +708 -0
  227. package/src/styles/tokens.css +183 -0
  228. package/src/utils/.gitkeep +0 -0
  229. package/src/utils/color.ts +277 -0
  230. package/src/utils/date.test.ts +522 -0
  231. package/src/utils/date.ts +380 -0
  232. package/src/utils/index.ts +23 -0
  233. package/src/utils/object.test.ts +80 -0
  234. package/src/utils/object.ts +25 -0
  235. package/src/utils/string.test.ts +64 -0
  236. package/src/utils/string.ts +32 -0
  237. package/src/utils/time.ts +156 -0
@@ -0,0 +1,288 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject, provide, ref, watch, onMounted, Transition, type Component } from 'vue'
3
+ import { TreeViewKey, TreeViewItemKey } from './keys'
4
+ import type { TreeNode } from './keys'
5
+ import { useInternalIcon } from '../../config/icons'
6
+
7
+ export interface TreeViewItemProps {
8
+ /** Node data */
9
+ node: TreeNode
10
+ }
11
+
12
+ const props = defineProps<TreeViewItemProps>()
13
+
14
+ const injectedTreeView = inject(TreeViewKey)
15
+ const injectedParentContext = inject(TreeViewItemKey)
16
+
17
+ if (!injectedTreeView || !injectedParentContext) {
18
+ throw new Error('TreeViewItem must be used within TreeView')
19
+ }
20
+
21
+ const treeView = injectedTreeView
22
+ const parentContext = injectedParentContext
23
+
24
+ const depth = parentContext.depth
25
+ const parentId = parentContext.parentId
26
+
27
+ const ExpandIcon = useInternalIcon('expand')
28
+ const CollapseIcon = useInternalIcon('collapse')
29
+
30
+ const localChildren = ref<TreeNode[] | undefined>(props.node.children)
31
+ const isLoading = computed(() => treeView.loadingIds.value.has(props.node.id))
32
+
33
+ const isSelected = computed(() => treeView.selectedIds.value.has(props.node.id))
34
+ const isExpanded = computed(() => treeView.expandedIds.value.has(props.node.id))
35
+ const isFocused = computed(() => treeView.focusedId.value === props.node.id)
36
+
37
+ const hasChildren = computed(() => {
38
+ if (props.node.isLeaf) return false
39
+ if (localChildren.value && localChildren.value.length > 0) return true
40
+ if (treeView.loadChildren && !props.node.isLeaf) return true
41
+ return false
42
+ })
43
+
44
+ onMounted(() => {
45
+ treeView.registerNode(props.node.id, parentId, hasChildren.value)
46
+ })
47
+
48
+ watch(hasChildren, (val) => {
49
+ treeView.registerNode(props.node.id, parentId, val)
50
+ })
51
+
52
+ async function handleToggleExpand() {
53
+ if (!hasChildren.value) return
54
+
55
+ if (!isExpanded.value && treeView.loadChildren && !localChildren.value?.length) {
56
+ treeView.loadingIds.value.add(props.node.id)
57
+ try {
58
+ localChildren.value = await treeView.loadChildren(props.node)
59
+ } finally {
60
+ treeView.loadingIds.value.delete(props.node.id)
61
+ }
62
+ }
63
+
64
+ treeView.toggleExpand(props.node.id)
65
+ }
66
+
67
+ function handleSelect() {
68
+ if (props.node.disabled) return
69
+ treeView.selectNode(props.node.id)
70
+ }
71
+
72
+ function handleClick() {
73
+ treeView.setFocusedId(props.node.id)
74
+ handleSelect()
75
+ }
76
+
77
+ function handleChevronClick(event: Event) {
78
+ event.stopPropagation()
79
+ treeView.setFocusedId(props.node.id)
80
+ handleToggleExpand()
81
+ }
82
+
83
+ provide(TreeViewItemKey, {
84
+ depth: depth + 1,
85
+ parentId: props.node.id
86
+ })
87
+
88
+ const itemClasses = computed(() => [
89
+ 'ui-treeview__item',
90
+ {
91
+ 'ui-treeview__item--selected': isSelected.value,
92
+ 'ui-treeview__item--focused': isFocused.value,
93
+ 'ui-treeview__item--disabled': props.node.disabled,
94
+ 'ui-treeview__item--loading': isLoading.value
95
+ }
96
+ ])
97
+
98
+ const contentStyle = computed(() => ({
99
+ paddingLeft: `calc(var(--tree-indent) * ${depth})`
100
+ }))
101
+ </script>
102
+
103
+ <template>
104
+ <div
105
+ :class="itemClasses"
106
+ role="treeitem"
107
+ :aria-selected="isSelected"
108
+ :aria-expanded="hasChildren ? isExpanded : undefined"
109
+ :aria-disabled="node.disabled"
110
+ :data-focused="isFocused"
111
+ >
112
+ <div
113
+ class="ui-treeview__content"
114
+ :style="contentStyle"
115
+ @click="handleClick"
116
+ >
117
+ <button
118
+ v-if="hasChildren"
119
+ class="ui-treeview__chevron"
120
+ :class="{ 'ui-treeview__chevron--expanded': isExpanded }"
121
+ type="button"
122
+ tabindex="-1"
123
+ :aria-label="isExpanded ? 'Collapse' : 'Expand'"
124
+ @click="handleChevronClick"
125
+ >
126
+ <component
127
+ :is="isExpanded ? CollapseIcon : ExpandIcon"
128
+ class="ui-treeview__chevron-icon"
129
+ />
130
+ </button>
131
+ <span v-else class="ui-treeview__chevron-placeholder" />
132
+
133
+ <span v-if="isLoading" class="ui-treeview__spinner" />
134
+
135
+ <component
136
+ v-if="node.icon && !isLoading"
137
+ :is="node.icon"
138
+ class="ui-treeview__icon"
139
+ />
140
+
141
+ <span class="ui-treeview__label">
142
+ <slot>{{ node.label }}</slot>
143
+ </span>
144
+ </div>
145
+
146
+ <Transition name="ui-treeview-expand">
147
+ <div
148
+ v-if="hasChildren && isExpanded && localChildren"
149
+ class="ui-treeview__children"
150
+ role="group"
151
+ >
152
+ <TreeViewItem
153
+ v-for="child in localChildren"
154
+ :key="child.id"
155
+ :node="child"
156
+ />
157
+ </div>
158
+ </Transition>
159
+ </div>
160
+ </template>
161
+
162
+ <style scoped>
163
+ .ui-treeview__item {
164
+ user-select: none;
165
+ }
166
+
167
+ .ui-treeview__content {
168
+ display: flex;
169
+ align-items: center;
170
+ gap: var(--space-2);
171
+ padding: var(--space-1) var(--space-2);
172
+ border-radius: var(--radius-md);
173
+ cursor: pointer;
174
+ transition: background-color 0.15s ease;
175
+ }
176
+
177
+ .ui-treeview__content:hover {
178
+ background: var(--bg-tertiary);
179
+ }
180
+
181
+ .ui-treeview__item--selected > .ui-treeview__content {
182
+ background: var(--status-info-bg);
183
+ }
184
+
185
+ .ui-treeview__item--selected > .ui-treeview__content:hover {
186
+ background: var(--status-info-bg);
187
+ }
188
+
189
+ .ui-treeview__item--focused > .ui-treeview__content {
190
+ outline: 2px solid var(--ring-color);
191
+ outline-offset: -2px;
192
+ }
193
+
194
+ .ui-treeview__item--disabled > .ui-treeview__content {
195
+ opacity: 0.5;
196
+ cursor: not-allowed;
197
+ }
198
+
199
+ .ui-treeview__chevron {
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ width: 1.25rem;
204
+ height: 1.25rem;
205
+ padding: 0;
206
+ background: transparent;
207
+ border: none;
208
+ border-radius: var(--radius-sm);
209
+ cursor: pointer;
210
+ color: var(--text-tertiary);
211
+ transition: color 0.15s ease, background-color 0.15s ease;
212
+ }
213
+
214
+ .ui-treeview__chevron:hover {
215
+ color: var(--text-primary);
216
+ background: var(--bg-secondary);
217
+ }
218
+
219
+ .ui-treeview__chevron-icon {
220
+ width: 1rem;
221
+ height: 1rem;
222
+ flex-shrink: 0;
223
+ transition: transform 0.15s ease;
224
+ }
225
+
226
+ .ui-treeview__chevron--expanded .ui-treeview__chevron-icon {
227
+ transform: rotate(180deg);
228
+ }
229
+
230
+ .ui-treeview__chevron-placeholder {
231
+ width: 1.25rem;
232
+ height: 1.25rem;
233
+ flex-shrink: 0;
234
+ }
235
+
236
+ .ui-treeview__spinner {
237
+ width: 1rem;
238
+ height: 1rem;
239
+ border: 2px solid var(--border-default);
240
+ border-top-color: var(--action-primary);
241
+ border-radius: var(--radius-full);
242
+ animation: spin 0.6s linear infinite;
243
+ }
244
+
245
+ @keyframes spin {
246
+ to { transform: rotate(360deg); }
247
+ }
248
+
249
+ .ui-treeview__icon {
250
+ width: 1rem;
251
+ height: 1rem;
252
+ flex-shrink: 0;
253
+ color: var(--text-tertiary);
254
+ }
255
+
256
+ .ui-treeview__label {
257
+ flex: 1;
258
+ font-size: var(--text-sm);
259
+ color: var(--text-primary);
260
+ white-space: nowrap;
261
+ overflow: hidden;
262
+ text-overflow: ellipsis;
263
+ }
264
+
265
+ .ui-treeview__children {
266
+ display: flex;
267
+ flex-direction: column;
268
+ overflow: hidden;
269
+ }
270
+
271
+ .ui-treeview-expand-enter-active,
272
+ .ui-treeview-expand-leave-active {
273
+ transition: opacity 0.15s ease, transform 0.15s ease;
274
+ transform-origin: top;
275
+ }
276
+
277
+ .ui-treeview-expand-enter-from,
278
+ .ui-treeview-expand-leave-to {
279
+ opacity: 0;
280
+ transform: scaleY(0.95) translateY(-4px);
281
+ }
282
+
283
+ .ui-treeview-expand-enter-to,
284
+ .ui-treeview-expand-leave-from {
285
+ opacity: 1;
286
+ transform: scaleY(1) translateY(0);
287
+ }
288
+ </style>
@@ -0,0 +1,11 @@
1
+ export { default as TreeView } from './TreeView.vue'
2
+ export { default as TreeViewItem } from './TreeViewItem.vue'
3
+
4
+ export type { TreeViewProps } from './TreeView.vue'
5
+ export type { TreeViewItemProps } from './TreeViewItem.vue'
6
+ export type {
7
+ TreeNode,
8
+ TreeViewContext,
9
+ TreeViewItemContext
10
+ } from './keys'
11
+ export { TreeViewKey, TreeViewItemKey } from './keys'
@@ -0,0 +1,35 @@
1
+ import type { InjectionKey, Ref, Component } from 'vue'
2
+
3
+ export interface TreeNode {
4
+ id: string
5
+ label: string
6
+ icon?: Component
7
+ children?: TreeNode[]
8
+ disabled?: boolean
9
+ isLeaf?: boolean
10
+ }
11
+
12
+ export interface TreeViewContext {
13
+ selectedIds: Ref<Set<string>>
14
+ expandedIds: Ref<Set<string>>
15
+ focusedId: Ref<string | null>
16
+ multiSelect: Ref<boolean>
17
+ selectNode: (id: string) => void
18
+ toggleExpand: (id: string) => void
19
+ setFocusedId: (id: string | null) => void
20
+ loadChildren: ((node: TreeNode) => Promise<TreeNode[]>) | null
21
+ loadingIds: Ref<Set<string>>
22
+ registerNode: (id: string, parentId: string | null, hasChildren: boolean) => void
23
+ getVisibleNodes: () => string[]
24
+ getParentId: (id: string) => string | null
25
+ getChildIds: (id: string) => string[]
26
+ hasChildren: (id: string) => boolean
27
+ }
28
+
29
+ export interface TreeViewItemContext {
30
+ depth: number
31
+ parentId: string | null
32
+ }
33
+
34
+ export const TreeViewKey: InjectionKey<TreeViewContext> = Symbol('treeview')
35
+ export const TreeViewItemKey: InjectionKey<TreeViewItemContext> = Symbol('treeview-item')
@@ -0,0 +1,12 @@
1
+ export { useClickOutside } from './useClickOutside'
2
+ export { useClipboard } from './useClipboard'
3
+ export { useEventListener } from './useEventListener'
4
+ export { useFocusTrap } from './useFocusTrap'
5
+ export { useHoverReveal, type UseHoverRevealOptions } from './useHoverReveal'
6
+ export { useId } from './useId'
7
+ export { useMagnetic, type UseMagneticOptions, type UseMagneticReturn } from './useMagnetic'
8
+ export { useRelativePosition, calculatePosition, type Placement, type PositionResult } from './useRelativePosition'
9
+ export { useRipple, type UseRippleOptions } from './useRipple'
10
+ export { useScrollLock } from './useScrollLock'
11
+ export { useSpireConfig } from './useSpireConfig'
12
+ export { useStagger, getStaggerStyle, type UseStaggerOptions, type UseStaggerReturn } from './useStagger'
@@ -0,0 +1,36 @@
1
+ import { onMounted, onUnmounted, type Ref } from 'vue'
2
+
3
+ /**
4
+ * Detects clicks outside of a target element and executes a callback.
5
+ * Essential for modals, dropdowns, and popovers.
6
+ *
7
+ * @param el - Ref to the element to watch
8
+ * @param handler - Callback executed when click occurs outside
9
+ *
10
+ * Time Complexity: O(1) per event
11
+ * Space Complexity: O(1)
12
+ */
13
+ export function useClickOutside(
14
+ el: Ref<HTMLElement | null>,
15
+ handler: () => void
16
+ ): void {
17
+ const listener = (event: MouseEvent | TouchEvent): void => {
18
+ const target = event.target as Node
19
+
20
+ if (!el.value || el.value.contains(target)) {
21
+ return
22
+ }
23
+
24
+ handler()
25
+ }
26
+
27
+ onMounted(() => {
28
+ document.addEventListener('mousedown', listener)
29
+ document.addEventListener('touchstart', listener)
30
+ })
31
+
32
+ onUnmounted(() => {
33
+ document.removeEventListener('mousedown', listener)
34
+ document.removeEventListener('touchstart', listener)
35
+ })
36
+ }
@@ -0,0 +1,35 @@
1
+ import { ref } from 'vue'
2
+
3
+ /**
4
+ * Copy text to clipboard with success state tracking.
5
+ * @param timeout Reset duration for copied state (ms)
6
+ */
7
+ export function useClipboard(timeout = 2000) {
8
+ const copied = ref(false)
9
+ let timeoutId: ReturnType<typeof setTimeout> | null = null
10
+
11
+ const copy = async (text: string): Promise<boolean> => {
12
+ if (!navigator?.clipboard) {
13
+ console.warn('Clipboard API not supported')
14
+ return false
15
+ }
16
+
17
+ try {
18
+ await navigator.clipboard.writeText(text)
19
+ copied.value = true
20
+
21
+ if (timeoutId) clearTimeout(timeoutId)
22
+ timeoutId = setTimeout(() => {
23
+ copied.value = false
24
+ }, timeout)
25
+
26
+ return true
27
+ } catch (e) {
28
+ console.error('Failed to copy:', e)
29
+ copied.value = false
30
+ return false
31
+ }
32
+ }
33
+
34
+ return { copy, copied }
35
+ }
@@ -0,0 +1,48 @@
1
+ import { onMounted, onUnmounted, unref, watch, isRef, type Ref } from 'vue'
2
+
3
+ type EventTarget = Window | Document | HTMLElement | null
4
+ type MaybeRef<T> = T | Ref<T>
5
+
6
+ /**
7
+ * Attaches an event listener with automatic cleanup on unmount.
8
+ * Prevents memory leaks from forgotten removeEventListener calls.
9
+ */
10
+ export function useEventListener<K extends keyof WindowEventMap>(
11
+ target: MaybeRef<EventTarget>,
12
+ event: K,
13
+ callback: (e: WindowEventMap[K]) => void,
14
+ options?: AddEventListenerOptions
15
+ ): void
16
+
17
+ export function useEventListener(
18
+ target: MaybeRef<EventTarget>,
19
+ event: string,
20
+ callback: (e: Event) => void,
21
+ options?: AddEventListenerOptions
22
+ ): void
23
+
24
+ export function useEventListener(
25
+ target: MaybeRef<EventTarget>,
26
+ event: string,
27
+ callback: (e: Event) => void,
28
+ options?: AddEventListenerOptions
29
+ ) {
30
+ const attach = (el: EventTarget) => {
31
+ el?.addEventListener(event, callback, options)
32
+ }
33
+
34
+ const detach = (el: EventTarget) => {
35
+ el?.removeEventListener(event, callback, options)
36
+ }
37
+
38
+ if (isRef(target)) {
39
+ watch(target, (newEl, oldEl) => {
40
+ if (oldEl) detach(oldEl)
41
+ if (newEl) attach(newEl)
42
+ }, { immediate: true })
43
+ } else {
44
+ onMounted(() => attach(target))
45
+ }
46
+
47
+ onUnmounted(() => detach(unref(target)))
48
+ }
@@ -0,0 +1,58 @@
1
+ import { watch, nextTick, onUnmounted, type Ref } from 'vue'
2
+
3
+ const FOCUSABLE_SELECTOR = [
4
+ 'a[href]',
5
+ 'button:not([disabled])',
6
+ 'input:not([disabled])',
7
+ 'textarea:not([disabled])',
8
+ 'select:not([disabled])',
9
+ '[tabindex]:not([tabindex="-1"])'
10
+ ].join(', ')
11
+
12
+ /**
13
+ * Traps focus within a container element when active.
14
+ * Tab cycles through focusable elements, never escaping the container.
15
+ * Critical for modal accessibility.
16
+ */
17
+ export function useFocusTrap(
18
+ container: Ref<HTMLElement | null>,
19
+ isActive: Ref<boolean>
20
+ ) {
21
+ const trapFocus = (e: KeyboardEvent) => {
22
+ if (!isActive.value || !container.value) return
23
+ if (e.key !== 'Tab') return
24
+
25
+ const elements = container.value.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)
26
+ if (elements.length === 0) return
27
+
28
+ const first = elements[0]
29
+ const last = elements[elements.length - 1]
30
+
31
+ if (e.shiftKey) {
32
+ if (document.activeElement === first) {
33
+ e.preventDefault()
34
+ last.focus()
35
+ }
36
+ } else {
37
+ if (document.activeElement === last) {
38
+ e.preventDefault()
39
+ first.focus()
40
+ }
41
+ }
42
+ }
43
+
44
+ watch(isActive, async (val) => {
45
+ if (val) {
46
+ await nextTick()
47
+ const first = container.value?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR)
48
+ first?.focus()
49
+ window.addEventListener('keydown', trapFocus)
50
+ } else {
51
+ window.removeEventListener('keydown', trapFocus)
52
+ }
53
+ })
54
+
55
+ onUnmounted(() => {
56
+ window.removeEventListener('keydown', trapFocus)
57
+ })
58
+ }
@@ -0,0 +1,98 @@
1
+ import { watch, onUnmounted, type Ref } from 'vue'
2
+
3
+ export interface UseHoverRevealOptions {
4
+ size?: number
5
+ opacity?: number
6
+ }
7
+
8
+ /**
9
+ * Creates a radial gradient spotlight effect that follows the cursor on hover.
10
+ * Handles deferred elements (tabs, v-if) by watching for target availability.
11
+ *
12
+ * @param target - Ref to the target element
13
+ * @param options - Configuration options
14
+ *
15
+ * @example
16
+ * ```vue
17
+ * <script setup>
18
+ * const cardRef = ref<HTMLElement | null>(null)
19
+ * useHoverReveal(cardRef, { size: 200, opacity: 0.1 })
20
+ * </script>
21
+ *
22
+ * <template>
23
+ * <div ref="cardRef" data-hover-reveal>Hover over me</div>
24
+ * </template>
25
+ * ```
26
+ */
27
+ export function useHoverReveal(
28
+ target: Ref<HTMLElement | null>,
29
+ options: UseHoverRevealOptions = {}
30
+ ): void {
31
+ const { size, opacity } = options
32
+
33
+ let isInitialized = false
34
+ let currentElement: HTMLElement | null = null
35
+
36
+ function handleMouseMove(event: MouseEvent) {
37
+ if (!target.value) return
38
+
39
+ const rect = target.value.getBoundingClientRect()
40
+ const x = ((event.clientX - rect.left) / rect.width) * 100
41
+ const y = ((event.clientY - rect.top) / rect.height) * 100
42
+
43
+ target.value.style.setProperty('--mouse-x', `${x}%`)
44
+ target.value.style.setProperty('--mouse-y', `${y}%`)
45
+ if (size !== undefined) {
46
+ target.value.style.setProperty('--effect-hover-reveal-size', `${size}px`)
47
+ }
48
+ if (opacity !== undefined) {
49
+ target.value.style.setProperty('--effect-hover-reveal-opacity', `${opacity}`)
50
+ }
51
+ }
52
+
53
+ function handleMouseLeave() {
54
+ if (!target.value) return
55
+ target.value.style.removeProperty('--mouse-x')
56
+ target.value.style.removeProperty('--mouse-y')
57
+ }
58
+
59
+ function initialize(element: HTMLElement) {
60
+ if (isInitialized && currentElement === element) return
61
+
62
+ cleanup()
63
+
64
+ currentElement = element
65
+ isInitialized = true
66
+
67
+ element.setAttribute('data-hover-reveal', '')
68
+ element.addEventListener('mousemove', handleMouseMove)
69
+ element.addEventListener('mouseleave', handleMouseLeave)
70
+ }
71
+
72
+ function cleanup() {
73
+ if (currentElement) {
74
+ currentElement.removeEventListener('mousemove', handleMouseMove)
75
+ currentElement.removeEventListener('mouseleave', handleMouseLeave)
76
+ currentElement.style.removeProperty('--mouse-x')
77
+ currentElement.style.removeProperty('--mouse-y')
78
+ }
79
+ isInitialized = false
80
+ currentElement = null
81
+ }
82
+
83
+ watch(
84
+ target,
85
+ (newTarget) => {
86
+ if (newTarget) {
87
+ initialize(newTarget)
88
+ } else {
89
+ cleanup()
90
+ }
91
+ },
92
+ { immediate: true }
93
+ )
94
+
95
+ onUnmounted(() => {
96
+ cleanup()
97
+ })
98
+ }
@@ -0,0 +1,10 @@
1
+ let count = 0
2
+
3
+ /**
4
+ * Generates a unique ID for accessibility attributes.
5
+ * Links labels to inputs, aria-labelledby to modals, etc.
6
+ * @param prefix Optional prefix (default: 'ui')
7
+ */
8
+ export function useId(prefix = 'ui'): string {
9
+ return `${prefix}-${++count}`
10
+ }