@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.
- package/dist/components/controls/InlineDropdown/InlineDropdown.vue +290 -0
- package/dist/components/controls/InlineDropdown/README.md +35 -0
- package/dist/components/controls/InlineDropdown/theme.css +40 -0
- package/dist/components/dialogs/Alert/Alert.vue +122 -11
- package/dist/components/dialogs/Alert/theme.css +20 -0
- package/dist/components/dialogs/Toast/useToast.d.ts +1 -1
- package/dist/components/inputs/AutogrowRichTextView/AutogrowRichTextView.vue +128 -0
- package/dist/components/inputs/AutogrowRichTextView/README.md +86 -0
- package/dist/components/inputs/AutogrowRichTextView/editor.css +211 -0
- package/dist/components/inputs/AutogrowRichTextView/theme.css +28 -0
- package/dist/components/inputs/InputCryptoAddress/InputCryptoAddress.vue +512 -0
- package/dist/components/inputs/InputCryptoAddress/README.md +45 -0
- package/dist/components/inputs/InputCryptoAddress/theme.css +80 -0
- package/dist/components/inputs/Tags/TagBar.vue +7 -4
- package/dist/components/inputs/Tags/theme.css +4 -0
- package/dist/components/inputs/search/README.md +64 -736
- package/dist/components/inputs/search/SearchOverlay.vue +376 -0
- package/dist/components/inputs/search/SearchResultCell.vue +205 -0
- package/dist/components/inputs/search/theme.css +66 -21
- package/dist/components/inputs/search/types.d.ts +27 -5
- package/dist/components/inputs/search/types.d.ts.map +1 -1
- package/dist/components/inputs/search/types.ts +33 -5
- package/dist/components/menus/ActionMenu/ActionMenu.vue +29 -7
- package/dist/components/menus/ActionMenu/theme.css +1 -1
- package/dist/components/menus/ActionMenu/types.d.ts +9 -0
- package/dist/components/menus/ActionMenu/types.d.ts.map +1 -0
- package/dist/components/menus/ActionMenu/types.js +1 -0
- package/dist/components/menus/ActionMenu/types.ts +9 -0
- package/dist/components/models/Popover/Popover.vue +6 -84
- package/dist/css/umbra-ui.css +1 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/package.json +21 -16
- package/src/components/controls/InlineDropdown/InlineDropdown.vue +290 -0
- package/src/components/controls/InlineDropdown/README.md +35 -0
- package/src/components/controls/InlineDropdown/theme.css +40 -0
- package/src/components/dialogs/Alert/Alert.vue +122 -11
- package/src/components/dialogs/Alert/theme.css +20 -0
- package/src/components/inputs/AutogrowRichTextView/AutogrowRichTextView.vue +128 -0
- package/src/components/inputs/AutogrowRichTextView/README.md +86 -0
- package/src/components/inputs/AutogrowRichTextView/editor.css +211 -0
- package/src/components/inputs/AutogrowRichTextView/theme.css +28 -0
- package/src/components/inputs/InputCryptoAddress/InputCryptoAddress.vue +512 -0
- package/src/components/inputs/InputCryptoAddress/README.md +45 -0
- package/src/components/inputs/InputCryptoAddress/theme.css +80 -0
- package/src/components/inputs/Tags/TagBar.vue +7 -4
- package/src/components/inputs/Tags/theme.css +4 -0
- package/src/components/inputs/search/README.md +64 -736
- package/src/components/inputs/search/SearchOverlay.vue +376 -0
- package/src/components/inputs/search/SearchResultCell.vue +205 -0
- package/src/components/inputs/search/theme.css +66 -21
- package/src/components/inputs/search/types.ts +33 -5
- package/src/components/menus/ActionMenu/ActionMenu.vue +29 -7
- package/src/components/menus/ActionMenu/theme.css +1 -1
- package/src/components/menus/ActionMenu/types.ts +9 -0
- package/src/components/models/Popover/Popover.vue +6 -84
- package/src/index.ts +13 -3
- package/src/vue.d.ts +7 -26
- package/src/components/inputs/search/SearchBar.vue +0 -394
- package/src/components/inputs/search/SearchResults.vue +0 -310
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, nextTick, ref, watch } from "vue";
|
|
3
|
+
import type { SearchOverlayResult } from "./types";
|
|
4
|
+
import "./theme.css";
|
|
5
|
+
import { MagnifierIcon, ChevronRightIcon } from "@umbra.ui/icons";
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
modelValue: boolean;
|
|
9
|
+
query: string;
|
|
10
|
+
results: SearchOverlayResult[];
|
|
11
|
+
activeIndex: number;
|
|
12
|
+
showRecent: boolean;
|
|
13
|
+
recentLabel?: string;
|
|
14
|
+
searchLabel?: string;
|
|
15
|
+
resultsLabel?: string;
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
recentEmptyLabel?: string;
|
|
18
|
+
resultsEmptyLabel?: string;
|
|
19
|
+
canLoadMore?: boolean;
|
|
20
|
+
}>();
|
|
21
|
+
|
|
22
|
+
const emit = defineEmits<{
|
|
23
|
+
(event: "update:modelValue", value: boolean): void;
|
|
24
|
+
(event: "update:query", value: string): void;
|
|
25
|
+
(event: "update:activeIndex", value: number): void;
|
|
26
|
+
(event: "select", value: SearchOverlayResult): void;
|
|
27
|
+
(event: "load-more"): void;
|
|
28
|
+
}>();
|
|
29
|
+
|
|
30
|
+
const inputRef = ref<HTMLInputElement | null>(null);
|
|
31
|
+
const resultsRef = ref<HTMLElement | null>(null);
|
|
32
|
+
const shouldScrollToBottom = ref(false);
|
|
33
|
+
|
|
34
|
+
const isOpen = computed(() => props.modelValue);
|
|
35
|
+
const searchQuery = computed({
|
|
36
|
+
get: () => props.query,
|
|
37
|
+
set: (value) => emit("update:query", value),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const hasResults = computed(() => props.results.length > 0);
|
|
41
|
+
|
|
42
|
+
const setActiveIndex = (index: number) => {
|
|
43
|
+
emit("update:activeIndex", index);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const closeOverlay = () => {
|
|
47
|
+
emit("update:modelValue", false);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
51
|
+
// Keyboard control mirrors command-palette behavior.
|
|
52
|
+
if (event.key === "Escape") {
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
closeOverlay();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (event.key === "ArrowDown") {
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
const maxIndex = props.results.length - 1;
|
|
61
|
+
if (maxIndex < 0) return;
|
|
62
|
+
emit("update:activeIndex", Math.min(props.activeIndex + 1, maxIndex));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (event.key === "ArrowUp") {
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
emit("update:activeIndex", Math.max(props.activeIndex - 1, 0));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (event.key === "Enter") {
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
const result = props.results[props.activeIndex];
|
|
75
|
+
if (result) {
|
|
76
|
+
emit("select", result);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const focusInput = () => {
|
|
82
|
+
nextTick(() => {
|
|
83
|
+
inputRef.value?.focus();
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
watch(isOpen, (value) => {
|
|
88
|
+
if (value) {
|
|
89
|
+
focusInput();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
watch(
|
|
94
|
+
() => props.results.length,
|
|
95
|
+
() => {
|
|
96
|
+
if (!shouldScrollToBottom.value) return;
|
|
97
|
+
nextTick(() => {
|
|
98
|
+
if (resultsRef.value) {
|
|
99
|
+
// Scroll to newly loaded results after "Load More".
|
|
100
|
+
resultsRef.value.scrollTo({
|
|
101
|
+
top: resultsRef.value.scrollHeight,
|
|
102
|
+
behavior: "smooth",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
shouldScrollToBottom.value = false;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const handleLoadMoreClick = () => {
|
|
111
|
+
// Flag triggers scrolling once results are appended.
|
|
112
|
+
shouldScrollToBottom.value = true;
|
|
113
|
+
emit("load-more");
|
|
114
|
+
};
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<template>
|
|
118
|
+
<div :class="$style.container">
|
|
119
|
+
<Teleport to="body">
|
|
120
|
+
<Transition name="search-overlay">
|
|
121
|
+
<div v-if="isOpen" :class="$style.overlay" @click="closeOverlay">
|
|
122
|
+
<div :class="$style.panel" @click.stop>
|
|
123
|
+
<div
|
|
124
|
+
:class="[
|
|
125
|
+
$style.input_header,
|
|
126
|
+
hasResults && $style.input_header_filled,
|
|
127
|
+
]"
|
|
128
|
+
>
|
|
129
|
+
<div :class="$style.input_row">
|
|
130
|
+
<MagnifierIcon size="16" />
|
|
131
|
+
<input
|
|
132
|
+
ref="inputRef"
|
|
133
|
+
v-model="searchQuery"
|
|
134
|
+
type="text"
|
|
135
|
+
:placeholder="placeholder || 'Type to search'"
|
|
136
|
+
:class="[$style.input, 'body']"
|
|
137
|
+
@keydown="handleKeydown"
|
|
138
|
+
/>
|
|
139
|
+
<div :class="$style.esc_hint">ESC</div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div v-if="showRecent && hasResults" :class="$style.section_header">
|
|
144
|
+
<span class="subheadline">{{
|
|
145
|
+
recentLabel || "Recent Searches"
|
|
146
|
+
}}</span>
|
|
147
|
+
</div>
|
|
148
|
+
<div v-else-if="hasResults" :class="$style.section_header">
|
|
149
|
+
<span class="callout">
|
|
150
|
+
<span>Searching all notes for </span>
|
|
151
|
+
<span :class="$style.search_term"
|
|
152
|
+
>"{{ searchQuery.trim() }}"</span
|
|
153
|
+
>
|
|
154
|
+
</span>
|
|
155
|
+
<span :class="['callout']">{{ resultsLabel }}</span>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div
|
|
159
|
+
v-if="results.length > 0"
|
|
160
|
+
ref="resultsRef"
|
|
161
|
+
:class="$style.results"
|
|
162
|
+
>
|
|
163
|
+
<div
|
|
164
|
+
v-for="(result, index) in results"
|
|
165
|
+
:key="result.id"
|
|
166
|
+
@click="emit('select', result)"
|
|
167
|
+
@mouseenter="setActiveIndex(index)"
|
|
168
|
+
>
|
|
169
|
+
<slot
|
|
170
|
+
name="result"
|
|
171
|
+
:result="result"
|
|
172
|
+
:query="searchQuery"
|
|
173
|
+
:isActive="index === activeIndex"
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
<div v-else-if="!showRecent" :class="$style.empty_state">
|
|
178
|
+
<span>{{ resultsEmptyLabel || "No matches found." }}</span>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<button
|
|
182
|
+
v-if="canLoadMore"
|
|
183
|
+
type="button"
|
|
184
|
+
:class="$style.load_more"
|
|
185
|
+
@click="handleLoadMoreClick"
|
|
186
|
+
>
|
|
187
|
+
<span class="subheadline">Load More</span>
|
|
188
|
+
<ChevronRightIcon size="15" />
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</Transition>
|
|
193
|
+
</Teleport>
|
|
194
|
+
</div>
|
|
195
|
+
</template>
|
|
196
|
+
|
|
197
|
+
<style module>
|
|
198
|
+
.container {
|
|
199
|
+
position: relative;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.overlay {
|
|
203
|
+
position: fixed;
|
|
204
|
+
inset: 0;
|
|
205
|
+
display: flex;
|
|
206
|
+
justify-content: center;
|
|
207
|
+
align-items: flex-start;
|
|
208
|
+
padding-top: 3.529rem;
|
|
209
|
+
background-color: var(--search-overlay-bg);
|
|
210
|
+
backdrop-filter: var(--search-overlay-blur);
|
|
211
|
+
-webkit-backdrop-filter: var(--search-overlay-blur);
|
|
212
|
+
opacity: 1;
|
|
213
|
+
pointer-events: auto;
|
|
214
|
+
transition: opacity 0.2s ease, backdrop-filter 0.25s ease;
|
|
215
|
+
z-index: 9000;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.panel {
|
|
219
|
+
width: min(44.118rem, 90vw);
|
|
220
|
+
background-color: var(--search-panel-bg);
|
|
221
|
+
border-radius: 0.706rem;
|
|
222
|
+
box-shadow: var(--search-panel-shadow);
|
|
223
|
+
overflow: hidden;
|
|
224
|
+
transform: translateY(0) scale(1);
|
|
225
|
+
opacity: 1;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
:global(.search-overlay-enter-active),
|
|
229
|
+
:global(.search-overlay-leave-active) {
|
|
230
|
+
transition: opacity 0.2s ease, backdrop-filter 0.25s ease;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
:global(.search-overlay-enter-from),
|
|
234
|
+
:global(.search-overlay-leave-to) {
|
|
235
|
+
opacity: 0;
|
|
236
|
+
pointer-events: none;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
:global(.search-overlay-enter-to),
|
|
240
|
+
:global(.search-overlay-leave-from) {
|
|
241
|
+
opacity: 1;
|
|
242
|
+
pointer-events: auto;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
:global(.search-overlay-enter-active) .panel,
|
|
246
|
+
:global(.search-overlay-leave-active) .panel {
|
|
247
|
+
transition: transform 0.25s ease, opacity 0.2s ease;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
:global(.search-overlay-enter-from) .panel,
|
|
251
|
+
:global(.search-overlay-leave-to) .panel {
|
|
252
|
+
transform: translateY(-0.588rem) scale(0.94);
|
|
253
|
+
opacity: 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
:global(.search-overlay-enter-to) .panel,
|
|
257
|
+
:global(.search-overlay-leave-from) .panel {
|
|
258
|
+
transform: translateY(0) scale(1);
|
|
259
|
+
opacity: 1;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.input_header {
|
|
263
|
+
background-color: var(--search-input-header-bg);
|
|
264
|
+
padding: 0.882rem;
|
|
265
|
+
transition: background-color 0.2s ease;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.input_header_filled {
|
|
269
|
+
background-color: var(--search-input-header-filled-bg);
|
|
270
|
+
}
|
|
271
|
+
.input_row {
|
|
272
|
+
display: flex;
|
|
273
|
+
align-items: center;
|
|
274
|
+
gap: 0.471rem;
|
|
275
|
+
padding: 0.471rem;
|
|
276
|
+
background-color: var(--search-input-row-bg);
|
|
277
|
+
border: var(--search-input-row-border);
|
|
278
|
+
border-radius: 0.471rem;
|
|
279
|
+
color: var(--search-input-text);
|
|
280
|
+
transition: background-color 0.3s ease, color 0.3s ease;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.input_row:has(.input:not(:placeholder-shown)) {
|
|
284
|
+
background-color: var(--search-input-filled-bg);
|
|
285
|
+
color: var(--search-input-filled-text);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.input_row:has(.input:not(:placeholder-shown)) .input {
|
|
289
|
+
color: var(--search-input-filled-text);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.input_row:has(.input:not(:placeholder-shown)) .esc_hint {
|
|
293
|
+
background-color: var(--search-esc-bg);
|
|
294
|
+
color: var(--search-esc-text);
|
|
295
|
+
border: var(--search-esc-border);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.input {
|
|
299
|
+
flex: 1;
|
|
300
|
+
background: transparent;
|
|
301
|
+
border: none;
|
|
302
|
+
color: var(--search-input-text);
|
|
303
|
+
font-size: 0.882rem;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.input::placeholder {
|
|
307
|
+
color: var(--search-input-placeholder);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.input:focus {
|
|
311
|
+
outline: none;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.esc_hint {
|
|
315
|
+
font-size: 0.706rem;
|
|
316
|
+
color: var(--search-esc-text);
|
|
317
|
+
border: var(--search-esc-border);
|
|
318
|
+
border-radius: 0.294rem;
|
|
319
|
+
padding: 0.235rem 0.471rem;
|
|
320
|
+
background-color: transparent;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.section_header {
|
|
324
|
+
display: flex;
|
|
325
|
+
justify-content: space-between;
|
|
326
|
+
padding: 0.471rem 0.941rem;
|
|
327
|
+
font-size: 0.706rem;
|
|
328
|
+
color: var(--search-subheader-text);
|
|
329
|
+
background-color: var(--search-subheader-bg);
|
|
330
|
+
border-top: 1px solid var(--search-subheader-border);
|
|
331
|
+
border-bottom: 1px solid var(--search-subheader-border);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.search_term {
|
|
335
|
+
color: var(--search-term-text);
|
|
336
|
+
font-weight: 700;
|
|
337
|
+
opacity: 1;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.results {
|
|
341
|
+
max-height: 30rem;
|
|
342
|
+
overflow-y: auto;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.results_label {
|
|
346
|
+
opacity: 0.5;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.empty_state {
|
|
350
|
+
padding: 1.176rem 0.941rem;
|
|
351
|
+
font-size: 0.824rem;
|
|
352
|
+
color: var(--search-nothing-text);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.load_more {
|
|
356
|
+
width: 100%;
|
|
357
|
+
padding: 0.706rem 0.941rem;
|
|
358
|
+
background: transparent;
|
|
359
|
+
background-color: var(--search-load-more-bg);
|
|
360
|
+
color: var(--search-load-more-text);
|
|
361
|
+
border: none;
|
|
362
|
+
border-top: 1px solid var(--search-subheader-border);
|
|
363
|
+
cursor: pointer;
|
|
364
|
+
font-size: 0.824rem;
|
|
365
|
+
display: flex;
|
|
366
|
+
align-items: center;
|
|
367
|
+
justify-content: end;
|
|
368
|
+
gap: 0.235rem;
|
|
369
|
+
transition: color 0.3s ease, background-color 0.3s ease;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.load_more:hover {
|
|
373
|
+
color: var(--search-result-text);
|
|
374
|
+
background-color: var(--search-load-more-hover-bg);
|
|
375
|
+
}
|
|
376
|
+
</style>
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import type { SearchOverlayResult, SearchPathSegment } from "./types";
|
|
4
|
+
import "./theme.css";
|
|
5
|
+
import { ChevronRightIcon } from "@umbra.ui/icons";
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
result: SearchOverlayResult;
|
|
9
|
+
query: string;
|
|
10
|
+
isActive?: boolean;
|
|
11
|
+
}>();
|
|
12
|
+
|
|
13
|
+
const emit = defineEmits<{
|
|
14
|
+
(event: "segment-click", value: SearchPathSegment): void;
|
|
15
|
+
}>();
|
|
16
|
+
|
|
17
|
+
type HighlightSegment = {
|
|
18
|
+
text: string;
|
|
19
|
+
isMatch: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const escapeRegExp = (value: string): string => {
|
|
23
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Lightweight markdown stripping to keep snippets readable in search.
|
|
27
|
+
const stripMarkdown = (value: string): string => {
|
|
28
|
+
return value
|
|
29
|
+
.replace(/^#\s+.+\n?/m, "")
|
|
30
|
+
.replace(/^.+\n={3,}\n?/m, "")
|
|
31
|
+
.replace(/^#{1,6}\s+/gm, "")
|
|
32
|
+
.replace(/(\*\*|__)(.*?)\1/g, "$2")
|
|
33
|
+
.replace(/(\*|_)(.*?)\1/g, "$2")
|
|
34
|
+
.replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1")
|
|
35
|
+
.replace(/!\[([^\]]*)\]\([^\)]+\)/g, "")
|
|
36
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
37
|
+
.replace(/```[\s\S]*?```/g, "")
|
|
38
|
+
.replace(/^>\s+/gm, "")
|
|
39
|
+
.replace(/^[-*_]{3,}$/gm, "")
|
|
40
|
+
.replace(/^[\s]*[-*+]\s+/gm, "")
|
|
41
|
+
.replace(/^[\s]*\d+\.\s+/gm, "")
|
|
42
|
+
.replace(/\n\s*\n/g, "\n")
|
|
43
|
+
.trim();
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const getTerms = (value: string): string[] => {
|
|
47
|
+
return value
|
|
48
|
+
.trim()
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
.split(/\s+/)
|
|
51
|
+
.map((term) => term.trim())
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Highlight segments preserve match visibility after markdown stripping.
|
|
56
|
+
const buildHighlightSegments = (
|
|
57
|
+
text: string,
|
|
58
|
+
query: string
|
|
59
|
+
): HighlightSegment[] => {
|
|
60
|
+
const terms = getTerms(query);
|
|
61
|
+
if (terms.length === 0) {
|
|
62
|
+
return [{ text, isMatch: false }];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const pattern = terms.map((term) => escapeRegExp(term)).join("|");
|
|
66
|
+
const matcher = new RegExp(`(${pattern})`, "ig");
|
|
67
|
+
const parts = text.split(matcher);
|
|
68
|
+
|
|
69
|
+
return parts
|
|
70
|
+
.filter((part) => part.length > 0)
|
|
71
|
+
.map((part) => ({
|
|
72
|
+
text: part,
|
|
73
|
+
isMatch: terms.some((term) => part.toLowerCase() === term),
|
|
74
|
+
}));
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const titleSegments = computed(() => {
|
|
78
|
+
return buildHighlightSegments(stripMarkdown(props.result.title), props.query);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const excerptSegments = computed(() => {
|
|
82
|
+
return buildHighlightSegments(
|
|
83
|
+
stripMarkdown(props.result.excerpt),
|
|
84
|
+
props.query
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Prefer structured path segments when available for click targets.
|
|
89
|
+
const pathSegments = computed(() => {
|
|
90
|
+
if (props.result.pathSegments.length > 0) {
|
|
91
|
+
return props.result.pathSegments;
|
|
92
|
+
}
|
|
93
|
+
return props.result.path
|
|
94
|
+
.split(" > ")
|
|
95
|
+
.map((part, index) => ({
|
|
96
|
+
id: `${part}-${index}`,
|
|
97
|
+
type: "collection" as const,
|
|
98
|
+
title: part.trim(),
|
|
99
|
+
}))
|
|
100
|
+
.filter((segment) => segment.title.length > 0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const handleSegmentClick = (segment: SearchPathSegment) => {
|
|
104
|
+
emit("segment-click", segment);
|
|
105
|
+
};
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<template>
|
|
109
|
+
<div :class="[$style.container, isActive && $style.container_active]">
|
|
110
|
+
<div :class="[$style.path, 'subheadline']">
|
|
111
|
+
<template v-for="(segment, index) in pathSegments" :key="segment.id">
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
:class="$style.path_segment"
|
|
115
|
+
@click.stop="handleSegmentClick(segment)"
|
|
116
|
+
>
|
|
117
|
+
{{ segment.title }}
|
|
118
|
+
</button>
|
|
119
|
+
<ChevronRightIcon v-if="index < pathSegments.length - 1" size="12" />
|
|
120
|
+
</template>
|
|
121
|
+
</div>
|
|
122
|
+
<div :class="[$style.title, 'headline']">
|
|
123
|
+
<span
|
|
124
|
+
v-for="(segment, index) in titleSegments"
|
|
125
|
+
:key="`${segment.text}-${index}`"
|
|
126
|
+
:class="segment.isMatch ? $style.highlight : undefined"
|
|
127
|
+
>
|
|
128
|
+
{{ segment.text }}
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
<div v-if="result.excerpt" :class="[$style.excerpt, 'body']">
|
|
132
|
+
<span
|
|
133
|
+
v-for="(segment, index) in excerptSegments"
|
|
134
|
+
:key="`${segment.text}-${index}`"
|
|
135
|
+
:class="segment.isMatch ? $style.highlight : undefined"
|
|
136
|
+
>
|
|
137
|
+
{{ segment.text }}
|
|
138
|
+
</span>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</template>
|
|
142
|
+
|
|
143
|
+
<style module>
|
|
144
|
+
.container {
|
|
145
|
+
padding: 0.882rem;
|
|
146
|
+
display: flex;
|
|
147
|
+
flex-direction: column;
|
|
148
|
+
gap: 0.235rem;
|
|
149
|
+
cursor: default;
|
|
150
|
+
background-color: var(--search-result-bg);
|
|
151
|
+
border-bottom: 1px solid var(--search-result-border);
|
|
152
|
+
color: var(--search-result-text);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.container_active {
|
|
156
|
+
background-color: var(--search-result-active-bg);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.path {
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
gap: 0.235rem;
|
|
163
|
+
color: var(--search-result-secondary-text);
|
|
164
|
+
opacity: 0.5;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.path_segment {
|
|
168
|
+
background: none;
|
|
169
|
+
border: none;
|
|
170
|
+
padding: 0;
|
|
171
|
+
margin: 0;
|
|
172
|
+
color: inherit;
|
|
173
|
+
font: inherit;
|
|
174
|
+
cursor: pointer;
|
|
175
|
+
text-align: left;
|
|
176
|
+
transition: color 0.2s ease, text-decoration-color 0.2s ease;
|
|
177
|
+
text-decoration: underline;
|
|
178
|
+
text-decoration-color: transparent;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.path_segment:hover {
|
|
182
|
+
color: var(--search-path-hover-text);
|
|
183
|
+
text-decoration-color: var(--search-path-hover-underline);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.title {
|
|
187
|
+
opacity: 1;
|
|
188
|
+
color: var(--search-result-text);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.excerpt {
|
|
192
|
+
opacity: 0.7;
|
|
193
|
+
color: var(--search-result-text);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.highlight {
|
|
197
|
+
background-color: var(--search-highlight-bg);
|
|
198
|
+
color: var(--search-highlight-text);
|
|
199
|
+
padding-left: 0.118rem;
|
|
200
|
+
padding-right: 0.118rem;
|
|
201
|
+
padding-top: 0;
|
|
202
|
+
padding-bottom: 0;
|
|
203
|
+
border-radius: 0.176rem;
|
|
204
|
+
}
|
|
205
|
+
</style>
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
0.1
|
|
51
51
|
); /* blackA6 - border for light mode */
|
|
52
52
|
--search-result-text: #1f2937; /* gray8 - dark text for light mode */
|
|
53
|
-
--search-result-secondary-text: #
|
|
53
|
+
--search-result-secondary-text: #1f2937; /* match text for opacity rules */
|
|
54
54
|
|
|
55
55
|
/* SearchResults icon colors */
|
|
56
56
|
--search-icon-bg: #f3f4f6; /* gray1 - light background for light mode */
|
|
@@ -97,6 +97,31 @@
|
|
|
97
97
|
0,
|
|
98
98
|
0.1
|
|
99
99
|
); /* blackA8 - lighter overlay for light mode */
|
|
100
|
+
--search-overlay-blur: blur(8px);
|
|
101
|
+
|
|
102
|
+
/* SearchOverlay panel colors */
|
|
103
|
+
--search-panel-bg: #ffffff;
|
|
104
|
+
--search-panel-shadow: 0 0.824rem 1.765rem rgba(0, 0, 0, 0.1);
|
|
105
|
+
|
|
106
|
+
/* SearchOverlay input colors */
|
|
107
|
+
--search-input-header-bg: #ffffff;
|
|
108
|
+
--search-input-header-filled-bg: #f9fafb;
|
|
109
|
+
--search-input-row-bg: #ffffff;
|
|
110
|
+
--search-input-row-border: 1px solid #d9d9d9;
|
|
111
|
+
--search-input-text: #1f2937;
|
|
112
|
+
--search-input-placeholder: #6b7280;
|
|
113
|
+
--search-input-filled-bg: #f3f4f6;
|
|
114
|
+
--search-input-filled-text: #111827;
|
|
115
|
+
--search-esc-bg: rgba(0, 0, 0, 0.08);
|
|
116
|
+
--search-esc-text: #6b7280;
|
|
117
|
+
--search-esc-border: 1px solid rgba(0, 0, 0, 0.12);
|
|
118
|
+
|
|
119
|
+
/* SearchOverlay result cell colors */
|
|
120
|
+
--search-result-active-bg: rgba(0, 0, 0, 0.05);
|
|
121
|
+
--search-path-hover-text: #1f2937;
|
|
122
|
+
--search-path-hover-underline: rgba(0, 0, 0, 0.4);
|
|
123
|
+
--search-highlight-text: #111111;
|
|
124
|
+
--search-term-text: #1f2937;
|
|
100
125
|
}
|
|
101
126
|
|
|
102
127
|
/* Dark theme */
|
|
@@ -128,9 +153,14 @@
|
|
|
128
153
|
); /* Original dark mode value */
|
|
129
154
|
|
|
130
155
|
/* SearchResults subheader colors */
|
|
131
|
-
--search-subheader-bg: #
|
|
132
|
-
--search-subheader-border: rgba(
|
|
133
|
-
|
|
156
|
+
--search-subheader-bg: #606060; /* Updated dark mode value */
|
|
157
|
+
--search-subheader-border: rgba(
|
|
158
|
+
255,
|
|
159
|
+
255,
|
|
160
|
+
255,
|
|
161
|
+
0.15
|
|
162
|
+
); /* Updated dark mode value */
|
|
163
|
+
--search-subheader-text: #b4b4b4; /* Updated dark mode value */
|
|
134
164
|
|
|
135
165
|
/* SearchResults result colors */
|
|
136
166
|
--search-result-bg: #484848; /* Original dark mode value */
|
|
@@ -146,8 +176,8 @@
|
|
|
146
176
|
255,
|
|
147
177
|
0.12
|
|
148
178
|
); /* Original dark mode value */
|
|
149
|
-
--search-result-text: #
|
|
150
|
-
--search-result-secondary-text: #
|
|
179
|
+
--search-result-text: #ffffff; /* Updated dark mode value */
|
|
180
|
+
--search-result-secondary-text: #ffffff; /* Updated dark mode value */
|
|
151
181
|
|
|
152
182
|
/* SearchResults icon colors */
|
|
153
183
|
--search-icon-bg: #48484a; /* Original dark mode value */
|
|
@@ -165,23 +195,38 @@
|
|
|
165
195
|
--search-nothing-text: #eeeeee; /* Original dark mode value */
|
|
166
196
|
|
|
167
197
|
/* SearchResults load more colors */
|
|
168
|
-
--search-load-more-bg:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
255,
|
|
172
|
-
0.1
|
|
173
|
-
); /* Original dark mode value */
|
|
174
|
-
--search-load-more-hover-bg: rgba(
|
|
175
|
-
255,
|
|
176
|
-
255,
|
|
177
|
-
255,
|
|
178
|
-
0.1
|
|
179
|
-
); /* Original dark mode value */
|
|
180
|
-
--search-load-more-text: #eeeeee; /* Original dark mode value */
|
|
198
|
+
--search-load-more-bg: #606060; /* Updated dark mode value */
|
|
199
|
+
--search-load-more-hover-bg: #6e6e6e; /* Updated dark mode value */
|
|
200
|
+
--search-load-more-text: #b4b4b4; /* Updated dark mode value */
|
|
181
201
|
|
|
182
202
|
/* SearchResults highlight colors */
|
|
183
|
-
--search-highlight-bg:
|
|
203
|
+
--search-highlight-bg: #f6eeb4; /* Updated dark mode value */
|
|
184
204
|
|
|
185
205
|
/* SearchResults overlay colors */
|
|
186
|
-
--search-overlay-bg: rgba(
|
|
206
|
+
--search-overlay-bg: rgba(17, 17, 17, 0.6); /* Updated dark mode value */
|
|
207
|
+
--search-overlay-blur: blur(10px);
|
|
208
|
+
|
|
209
|
+
/* SearchOverlay panel colors */
|
|
210
|
+
--search-panel-bg: #3a3a3a; /* Updated dark mode value */
|
|
211
|
+
--search-panel-shadow: 0 0.824rem 1.765rem rgba(0, 0, 0, 0.4);
|
|
212
|
+
|
|
213
|
+
/* SearchOverlay input colors */
|
|
214
|
+
--search-input-header-bg: #484848;
|
|
215
|
+
--search-input-header-filled-bg: #606060;
|
|
216
|
+
--search-input-row-bg: #4a4a4a;
|
|
217
|
+
--search-input-row-border: 1px solid #ffffff;
|
|
218
|
+
--search-input-text: #ffffff;
|
|
219
|
+
--search-input-placeholder: #b4b4b4;
|
|
220
|
+
--search-input-filled-bg: #b4b4b4;
|
|
221
|
+
--search-input-filled-text: #000000;
|
|
222
|
+
--search-esc-bg: #7b7b7b;
|
|
223
|
+
--search-esc-text: #ffffff;
|
|
224
|
+
--search-esc-border: 1px solid #6e6e6e;
|
|
225
|
+
|
|
226
|
+
/* SearchOverlay result cell colors */
|
|
227
|
+
--search-result-active-bg: rgba(255, 255, 255, 0.12);
|
|
228
|
+
--search-path-hover-text: #ffffff;
|
|
229
|
+
--search-path-hover-underline: rgba(255, 255, 255, 0.7);
|
|
230
|
+
--search-highlight-text: #111111;
|
|
231
|
+
--search-term-text: #ffffff;
|
|
187
232
|
}
|