@umbra.ui/core 0.2.0 → 0.4.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 (61) hide show
  1. package/dist/components/controls/InlineDropdown/InlineDropdown.vue +290 -0
  2. package/dist/components/controls/InlineDropdown/README.md +35 -0
  3. package/dist/components/controls/InlineDropdown/theme.css +40 -0
  4. package/dist/components/dialogs/Alert/Alert.vue +122 -11
  5. package/dist/components/dialogs/Alert/theme.css +20 -0
  6. package/dist/components/dialogs/Toast/useToast.d.ts +1 -1
  7. package/dist/components/inputs/AutogrowRichTextView/AutogrowRichTextView.vue +128 -0
  8. package/dist/components/inputs/AutogrowRichTextView/README.md +86 -0
  9. package/dist/components/inputs/AutogrowRichTextView/editor.css +211 -0
  10. package/dist/components/inputs/AutogrowRichTextView/theme.css +28 -0
  11. package/dist/components/inputs/InputCryptoAddress/InputCryptoAddress.vue +512 -0
  12. package/dist/components/inputs/InputCryptoAddress/README.md +45 -0
  13. package/dist/components/inputs/InputCryptoAddress/theme.css +80 -0
  14. package/dist/components/inputs/Tags/TagBar.vue +7 -4
  15. package/dist/components/inputs/Tags/theme.css +4 -0
  16. package/dist/components/inputs/search/README.md +64 -736
  17. package/dist/components/inputs/search/SearchOverlay.vue +376 -0
  18. package/dist/components/inputs/search/SearchResultCell.vue +205 -0
  19. package/dist/components/inputs/search/theme.css +66 -21
  20. package/dist/components/inputs/search/types.d.ts +27 -5
  21. package/dist/components/inputs/search/types.d.ts.map +1 -1
  22. package/dist/components/inputs/search/types.ts +33 -5
  23. package/dist/components/menus/ActionMenu/ActionMenu.vue +29 -7
  24. package/dist/components/menus/ActionMenu/theme.css +1 -1
  25. package/dist/components/menus/ActionMenu/types.d.ts +9 -0
  26. package/dist/components/menus/ActionMenu/types.d.ts.map +1 -0
  27. package/dist/components/menus/ActionMenu/types.js +1 -0
  28. package/dist/components/menus/ActionMenu/types.ts +9 -0
  29. package/dist/components/models/Popover/Popover.vue +6 -84
  30. package/dist/css/umbra-ui.css +1 -0
  31. package/dist/index.d.ts +7 -3
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +5 -2
  34. package/package.json +21 -16
  35. package/src/components/controls/InlineDropdown/InlineDropdown.vue +290 -0
  36. package/src/components/controls/InlineDropdown/README.md +35 -0
  37. package/src/components/controls/InlineDropdown/theme.css +40 -0
  38. package/src/components/dialogs/Alert/Alert.vue +122 -11
  39. package/src/components/dialogs/Alert/theme.css +20 -0
  40. package/src/components/inputs/AutogrowRichTextView/AutogrowRichTextView.vue +128 -0
  41. package/src/components/inputs/AutogrowRichTextView/README.md +86 -0
  42. package/src/components/inputs/AutogrowRichTextView/editor.css +211 -0
  43. package/src/components/inputs/AutogrowRichTextView/theme.css +28 -0
  44. package/src/components/inputs/InputCryptoAddress/InputCryptoAddress.vue +512 -0
  45. package/src/components/inputs/InputCryptoAddress/README.md +45 -0
  46. package/src/components/inputs/InputCryptoAddress/theme.css +80 -0
  47. package/src/components/inputs/Tags/TagBar.vue +7 -4
  48. package/src/components/inputs/Tags/theme.css +4 -0
  49. package/src/components/inputs/search/README.md +64 -736
  50. package/src/components/inputs/search/SearchOverlay.vue +376 -0
  51. package/src/components/inputs/search/SearchResultCell.vue +205 -0
  52. package/src/components/inputs/search/theme.css +66 -21
  53. package/src/components/inputs/search/types.ts +33 -5
  54. package/src/components/menus/ActionMenu/ActionMenu.vue +29 -7
  55. package/src/components/menus/ActionMenu/theme.css +1 -1
  56. package/src/components/menus/ActionMenu/types.ts +9 -0
  57. package/src/components/models/Popover/Popover.vue +6 -84
  58. package/src/index.ts +13 -3
  59. package/src/vue.d.ts +7 -26
  60. package/src/components/inputs/search/SearchBar.vue +0 -394
  61. package/src/components/inputs/search/SearchResults.vue +0 -310
@@ -1,394 +0,0 @@
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>
@@ -1,310 +0,0 @@
1
- <!-- SearchResults.vue -->
2
- <script setup lang="ts">
3
- import type { 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>