@umbra.ui/core 0.1.18 → 0.1.19
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.
- package/dist/components/controls/Button/Button.vue +417 -0
- package/dist/components/controls/Button/README.md +348 -0
- package/dist/components/controls/Button/theme.css +200 -0
- package/dist/components/controls/Checkbox/Checkbox.vue +164 -0
- package/dist/components/controls/Checkbox/README.md +441 -0
- package/dist/components/controls/Checkbox/theme.css +36 -0
- package/dist/components/controls/Dropdown/Dropdown.vue +476 -0
- package/dist/components/controls/Dropdown/README.md +370 -0
- package/dist/components/controls/Dropdown/theme.css +50 -0
- package/dist/components/controls/Dropdown/types.ts +6 -0
- package/dist/components/controls/IconButton/IconButton.vue +267 -0
- package/dist/components/controls/IconButton/README.md +502 -0
- package/dist/components/controls/IconButton/theme.css +89 -0
- package/dist/components/controls/Radio/README.md +591 -0
- package/dist/components/controls/Radio/Radio.vue +89 -0
- package/dist/components/controls/Radio/theme.css +14 -0
- package/dist/components/controls/RangeSlider/README.md +608 -0
- package/dist/components/controls/RangeSlider/RangeSlider.vue +535 -0
- package/dist/components/controls/RangeSlider/theme.css +80 -0
- package/dist/components/controls/SegmentedControl/README.md +587 -0
- package/dist/components/controls/SegmentedControl/SegmentedControl.vue +284 -0
- package/dist/components/controls/SegmentedControl/theme.css +60 -0
- package/dist/components/controls/SegmentedControl/types.ts +5 -0
- package/dist/components/controls/Slider/README.md +627 -0
- package/dist/components/controls/Slider/Slider.vue +260 -0
- package/dist/components/controls/Slider/theme.css +74 -0
- package/dist/components/controls/Stepper/README.md +601 -0
- package/dist/components/controls/Stepper/Stepper.vue +103 -0
- package/dist/components/controls/Stepper/theme.css +53 -0
- package/dist/components/controls/Switch/README.md +667 -0
- package/dist/components/controls/Switch/Switch.vue +127 -0
- package/dist/components/controls/Switch/theme.css +42 -0
- package/dist/components/dialogs/Alert/Alert.vue +218 -0
- package/dist/components/dialogs/Alert/README.md +450 -0
- package/dist/components/dialogs/Alert/theme.css +44 -0
- package/dist/components/dialogs/Alert/types.ts +11 -0
- package/dist/components/dialogs/Toast/README.md +522 -0
- package/dist/components/dialogs/Toast/Toast.vue +296 -0
- package/dist/components/dialogs/Toast/ToastContainer.vue +330 -0
- package/dist/components/dialogs/Toast/theme.css +44 -0
- package/dist/components/dialogs/Toast/types.ts +46 -0
- package/dist/components/dialogs/Toast/useToast.ts +127 -0
- package/dist/components/indicators/ProgressBar/ProgressBar.vue +98 -0
- package/dist/components/indicators/ProgressBar/README.md +744 -0
- package/dist/components/indicators/ProgressBar/theme.css +36 -0
- package/dist/components/indicators/Tooltip/README.md +723 -0
- package/dist/components/indicators/Tooltip/TooltipProvider.vue +142 -0
- package/dist/components/indicators/Tooltip/theme.css +18 -0
- package/dist/components/indicators/Tooltip/tooltip.ts +48 -0
- package/dist/components/indicators/Tooltip/types.ts +15 -0
- package/dist/components/indicators/Tooltip/useTooltip.ts +71 -0
- package/dist/components/inputs/AutogrowTextView/AutogrowTextView.vue +110 -0
- package/dist/components/inputs/AutogrowTextView/README.md +643 -0
- package/dist/components/inputs/AutogrowTextView/theme.css +28 -0
- package/dist/components/inputs/InputCard/InputCard.vue +600 -0
- package/dist/components/inputs/InputCard/README.md +636 -0
- package/dist/components/inputs/InputEmail/InputEmail.vue +698 -0
- package/dist/components/inputs/InputEmail/README.md +764 -0
- package/dist/components/inputs/InputNumber/InputNumber.vue +300 -0
- package/dist/components/inputs/InputNumber/README.md +749 -0
- package/dist/components/inputs/InputPhone/InputPhone.vue +645 -0
- package/dist/components/inputs/InputPhone/README.md +636 -0
- package/dist/components/inputs/InputSecure/InputSecure.vue +646 -0
- package/dist/components/inputs/InputSecure/README.md +771 -0
- package/dist/components/inputs/InputText/InputText.vue +225 -0
- package/dist/components/inputs/InputText/README.md +844 -0
- package/dist/components/inputs/OTP/OTP.vue +349 -0
- package/dist/components/inputs/OTP/README.md +736 -0
- package/dist/components/inputs/OTP/theme.css +50 -0
- package/dist/components/inputs/StringCapture/README.md +718 -0
- package/dist/components/inputs/StringCapture/StringCapture.vue +315 -0
- package/dist/components/inputs/StringCapture/theme.css +86 -0
- package/dist/components/inputs/Tags/README.md +897 -0
- package/dist/components/inputs/Tags/TagBar.vue +793 -0
- package/dist/components/inputs/Tags/TagCreation.vue +219 -0
- package/dist/components/inputs/Tags/TagPicker.vue +380 -0
- package/dist/components/inputs/Tags/tag-bar-styles.ts +354 -0
- package/dist/components/inputs/Tags/theme.css +121 -0
- package/dist/components/inputs/Tags/types.ts +346 -0
- package/dist/components/inputs/search/README.md +759 -0
- package/dist/components/inputs/search/SearchBar.vue +394 -0
- package/dist/components/inputs/search/SearchResults.vue +310 -0
- package/dist/components/inputs/search/theme.css +187 -0
- package/dist/components/inputs/search/types.ts +8 -0
- package/dist/components/inputs/theme.css +102 -0
- package/dist/components/menus/ActionMenu/ActionMenu.vue +383 -0
- package/dist/components/menus/ActionMenu/README.md +825 -0
- package/dist/components/menus/ActionMenu/theme.css +93 -0
- package/dist/components/models/Popover/Popover.vue +551 -0
- package/dist/components/models/Popover/README.md +885 -0
- package/dist/components/models/Popover/theme.css +52 -0
- package/dist/components/models/Sheet/README.md +1159 -0
- package/dist/components/models/Sheet/Sheet.vue +465 -0
- package/dist/components/models/Sheet/theme.css +72 -0
- package/dist/components/models/Sidebar/README.md +1228 -0
- package/dist/components/models/Sidebar/Sidebar.vue +480 -0
- package/dist/components/models/Sidebar/theme.css +90 -0
- package/dist/components/navigation/adaptive/AdaptiveLayout.vue +779 -0
- package/dist/components/navigation/adaptive/AdaptiveLayoutBreadcrumbs.vue +192 -0
- package/dist/components/navigation/adaptive/AdaptiveLayoutMenuButton.vue +149 -0
- package/dist/components/navigation/adaptive/README.md +768 -0
- package/dist/components/navigation/adaptive/types.ts +19 -0
- package/dist/components/navigation/adaptive/useAdaptiveLayout.ts +89 -0
- package/dist/components/navigation/adaptive/useBreakpoints.ts +41 -0
- package/dist/components/navigation/adaptive/useContainerMonitor.ts +214 -0
- package/dist/components/navigation/adaptive/useViewAnimation.ts +721 -0
- package/dist/components/navigation/adaptive/useViewResize.ts +211 -0
- package/dist/components/navigation/navstack/NavigationStack.vue +180 -0
- package/dist/components/navigation/navstack/README.md +994 -0
- package/dist/components/navigation/navstack/useNavigationStack.ts +164 -0
- package/dist/components/navigation/slideover/README.md +1275 -0
- package/dist/components/navigation/slideover/SlideoverController.vue +287 -0
- package/dist/components/navigation/slideover/useSlideoverController.ts +320 -0
- package/dist/components/navigation/splitview/README.md +1115 -0
- package/dist/components/navigation/splitview/SplitViewController.vue +176 -0
- package/dist/components/navigation/splitview/useSplitViewController.ts +388 -0
- package/dist/components/navigation/tabcontroller/README.md +919 -0
- package/dist/components/navigation/tabcontroller/TabController.vue +307 -0
- package/dist/components/navigation/tabcontroller/TabItem.vue +57 -0
- package/dist/components/navigation/tabcontroller/types.ts +24 -0
- package/dist/components/navigation/tabcontroller/useTabController.ts +18 -0
- package/dist/components/navigation/theme.css +91 -0
- package/dist/components/navigation/types.ts +7 -0
- package/dist/components/pickers/CollectionPicker/CollectionPicker.vue +398 -0
- package/dist/components/pickers/CollectionPicker/README.md +1115 -0
- package/dist/components/pickers/CollectionPicker/theme.css +14 -0
- package/dist/components/pickers/CollectionPicker/types.ts +11 -0
- package/dist/components/pickers/ColorPicker/ColorPicker.vue +376 -0
- package/dist/components/pickers/ColorPicker/README.md +1439 -0
- package/dist/components/pickers/ColorPicker/colors.ts +299 -0
- package/dist/components/pickers/ColorPicker/theme.css +32 -0
- package/dist/components/pickers/DatePicker/DatePicker.vue +660 -0
- package/dist/components/pickers/DatePicker/README.md +1195 -0
- package/dist/components/pickers/DatePicker/theme.css +22 -0
- package/dist/components/pickers/FilePicker/FilePicker.vue +534 -0
- package/dist/components/pickers/FilePicker/README.md +1542 -0
- package/dist/components/pickers/FilePicker/theme.css +48 -0
- package/dist/components/pickers/FilePicker/types.ts +10 -0
- package/dist/components/pickers/IconPicker/IconPicker.vue +327 -0
- package/dist/components/pickers/IconPicker/README.md +1161 -0
- package/dist/components/pickers/IconPicker/theme.css +28 -0
- package/dist/components/pickers/theme.css +82 -0
- package/dist/components/views/MarkdownViewer/MarkdownViewer.vue +442 -0
- package/dist/components/views/MarkdownViewer/README.md +833 -0
- package/dist/components/views/MarkdownViewer/theme.css +130 -0
- package/package.json +3 -2
|
@@ -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>
|