@lapis-notes/search 0.1.2 → 0.1.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Search changelog
2
2
 
3
+ ## 0.1.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [#28](https://github.com/lapismd/lapis-plugins/pull/28) [`d27467e`](https://github.com/lapismd/lapis-plugins/commit/d27467ee80c273011b7eb6ae792fa8e0330a11c1) Thanks [@stevejuma](https://github.com/stevejuma)! - Align shared presentation dependencies with the Design Core 0.6 public boundary used by current Notes hosts.
8
+
9
+ - [#28](https://github.com/lapismd/lapis-plugins/pull/28) [`0ed680e`](https://github.com/lapismd/lapis-plugins/commit/0ed680ef409b9bb179312d1b06cee02af56b3e8a) Thanks [@stevejuma](https://github.com/stevejuma)! - Use an explicitly selected native indexing observer for Search configuration,
10
+ refresh and progress while preserving standalone browser processing. Keep view
11
+ teardown passive, fence late continuations and recreate the manager when the
12
+ plugin is enabled again.
13
+
14
+ - [#28](https://github.com/lapismd/lapis-plugins/pull/28) [`56b5d6c`](https://github.com/lapismd/lapis-plugins/commit/56b5d6c1a016ef9bca43703b5fecf57f8234d1af) Thanks [@stevejuma](https://github.com/stevejuma)! - Consume published Design Core 0.4.0 with prepared browser CSS and bundled font assets through normal npm ranges.
15
+
16
+ Restore Bases picker input/list accessibility relationships and name property actions.
17
+
18
+ - [#28](https://github.com/lapismd/lapis-plugins/pull/28) [`c9febb1`](https://github.com/lapismd/lapis-plugins/commit/c9febb1c8b431db30ecdcf7507cfff8f727bc4ed) Thanks [@stevejuma](https://github.com/stevejuma)! - Consume the published Notes next packages, Mira 0.1.2, and Design Core 0.7.0 through the existing semver floors. Markdown dependents accept ^0.2.0. History keeps its public repository entrypoints, and Bases keeps the file-icon contract on API 0.2.0.
19
+
20
+ - [#28](https://github.com/lapismd/lapis-plugins/pull/28) [`d463ee8`](https://github.com/lapismd/lapis-plugins/commit/d463ee8c5b48ee0f3a8992c7394f48124fb2cc1c) Thanks [@stevejuma](https://github.com/stevejuma)! - Send every Search command, background-indexing, settings-recovery and
21
+ copy-feedback notice through the explicitly owned App so a concurrent
22
+ compatibility host cannot receive delayed errors.
23
+
24
+ - [#28](https://github.com/lapismd/lapis-plugins/pull/28) [`5bb36e5`](https://github.com/lapismd/lapis-plugins/commit/5bb36e5e01b7e6fb928955edc41426c12c4e5d55) Thanks [@stevejuma](https://github.com/stevejuma)! - Await Search panel teardown before the owning view finishes closing.
25
+
26
+ - [#28](https://github.com/lapismd/lapis-plugins/pull/28) [`c2d1149`](https://github.com/lapismd/lapis-plugins/commit/c2d11499114ec92a2c168c4ec9a1efd6cbc01eee) Thanks [@stevejuma](https://github.com/stevejuma)! - Refresh Search controls after settings recovery and preserve recovered values
27
+ when saving another preference. Reconcile recovered native indexing settings
28
+ through the existing host controls.
29
+
30
+ ## 0.1.3
31
+
32
+ ### Patch Changes
33
+
34
+ - Rebuild Search after its accepted metadata inputs or parsers change, including
35
+ when metadata reconciliation clears derived documents. Retain warm checkpoints
36
+ for metadata files without a registered Search provider.
37
+
38
+ - Add official author avatars and deterministic split, light, and dark registry media for plugin banners, overviews, and galleries.
39
+
3
40
  ## 0.1.2
4
41
 
5
42
  - Initial independent public package extracted from Lapis Notes without changing
package/LICENSE.md CHANGED
@@ -1,8 +1,4 @@
1
- # License
1
+ Apache-2.0
2
2
 
3
- Copyright (c) LapisMD contributors.
4
-
5
- This software is licensed under the GNU Affero General Public License, version
6
- 3 or (at your option) any later version (`AGPL-3.0-or-later`). The complete
7
- license text is available from the
8
- [GNU Project](https://www.gnu.org/licenses/agpl-3.0.html).
3
+ This project is licensed under the Apache License 2.0.
4
+ See <https://www.apache.org/licenses/LICENSE-2.0>.
package/README.md CHANGED
@@ -13,7 +13,7 @@ Register the exported plugin class in the application's `PluginProfile`. Import
13
13
  registration so the host owns its lifecycle.
14
14
 
15
15
  The runtime plugin ID is `search`. A matching signed
16
- `search-0.1.2.lapis-plugin` archive is attached to the
16
+ `search-0.1.4.lapis-plugin` archive is attached to the
17
17
  package-scoped GitHub release for manual or registry installation.
18
18
 
19
19
  See the [repository README](https://github.com/lapismd/lapis-plugins#readme) for
@@ -0,0 +1,5 @@
1
+ import type { App, NativeBackgroundIndexingObserver } from "@lapis-notes/api";
2
+ import type { SearchPluginSettings } from "./search-settings";
3
+ export declare function hasNativeSearch(app: App): boolean;
4
+ export declare function nativeSearch(app: App): NativeBackgroundIndexingObserver;
5
+ export declare function configureNativeSearch(app: App, settings: SearchPluginSettings): Promise<NativeBackgroundIndexingObserver>;
@@ -0,0 +1,24 @@
1
+ export function hasNativeSearch(app) {
2
+ return Boolean(app.session?.backgroundIndexing);
3
+ }
4
+ export function nativeSearch(app) {
5
+ const observer = app.backgroundIndexing;
6
+ if (!observer)
7
+ throw new Error("The selected native Search observer is unavailable");
8
+ return observer;
9
+ }
10
+ export async function configureNativeSearch(app, settings) {
11
+ const observer = nativeSearch(app);
12
+ const contribution = await observer.selectSearch({
13
+ providers: app.searchDocumentProviders.getAll().map((provider) => ({
14
+ provider_id: provider.id,
15
+ version: provider.version ?? "1",
16
+ priority: provider.priority ?? 0,
17
+ })),
18
+ chunking: { ...settings.chunking },
19
+ });
20
+ if (contribution.placement !== "native") {
21
+ throw new Error(contribution.message ?? "Native Search cannot execute this provider");
22
+ }
23
+ return observer;
24
+ }
@@ -0,0 +1,197 @@
1
+ <script lang="ts">
2
+ import * as Collapsible from "@lapismd/design-core/shadcn/collapsible";
3
+ import * as Sidebar from "@lapismd/design-core/shadcn/sidebar";
4
+ import { Badge } from "@lapismd/design-core/shadcn/badge";
5
+ import { Button } from "@lapismd/design-core/shadcn/button";
6
+ import ChevronUp from "@lucide/svelte/icons/chevron-up";
7
+ import ChevronDown from "@lucide/svelte/icons/chevron-down";
8
+ import ChevronRight from "@lucide/svelte/icons/chevron-right";
9
+ import type {
10
+ SearchProviderHit,
11
+ SearchProviderMatch,
12
+ SearchResultProvider,
13
+ } from "@lapis-notes/api";
14
+ import type { ProviderVersion } from "./search-provider-groups";
15
+ import HighlightedText from "./highlighted-text.svelte";
16
+ import { onDestroy } from "svelte";
17
+ const treeId = $props.id();
18
+ let {
19
+ versions,
20
+ report,
21
+ }: { versions: ProviderVersion[]; report(error: unknown): void } = $props();
22
+ let expanded = $state<Record<string, boolean>>({});
23
+ let contexts = $state<Record<string, SearchProviderMatch>>({});
24
+ let loading = $state<Record<string, boolean>>({});
25
+ const abort = new AbortController();
26
+ onDestroy(() => abort.abort());
27
+ const key = (provider: SearchResultProvider, hit: SearchProviderHit) =>
28
+ `${provider.id}/${hit.id}`;
29
+ async function open(
30
+ provider: SearchResultProvider,
31
+ hit: SearchProviderHit,
32
+ match?: SearchProviderMatch,
33
+ ) {
34
+ try {
35
+ await provider.activate(hit, match);
36
+ } catch (error) {
37
+ report(error);
38
+ }
39
+ }
40
+ async function context(
41
+ provider: SearchResultProvider,
42
+ hit: SearchProviderHit,
43
+ match: SearchProviderMatch,
44
+ direction: "before" | "after",
45
+ ) {
46
+ if (!provider.expand) return;
47
+ const id = `${key(provider, hit)}/${match.id}`;
48
+ loading[id] = true;
49
+ try {
50
+ const next = await provider.expand(hit, match, direction, abort.signal);
51
+ if (!abort.signal.aborted) contexts[id] = next;
52
+ } catch (error) {
53
+ if (!abort.signal.aborted) report(error);
54
+ } finally {
55
+ if (!abort.signal.aborted) loading[id] = false;
56
+ }
57
+ }
58
+ </script>
59
+
60
+ <Sidebar.MenuSub role="group" class="search-panel__history-list">
61
+ {#each versions as { provider, hit }, index (`${provider.id}/${hit.id}`)}
62
+ {@const id = key(provider, hit)}
63
+ <Sidebar.MenuSubItem
64
+ role="treeitem"
65
+ aria-selected="false"
66
+ aria-level={2}
67
+ aria-expanded={expanded[id] ?? false}
68
+ >
69
+ <Collapsible.Root
70
+ open={expanded[id] ?? false}
71
+ onOpenChange={(value) => (expanded[id] = value)}
72
+ >
73
+ <Collapsible.Trigger
74
+ class="search-panel__history-version"
75
+ id={`${treeId}-${index}-label`}
76
+ aria-label={`History ${hit.version?.label ?? hit.title}${hit.timestamp ? ` ${new Date(hit.timestamp).toLocaleDateString()}` : ""}`}
77
+ aria-expanded={expanded[id] ?? false}
78
+ onkeydown={(event) => {
79
+ if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
80
+ event.preventDefault();
81
+ expanded[id] = event.key === "ArrowRight";
82
+ }
83
+ }}
84
+ >
85
+ <ChevronRight class="search-panel__disclosure" aria-hidden="true" />
86
+ <Badge variant="outline">History</Badge>
87
+ <span class="search-panel__history-label"
88
+ >{hit.version?.label ?? hit.title}</span
89
+ >
90
+ {#if hit.timestamp}<time
91
+ datetime={new Date(hit.timestamp).toISOString()}
92
+ >{new Date(hit.timestamp).toLocaleDateString()}</time
93
+ >{/if}
94
+ </Collapsible.Trigger>
95
+ <Collapsible.Content
96
+ class="search-panel__history-content"
97
+ role="group"
98
+ id={`${treeId}-${index}`}
99
+ >
100
+ <div class="search-panel__history-details">
101
+ <Button
102
+ variant="ghost"
103
+ size="sm"
104
+ onclick={() => open(provider, hit)}
105
+ aria-label={`Open saved version ${hit.version?.label ?? hit.title}: ${hit.version?.author ?? provider.label}${hit.version ? ` · ${hit.version.id.slice(0, 12)}` : ""}`}
106
+ >
107
+ {hit.version?.author ?? provider.label}{hit.version
108
+ ? ` · ${hit.version.id.slice(0, 12)}`
109
+ : ""}
110
+ </Button>
111
+ {#each hit.badges ?? [] as badge}<Badge variant="outline"
112
+ >{badge}</Badge
113
+ >{/each}
114
+ {#if (hit.version?.occurrences ?? 0) > 1}<Badge variant="outline"
115
+ >{hit.version?.occurrences} checkpoints</Badge
116
+ >{/if}
117
+ </div>
118
+ {#if hit.version?.references && hit.version.references.length > 1}
119
+ <div
120
+ class="search-panel__history-details"
121
+ aria-label="Matching checkpoints"
122
+ >
123
+ {#each hit.version.references as reference (reference.id)}
124
+ <Button
125
+ variant="ghost"
126
+ size="sm"
127
+ onclick={() =>
128
+ open(provider, { ...hit, target: reference.target })}
129
+ >{reference.id.slice(0, 12)} · {new Date(
130
+ reference.timestamp,
131
+ ).toLocaleDateString()}</Button
132
+ >
133
+ {/each}
134
+ </div>
135
+ {/if}
136
+ <Sidebar.MenuSub role="group" class="search-panel__history-matches">
137
+ {#each hit.matches as original (original.id)}
138
+ {@const matchId = `${id}/${original.id}`}
139
+ {@const match = contexts[matchId] ?? original}
140
+ <Sidebar.MenuSubItem role="none">
141
+ <div
142
+ class="search-panel__match-shell"
143
+ role="treeitem"
144
+ aria-selected="false"
145
+ aria-level={3}
146
+ aria-label={`${match.text} ${match.field}`}
147
+ tabindex={0}
148
+ onclick={() => open(provider, hit, match)}
149
+ onkeydown={(event) => {
150
+ if (event.key === "Enter" || event.key === " ") {
151
+ event.preventDefault();
152
+ void open(provider, hit, match);
153
+ }
154
+ }}
155
+ >
156
+ {#if match.context && provider.expand}<Button
157
+ size="icon-xs"
158
+ variant="ghost"
159
+ aria-label="Show more context before this saved match"
160
+ disabled={loading[matchId]}
161
+ onclick={(event) => {
162
+ event.stopPropagation();
163
+ void context(provider, hit, match, "before");
164
+ }}><ChevronUp aria-hidden="true" /></Button
165
+ >{/if}
166
+ <div class="search-panel__match">
167
+ <span class="search-panel__match-text"
168
+ ><HighlightedText
169
+ text={match.text}
170
+ ranges={match.ranges}
171
+ /></span
172
+ >
173
+ <span class="search-panel__match-meta"
174
+ ><Badge variant="outline" class="search-panel__match-key"
175
+ >{match.field}</Badge
176
+ ></span
177
+ >
178
+ </div>
179
+ {#if match.context && provider.expand}<Button
180
+ size="icon-xs"
181
+ variant="ghost"
182
+ aria-label="Show more context after this saved match"
183
+ disabled={loading[matchId]}
184
+ onclick={(event) => {
185
+ event.stopPropagation();
186
+ void context(provider, hit, match, "after");
187
+ }}><ChevronDown aria-hidden="true" /></Button
188
+ >{/if}
189
+ </div>
190
+ </Sidebar.MenuSubItem>
191
+ {/each}
192
+ </Sidebar.MenuSub>
193
+ </Collapsible.Content>
194
+ </Collapsible.Root>
195
+ </Sidebar.MenuSubItem>
196
+ {/each}
197
+ </Sidebar.MenuSub>
@@ -0,0 +1,8 @@
1
+ import type { ProviderVersion } from "./search-provider-groups";
2
+ type $$ComponentProps = {
3
+ versions: ProviderVersion[];
4
+ report(error: unknown): void;
5
+ };
6
+ declare const SearchHistoryVersions: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type SearchHistoryVersions = ReturnType<typeof SearchHistoryVersions>;
8
+ export default SearchHistoryVersions;
@@ -44,6 +44,7 @@ export interface SearchQueryResult {
44
44
  export declare class SearchManager {
45
45
  readonly app: App;
46
46
  private readonly getSettings;
47
+ private observedNativeRefreshActive;
47
48
  private refreshPromise;
48
49
  private queuedRefreshReason;
49
50
  private refreshState;
@@ -71,6 +72,8 @@ export declare class SearchManager {
71
72
  private runRefreshQueue;
72
73
  trackChanges(): () => void;
73
74
  dispose(): Promise<void>;
75
+ configureIndexing(): Promise<void>;
76
+ private refreshNative;
74
77
  private processFile;
75
78
  private projectionSignature;
76
79
  private currentReconcileFingerprint;
@@ -1,5 +1,6 @@
1
1
  import { debounce, md5, } from "@lapis-notes/api";
2
2
  import { DEFAULT_SEARCH_SETTINGS, } from "./search-settings";
3
+ import { configureNativeSearch, hasNativeSearch, nativeSearch, } from "./native-search";
3
4
  const REACTIVE_INDEX_DELAY_MS = 75;
4
5
  const SEARCH_RECONCILE_CHECKPOINT_KEY = "search.reconcile-checkpoint.v1";
5
6
  const SEARCH_RECONCILE_CHECKPOINT_VERSION = 1;
@@ -80,6 +81,7 @@ function sourceMetadata(cache, settings, source, manifest) {
80
81
  export class SearchManager {
81
82
  app;
82
83
  getSettings;
84
+ observedNativeRefreshActive = false;
83
85
  refreshPromise = null;
84
86
  queuedRefreshReason = null;
85
87
  refreshState = {
@@ -113,6 +115,10 @@ export class SearchManager {
113
115
  }
114
116
  }
115
117
  async processChange(file, content, cache) {
118
+ if (hasNativeSearch(this.app)) {
119
+ nativeSearch(this.app);
120
+ return;
121
+ }
116
122
  const provider = this.app.searchDocumentProviders.resolve(file);
117
123
  if (!provider) {
118
124
  await this.processDelete(file);
@@ -159,6 +165,10 @@ export class SearchManager {
159
165
  });
160
166
  }
161
167
  async processDelete(file) {
168
+ if (hasNativeSearch(this.app)) {
169
+ nativeSearch(this.app);
170
+ return;
171
+ }
162
172
  await this.app.appDatabase.deleteSearchDocument(typeof file === "string" ? file : file.path);
163
173
  }
164
174
  async query(params) {
@@ -211,7 +221,9 @@ export class SearchManager {
211
221
  span.setAttribute("search.result.count", result.count);
212
222
  span.setAttribute("search.path_prefix", Boolean(params.pathPrefix));
213
223
  span.setAttribute("search.source_provider.count", params.sourceProviderIds?.length ?? 0);
214
- span.setAttribute("search.reconcile.active", this.refreshState.active);
224
+ span.setAttribute("search.reconcile.active", hasNativeSearch(this.app)
225
+ ? this.observedNativeRefreshActive
226
+ : this.refreshState.active);
215
227
  return result;
216
228
  }, {
217
229
  attributes: {
@@ -220,26 +232,42 @@ export class SearchManager {
220
232
  });
221
233
  }
222
234
  async getStatus() {
223
- const [provider, runtime, stats] = await Promise.all([
235
+ const selected = hasNativeSearch(this.app);
236
+ const [provider, runtime, stats, native] = await Promise.all([
224
237
  this.app.appDatabase.getSearchEmbeddingProvider(),
225
238
  this.app.appDatabase.getSearchEmbeddingRuntimeStatus(),
226
239
  this.app.appDatabase.getSearchIndexStats(),
240
+ selected ? nativeSearch(this.app).getStatus() : null,
227
241
  ]);
242
+ const nativeRefreshing = Boolean(native && (native.active || native.pending > 0));
243
+ if (selected)
244
+ this.observedNativeRefreshActive = nativeRefreshing;
228
245
  return {
229
246
  backendKind: this.app.appDatabase.kind,
230
247
  provider,
231
248
  runtime,
232
249
  ...stats,
233
- isRefreshing: this.refreshState.active,
234
- refreshReason: this.refreshState.reason,
250
+ lastError: native?.error ?? stats.lastError,
251
+ isRefreshing: selected ? nativeRefreshing : this.refreshState.active,
252
+ refreshReason: selected
253
+ ? nativeRefreshing
254
+ ? (this.refreshState.reason ?? "native-indexing")
255
+ : null
256
+ : this.refreshState.reason,
235
257
  refreshProgress: {
236
- processed: this.refreshState.processed,
237
- total: this.refreshState.total,
258
+ processed: selected
259
+ ? (native?.processed ?? 0)
260
+ : this.refreshState.processed,
261
+ total: selected ? (native?.total ?? 0) : this.refreshState.total,
238
262
  },
239
- refreshedAt: this.refreshState.refreshedAt,
263
+ refreshedAt: selected
264
+ ? (native?.completed_at ?? null)
265
+ : this.refreshState.refreshedAt,
240
266
  };
241
267
  }
242
268
  refreshFromVault(reason = "manual-refresh") {
269
+ if (hasNativeSearch(this.app))
270
+ return this.refreshNative(reason);
243
271
  this.queuedRefreshReason = reason;
244
272
  this.refreshPromise ??= this.runRefreshQueue().finally(() => {
245
273
  this.refreshPromise = null;
@@ -247,6 +275,8 @@ export class SearchManager {
247
275
  return this.refreshPromise;
248
276
  }
249
277
  async reconcileStartup() {
278
+ if (hasNativeSearch(this.app))
279
+ return this.refreshNative("startup");
250
280
  const execute = async (setCheckpoint) => {
251
281
  const initialGeneration = this.sourceGeneration;
252
282
  let status;
@@ -294,6 +324,10 @@ export class SearchManager {
294
324
  return status ?? this.getStatus();
295
325
  }
296
326
  trackChanges() {
327
+ if (hasNativeSearch(this.app)) {
328
+ nativeSearch(this.app);
329
+ return () => { };
330
+ }
297
331
  const changed = this.app.metadataCache.on("changed", (file, content, cache) => {
298
332
  if (!this.app.searchDocumentProviders.resolve(file))
299
333
  return;
@@ -378,6 +412,12 @@ export class SearchManager {
378
412
  if (this.disposed)
379
413
  return;
380
414
  this.flushQueuedChanges.cancel();
415
+ if (hasNativeSearch(this.app)) {
416
+ this.disposed = true;
417
+ this.queuedChanges.clear();
418
+ this.queuedDeletes.clear();
419
+ return;
420
+ }
381
421
  if (this.queuedChanges.size || this.queuedDeletes.size) {
382
422
  await this.drainQueuedChanges();
383
423
  }
@@ -391,6 +431,31 @@ export class SearchManager {
391
431
  this.queuedChanges.clear();
392
432
  this.queuedDeletes.clear();
393
433
  }
434
+ async configureIndexing() {
435
+ if (this.disposed)
436
+ return;
437
+ if (hasNativeSearch(this.app))
438
+ await configureNativeSearch(this.app, this.getSettings());
439
+ }
440
+ async refreshNative(reason) {
441
+ if (this.disposed)
442
+ throw new Error("Search manager is disposed");
443
+ const previous = await this.getStatus();
444
+ if (this.disposed)
445
+ return previous;
446
+ const observer = await configureNativeSearch(this.app, this.getSettings());
447
+ if (this.disposed || observer.detached)
448
+ return previous;
449
+ this.refreshState.reason = reason;
450
+ await observer.refresh(reason.includes("rebuild"), {
451
+ title: "Refreshing search index",
452
+ source: "Search",
453
+ readyMessage: "Search index ready",
454
+ });
455
+ if (this.disposed || observer.detached)
456
+ return previous;
457
+ return this.getStatus();
458
+ }
394
459
  async processFile(file) {
395
460
  if (!this.app.searchDocumentProviders.resolve(file)) {
396
461
  await this.processDelete(file);
@@ -409,6 +474,10 @@ export class SearchManager {
409
474
  }
410
475
  currentReconcileFingerprint() {
411
476
  const fingerprint = new StreamingManifestFingerprint();
477
+ fingerprint.add([
478
+ "metadata",
479
+ this.app.metadataCache.getReconciliationFingerprint((file) => Boolean(this.app.searchDocumentProviders.resolve(file))),
480
+ ].join("\u0000"));
412
481
  const providers = this.app.searchDocumentProviders
413
482
  .getAll()
414
483
  .sort((left, right) => left.id.localeCompare(right.id));
@@ -19,6 +19,7 @@
19
19
  color: var(--ui-workspace-view-foreground, inherit);
20
20
  background: var(--ui-workspace-view-background, transparent);
21
21
  font: inherit;
22
+ container: search-panel / inline-size;
22
23
  }
23
24
 
24
25
  .search-panel__chrome {
@@ -92,6 +93,18 @@
92
93
  white-space: nowrap;
93
94
  }
94
95
 
96
+ @container search-panel (max-width: 24rem) {
97
+ .search-panel__sort {
98
+ width: 2rem;
99
+ flex: 0 0 auto;
100
+ padding-inline: 0;
101
+ }
102
+
103
+ .search-panel__sort-label {
104
+ display: none;
105
+ }
106
+ }
107
+
95
108
  .search-panel__sort-popover {
96
109
  display: grid;
97
110
  width: 15rem;
@@ -458,3 +471,93 @@
458
471
  font-size: 0.8125rem;
459
472
  text-align: center;
460
473
  }
474
+
475
+ .search-panel__source {
476
+ margin-top: 0.5rem;
477
+ }
478
+ .search-panel__history-version {
479
+ display: grid;
480
+ grid-template-columns: 0.875rem auto minmax(0, 1fr) auto;
481
+ align-items: center;
482
+ gap: 0.375rem;
483
+ width: 100%;
484
+ min-width: 0;
485
+ padding: 0.375rem 0.5rem;
486
+ color: inherit;
487
+ font: inherit;
488
+ font-size: 0.8125rem;
489
+ line-height: 1.25rem;
490
+ background: transparent;
491
+ border: 0;
492
+ text-align: left;
493
+ border-radius: 0.25rem;
494
+ }
495
+ .search-panel__history-list,
496
+ .search-panel__history-matches {
497
+ display: grid;
498
+ gap: 0.125rem;
499
+ width: 100%;
500
+ min-width: 0;
501
+ margin: 0;
502
+ padding: 0;
503
+ border: 0;
504
+ list-style: none;
505
+ }
506
+ .search-panel__history-version > svg {
507
+ width: 0.875rem;
508
+ height: 0.875rem;
509
+ }
510
+ .search-panel__history-version[aria-expanded="true"] .search-panel__disclosure {
511
+ transform: rotate(90deg);
512
+ }
513
+ .search-panel__history-content {
514
+ padding: 0.125rem 0.5rem 0.5rem 1.375rem;
515
+ }
516
+ .search-panel__history-version:hover {
517
+ background: var(
518
+ --ui-workspace-hover,
519
+ color-mix(in srgb, currentColor 8%, transparent)
520
+ );
521
+ }
522
+ .search-panel__history-label {
523
+ flex: 1;
524
+ min-width: 0;
525
+ overflow: hidden;
526
+ text-overflow: ellipsis;
527
+ white-space: nowrap;
528
+ }
529
+ .search-panel__history-version time {
530
+ color: var(--search-panel-muted);
531
+ font-size: 0.7rem;
532
+ white-space: nowrap;
533
+ }
534
+ .search-panel__history-details {
535
+ display: flex;
536
+ flex-wrap: wrap;
537
+ align-items: center;
538
+ gap: 0.25rem;
539
+ padding-bottom: 0.25rem;
540
+ color: var(--search-panel-muted);
541
+ font-size: 0.6875rem;
542
+ }
543
+ .search-panel__history-details button {
544
+ height: auto;
545
+ min-height: 1.5rem;
546
+ padding: 0.125rem 0.375rem;
547
+ font-size: inherit;
548
+ }
549
+
550
+ .search-panel__loading {
551
+ display: grid;
552
+ gap: 0.5rem;
553
+ padding: 0.75rem;
554
+ }
555
+ .search-panel__loading-title {
556
+ height: 0.8rem;
557
+ width: 55%;
558
+ }
559
+ .search-panel__loading-snippet {
560
+ height: 0.65rem;
561
+ width: 85%;
562
+ margin-bottom: 0.6rem;
563
+ }