@lapis-notes/workspace 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/LICENSE.md +3 -3
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.js +3 -0
  5. package/dist/plugin-management/CommunityPluginsSettingsPage.svelte +152 -73
  6. package/dist/plugin-management/PluginBrowser.svelte +558 -0
  7. package/dist/plugin-management/PluginBrowser.svelte.d.ts +17 -0
  8. package/dist/plugin-management/PluginBrowserCard.svelte +230 -0
  9. package/dist/plugin-management/PluginBrowserCard.svelte.d.ts +9 -0
  10. package/dist/plugin-management/PluginBrowserDetail.svelte +1168 -0
  11. package/dist/plugin-management/PluginBrowserDetail.svelte.d.ts +20 -0
  12. package/dist/plugin-management/PluginBrowserDirectory.svelte +1069 -0
  13. package/dist/plugin-management/PluginBrowserDirectory.svelte.d.ts +21 -0
  14. package/dist/plugin-management/PluginBrowserLanding.svelte +341 -0
  15. package/dist/plugin-management/PluginBrowserLanding.svelte.d.ts +12 -0
  16. package/dist/plugin-management/PluginRegistryBadge.svelte +41 -0
  17. package/dist/plugin-management/PluginRegistryBadge.svelte.d.ts +8 -0
  18. package/dist/plugin-management/PluginRegistryContentState.svelte +80 -0
  19. package/dist/plugin-management/PluginRegistryContentState.svelte.d.ts +14 -0
  20. package/dist/plugin-management/PluginRegistryImage.svelte +64 -0
  21. package/dist/plugin-management/PluginRegistryImage.svelte.d.ts +9 -0
  22. package/dist/plugin-management/PluginRegistryMedia.svelte +223 -0
  23. package/dist/plugin-management/PluginRegistryMedia.svelte.d.ts +8 -0
  24. package/dist/plugin-management/PluginRegistryMediaViewer.svelte +155 -0
  25. package/dist/plugin-management/PluginRegistryMediaViewer.svelte.d.ts +13 -0
  26. package/dist/plugin-management/PluginRegistryRow.svelte +120 -0
  27. package/dist/plugin-management/PluginRegistryRow.svelte.d.ts +25 -0
  28. package/dist/plugin-management/PluginRegistrySettingsPage.svelte +1009 -512
  29. package/dist/plugin-management/PluginRegistryStatus.svelte +33 -0
  30. package/dist/plugin-management/PluginRegistryStatus.svelte.d.ts +8 -0
  31. package/dist/plugin-management/PluginRegistryToolbar.svelte +159 -0
  32. package/dist/plugin-management/PluginRegistryToolbar.svelte.d.ts +21 -0
  33. package/dist/plugin-management/community-presentation/CommunityChip.svelte +229 -0
  34. package/dist/plugin-management/community-presentation/CommunityChip.svelte.d.ts +4 -0
  35. package/dist/plugin-management/community-presentation/CommunityMemberAvatar.svelte +42 -0
  36. package/dist/plugin-management/community-presentation/CommunityMemberAvatar.svelte.d.ts +13 -0
  37. package/dist/plugin-management/community-presentation/RegistryMediaGallery.svelte +282 -0
  38. package/dist/plugin-management/community-presentation/RegistryMediaGallery.svelte.d.ts +8 -0
  39. package/dist/plugin-management/community-presentation/RegistryMediaViewer.css +179 -0
  40. package/dist/plugin-management/community-presentation/RegistryMediaViewer.svelte +241 -0
  41. package/dist/plugin-management/community-presentation/RegistryMediaViewer.svelte.d.ts +14 -0
  42. package/dist/plugin-management/community-presentation/RegistryPluginChangelog.svelte +216 -0
  43. package/dist/plugin-management/community-presentation/RegistryPluginChangelog.svelte.d.ts +8 -0
  44. package/dist/plugin-management/community-presentation/RegistryVerifiedImage.svelte +179 -0
  45. package/dist/plugin-management/community-presentation/RegistryVerifiedImage.svelte.d.ts +15 -0
  46. package/dist/plugin-management/community-presentation/avatar-model.d.ts +14 -0
  47. package/dist/plugin-management/community-presentation/avatar-model.js +21 -0
  48. package/dist/plugin-management/community-presentation/community-member-avatar.css +137 -0
  49. package/dist/plugin-management/community-presentation/community-typography.css +30 -0
  50. package/dist/plugin-management/community-presentation/model-adapter.d.ts +54 -0
  51. package/dist/plugin-management/community-presentation/model-adapter.js +90 -0
  52. package/dist/plugin-management/community-presentation/model.d.ts +9 -0
  53. package/dist/plugin-management/community-presentation/model.js +40 -0
  54. package/dist/plugin-management/community-presentation/props.d.ts +21 -0
  55. package/dist/plugin-management/community-presentation/props.js +1 -0
  56. package/dist/plugin-management/community-presentation/registry-filter-icons.d.ts +3 -0
  57. package/dist/plugin-management/community-presentation/registry-filter-icons.js +70 -0
  58. package/dist/plugin-management/community-presentation/registry-markdown.d.ts +1 -0
  59. package/dist/plugin-management/community-presentation/registry-markdown.js +57 -0
  60. package/dist/plugin-management/community-presentation/registry-plugin-icons.d.ts +2 -0
  61. package/dist/plugin-management/community-presentation/registry-plugin-icons.js +31 -0
  62. package/dist/plugin-management/plugin-browser-model.d.ts +44 -0
  63. package/dist/plugin-management/plugin-browser-model.js +104 -0
  64. package/dist/plugin-management/plugin-browser-navigation.d.ts +3 -0
  65. package/dist/plugin-management/plugin-browser-navigation.js +38 -0
  66. package/dist/plugin-management/plugin-download-stats.d.ts +1 -0
  67. package/dist/plugin-management/plugin-download-stats.js +15 -0
  68. package/dist/plugin-management/plugin-management.css +1088 -117
  69. package/dist/plugin-management/plugin-registry-controller.svelte.d.ts +29 -0
  70. package/dist/plugin-management/plugin-registry-controller.svelte.js +154 -0
  71. package/dist/plugin-management/plugin-registry-detail-session.svelte.d.ts +18 -0
  72. package/dist/plugin-management/plugin-registry-detail-session.svelte.js +106 -0
  73. package/dist/plugin-management/plugin-registry-detail.d.ts +17 -0
  74. package/dist/plugin-management/plugin-registry-detail.js +1 -0
  75. package/dist/plugin-management/plugins-view.svelte.d.ts +41 -0
  76. package/dist/plugin-management/plugins-view.svelte.js +109 -0
  77. package/package.json +5 -5
@@ -0,0 +1,558 @@
1
+ <script lang="ts">
2
+ import { onMount, tick, untrack } from "svelte";
3
+ import {
4
+ pluginDownloadCounts,
5
+ type App,
6
+ type PluginCatalogEntry,
7
+ } from "@lapis-notes/api";
8
+ import { AppShell, AppShellController } from "@lapismd/design-core/shell";
9
+ import { Button } from "@lapismd/design-core/shadcn/button";
10
+ import * as Tooltip from "@lapismd/design-core/shadcn/tooltip";
11
+ import ArrowLeft from "@lucide/svelte/icons/arrow-left";
12
+ import * as Breadcrumb from "@lapismd/design-core/shadcn/breadcrumb";
13
+ import {
14
+ communityDetail,
15
+ type RegistryInstallAction,
16
+ } from "./community-presentation/model-adapter";
17
+ import PluginReadmeRenderer from "./PluginReadmeRenderer.svelte";
18
+ import { getPluginRegistryController } from "./plugin-registry-controller.svelte";
19
+ import { PluginRegistryDetailSession } from "./plugin-registry-detail-session.svelte";
20
+ import { defaultDirectoryState } from "./plugin-browser-model";
21
+ import type {
22
+ PluginBrowserRoute,
23
+ PluginDirectoryState,
24
+ } from "./plugin-browser-model";
25
+ import { formatApproximateDownloadCount } from "./plugin-download-stats";
26
+ import PluginBrowserLanding from "./PluginBrowserLanding.svelte";
27
+ import PluginBrowserDirectory from "./PluginBrowserDirectory.svelte";
28
+ import PluginRegistryContentState from "./PluginRegistryContentState.svelte";
29
+ import PluginBrowserDetail from "./PluginBrowserDetail.svelte";
30
+ import "./community-presentation/community-typography.css";
31
+ export interface PluginBrowserNavigation {
32
+ route: PluginBrowserRoute;
33
+ directory: PluginDirectoryState;
34
+ returnPage: "landing" | "directory";
35
+ scrollTop: number;
36
+ }
37
+ let {
38
+ app,
39
+ navigation,
40
+ onstatechange,
41
+ }: {
42
+ app: App;
43
+ navigation: PluginBrowserNavigation;
44
+ onstatechange?: () => void;
45
+ } = $props();
46
+ const ownedApp = untrack(() => app);
47
+ const registry = getPluginRegistryController(ownedApp);
48
+ const session = new PluginRegistryDetailSession(ownedApp);
49
+ let contentRegion = $state<HTMLDivElement | null>(null);
50
+ const scroller = $derived(
51
+ contentRegion?.querySelector<HTMLElement>(
52
+ '[data-ui-part="scroll-area-viewport"]',
53
+ ),
54
+ );
55
+ $effect(() => {
56
+ const page = navigation.route.page;
57
+ const region = scroller;
58
+ const scrollTop = untrack(() => navigation.scrollTop);
59
+ if (region)
60
+ void tick().then(() => {
61
+ if (navigation.route.page === page)
62
+ region.scrollTop = page === "plugin" ? 0 : scrollTop;
63
+ });
64
+ });
65
+ let previousDetail = "";
66
+ const installedIds = $derived(
67
+ new Set(registry.installed.map((record) => record.pluginId)),
68
+ );
69
+ const bundledIds = $derived(
70
+ new Set(ownedApp.plugins.corePluginEntries.map((p) => p.manifest.id)),
71
+ );
72
+ const updateIds = $derived(
73
+ new Set(
74
+ registry.updates
75
+ .filter((update) => update.canUpdate && !bundledIds.has(update.id))
76
+ .map((update) => update.id),
77
+ ),
78
+ );
79
+ const availableIds = $derived(new Set([...installedIds, ...bundledIds]));
80
+ const selectedId = $derived(
81
+ navigation.route.page === "plugin" ? navigation.route.pluginId : null,
82
+ );
83
+ const selected = $derived(
84
+ registry.catalogEntries.find((p) => p.id === selectedId) ?? null,
85
+ );
86
+ const update = $derived(
87
+ registry.updates.find((p) => p.id === selected?.id) ?? null,
88
+ );
89
+ const shell = untrack(
90
+ () =>
91
+ new AppShellController({
92
+ rightClosed: true,
93
+ sidebarMinWidth: 48,
94
+ sidebarMaxWidth: 440,
95
+ }),
96
+ );
97
+ const selectedVersion = $derived(
98
+ navigation.route.page === "plugin" ? navigation.route.version : undefined,
99
+ );
100
+ const resolved = $derived(
101
+ selected && session.detail
102
+ ? communityDetail(selected, session.detail)
103
+ : null,
104
+ );
105
+ const related = $derived(
106
+ selected
107
+ ? registry.catalogEntries
108
+ .filter(
109
+ (entry) =>
110
+ entry.id !== selected.id &&
111
+ entry.categories.some((category) =>
112
+ selected.categories.includes(category),
113
+ ),
114
+ )
115
+ .slice(0, 3)
116
+ : [],
117
+ );
118
+ const installAction = $derived.by<RegistryInstallAction>(() => {
119
+ if (!selected) return { state: "disabled" };
120
+ const feedback =
121
+ registry.actionFeedback?.pluginId === selected.id
122
+ ? registry.actionFeedback
123
+ : null;
124
+ if (
125
+ registry.runningAction === `install:${selected.id}` ||
126
+ registry.runningAction === `update:${selected.id}`
127
+ )
128
+ return { state: "installing" };
129
+ if (feedback?.state === "error")
130
+ return { state: "rejected", reason: feedback.message };
131
+ if (bundledIds.has(selected.id))
132
+ return { state: "installed", label: "Bundled" };
133
+ const installed = registry.installed.find(
134
+ (record) => record.pluginId === selected.id,
135
+ );
136
+ const requestedVersion = selectedVersion ?? selected.latestVersion;
137
+ if (installed?.installedVersion === requestedVersion)
138
+ return {
139
+ state: "installed",
140
+ installedVersion: installed.installedVersion,
141
+ };
142
+ if (update?.canUpdate && requestedVersion === selected.latestVersion)
143
+ return {
144
+ state: "available",
145
+ label: "Update",
146
+ installedVersion: installed?.installedVersion,
147
+ };
148
+ if (installed && requestedVersion === selected.latestVersion)
149
+ return {
150
+ state: "installed",
151
+ installedVersion: installed.installedVersion,
152
+ };
153
+ const disabled = reason(selected);
154
+ return disabled
155
+ ? { state: "disabled", reason: disabled }
156
+ : { state: "available", installedVersion: installed?.installedVersion };
157
+ });
158
+ const downloads = (id: string) =>
159
+ registry.downloadStats?.periods["30d"].plugins[id]?.total ?? 0;
160
+ function reason(entry: PluginCatalogEntry): string | undefined {
161
+ if (entry.status && entry.status !== "active")
162
+ return `This plugin is ${entry.status}.`;
163
+ if (
164
+ !ownedApp.pluginDistribution
165
+ .search({ compatibleOnly: true })
166
+ .some((p) => p.id === entry.id)
167
+ )
168
+ return "This plugin is incompatible with this app.";
169
+ if (
170
+ !registry.registrySources.some(
171
+ (source) => source.source.enabled && source.installReady,
172
+ )
173
+ )
174
+ return "No enabled registry is ready to install plugins.";
175
+ const bundle =
176
+ session.entryId === entry.id
177
+ ? session.detail?.versions[entry.latestVersion]?.bundle
178
+ : undefined;
179
+ if (bundle && "pending" in bundle && bundle.pending === true)
180
+ return "The verified bundle is not ready.";
181
+ return undefined;
182
+ }
183
+ onMount(() => {
184
+ const release = registry.retain();
185
+ void registry.refresh(false);
186
+ return () => {
187
+ session.dispose();
188
+ release();
189
+ };
190
+ });
191
+ $effect(() => {
192
+ if (selected && previousDetail !== selected.id) {
193
+ previousDetail = selected.id;
194
+ void session.select(selected);
195
+ }
196
+ if (navigation.route.page !== "plugin") {
197
+ previousDetail = "";
198
+ session.dispose();
199
+ if (navigation.route.page === "landing" && scroller)
200
+ scroller.scrollTop = 0;
201
+ }
202
+ });
203
+ function persist() {
204
+ onstatechange?.();
205
+ }
206
+ function go(
207
+ page: "landing" | "directory",
208
+ category?: string,
209
+ platform?: string,
210
+ ) {
211
+ navigation.scrollTop = 0;
212
+ navigation.route = { page };
213
+ if (category)
214
+ navigation.directory = {
215
+ ...navigation.directory,
216
+ categories: [category],
217
+ };
218
+ if (platform)
219
+ navigation.directory = { ...navigation.directory, platforms: [platform] };
220
+ if (scroller) scroller.scrollTop = 0;
221
+ persist();
222
+ }
223
+ function backToPlugins() {
224
+ navigation.route = { page: navigation.returnPage };
225
+ persist();
226
+ }
227
+ function open(entry: PluginCatalogEntry) {
228
+ navigation.returnPage =
229
+ navigation.route.page === "directory" ? "directory" : "landing";
230
+ navigation.scrollTop = scroller?.scrollTop ?? 0;
231
+ navigation.route = { page: "plugin", pluginId: entry.id };
232
+ if (scroller) scroller.scrollTop = 0;
233
+ persist();
234
+ }
235
+ </script>
236
+
237
+ {#snippet downloadSummary()}
238
+ {@const counts =
239
+ selected && registry.downloadStats
240
+ ? pluginDownloadCounts(registry.downloadStats, selected.id)
241
+ : null}
242
+ {#if counts}
243
+ <dl>
244
+ <dt>Downloads (30d)</dt>
245
+ <dd>~{formatApproximateDownloadCount(counts.recent)}</dd>
246
+ <dt>Lifetime downloads</dt>
247
+ <dd>~{formatApproximateDownloadCount(counts.lifetime)}</dd>
248
+ </dl>
249
+ <p>
250
+ Tracked downloads since {new Date(
251
+ registry.downloadStats!.trackedSince + "T00:00:00Z",
252
+ ).toLocaleDateString("en-US", {
253
+ month: "short",
254
+ day: "numeric",
255
+ year: "numeric",
256
+ timeZone: "UTC",
257
+ })}. Approximate redirect requests.
258
+ </p>
259
+ {/if}
260
+ {/snippet}
261
+ {#snippet overviewContent()}
262
+ {#if session.overview.status === "loaded"}<PluginReadmeRenderer
263
+ app={ownedApp}
264
+ markdown={session.overview.markdown}
265
+ sourcePath={session.overview.url ?? ""}
266
+ />{:else if session.overview.status === "error"}<PluginRegistryContentState
267
+ kind="warning"
268
+ heading="Overview unavailable"
269
+ description={session.overview.message}
270
+ actionLabel="Retry"
271
+ onaction={() => void session.retry("overview")}
272
+ />{/if}
273
+ {/snippet}
274
+ {#snippet changelogContent()}
275
+ {#if session.changelog.status === "loaded"}<PluginReadmeRenderer
276
+ app={ownedApp}
277
+ markdown={session.changelog.markdown}
278
+ sourcePath={session.changelog.url ?? ""}
279
+ />{:else if session.changelog.status === "error"}<PluginRegistryContentState
280
+ kind="warning"
281
+ heading="Changelog unavailable"
282
+ description={session.changelog.message}
283
+ actionLabel="Retry"
284
+ onaction={() => void session.retry("changelog")}
285
+ />{/if}
286
+ {/snippet}
287
+ <div class="plugin-browser community-ui" data-ui-component="plugin-browser">
288
+ <AppShell.Root
289
+ controller={shell}
290
+ class="registry-shell"
291
+ style="--ui-shell-height:100%;"
292
+ >
293
+ <AppShell.Main>
294
+ {#if navigation.route.page === "directory" && !registry.initialLoading && registry.catalogEntries.length}
295
+ <PluginBrowserDirectory
296
+ {registry}
297
+ compact={shell.mobile.resolvedMode === "mobile"}
298
+ onhome={() => go("landing")}
299
+ filtersOpen={shell.mobile.stage === "left" &&
300
+ shell.mobile.activePanelId("left") === "registry-filters"}
301
+ ontogglefilters={(event) => {
302
+ if (shell.mobile.stage === "left") shell.mobile.showMain();
303
+ else
304
+ shell.mobile.show(
305
+ "left",
306
+ "registry-filters",
307
+ event.currentTarget as HTMLButtonElement,
308
+ );
309
+ }}
310
+ bind:contentRef={contentRegion}
311
+ directoryState={navigation.directory}
312
+ installedIds={availableIds}
313
+ {updateIds}
314
+ {downloads}
315
+ onopen={open}
316
+ onchange={(state) => {
317
+ navigation.directory = state;
318
+ persist();
319
+ }}
320
+ />
321
+ {:else}
322
+ <AppShell.Body
323
+ element="section"
324
+ layout="regions"
325
+ label="Registry pages"
326
+ >
327
+ <div class="registry-pages-main">
328
+ {#if navigation.route.page === "plugin"}
329
+ <div class="registry-detail-navigation">
330
+ <Tooltip.Provider>
331
+ <Tooltip.Root>
332
+ <Tooltip.Trigger>
333
+ {#snippet child({ props })}
334
+ <Button
335
+ {...props}
336
+ type="button"
337
+ variant="ghost"
338
+ size="icon-sm"
339
+ class="registry-detail-back"
340
+ aria-label="Back to plugins"
341
+ onclick={backToPlugins}
342
+ >
343
+ <ArrowLeft
344
+ data-icon="inline-start"
345
+ aria-hidden="true"
346
+ />
347
+ </Button>
348
+ {/snippet}
349
+ </Tooltip.Trigger>
350
+ <Tooltip.Content>Back to plugins</Tooltip.Content>
351
+ </Tooltip.Root>
352
+ </Tooltip.Provider>
353
+ <div
354
+ class="registry-detail-breadcrumbs"
355
+ data-ui-part="registry-detail-breadcrumbs"
356
+ >
357
+ <Breadcrumb.Root>
358
+ <Breadcrumb.List>
359
+ <Breadcrumb.Item>
360
+ <Breadcrumb.Link
361
+ href="#/registry"
362
+ onclick={(event) => {
363
+ event.preventDefault();
364
+ go("landing");
365
+ }}>Registry</Breadcrumb.Link
366
+ >
367
+ </Breadcrumb.Item>
368
+ <Breadcrumb.Separator />
369
+ <Breadcrumb.Item>
370
+ <Breadcrumb.Link
371
+ href="#/registry/plugins"
372
+ onclick={(event) => {
373
+ event.preventDefault();
374
+ backToPlugins();
375
+ }}>Plugins</Breadcrumb.Link
376
+ >
377
+ </Breadcrumb.Item>
378
+ <Breadcrumb.Separator />
379
+ <Breadcrumb.Item>
380
+ <Breadcrumb.Page
381
+ >{selected?.name ??
382
+ selectedId ??
383
+ "Plugin"}</Breadcrumb.Page
384
+ >
385
+ </Breadcrumb.Item>
386
+ </Breadcrumb.List>
387
+ </Breadcrumb.Root>
388
+ </div>
389
+ </div>
390
+ {/if}
391
+ <AppShell.Body.Content
392
+ bind:ref={contentRegion}
393
+ scrollbarVisibility="hover"
394
+ label="Registry content"
395
+ >
396
+ <div class="registry-browser">
397
+ {#if registry.initialLoading}<PluginRegistryContentState
398
+ kind="loading"
399
+ heading="Loading plugins"
400
+ description="Reading your verified registry…"
401
+ />
402
+ {:else if registry.lastError && !registry.catalogEntries.length}<PluginRegistryContentState
403
+ kind="warning"
404
+ heading="Plugins unavailable"
405
+ description={registry.lastError.message}
406
+ actionLabel="Retry"
407
+ onaction={() => void registry.refresh(true)}
408
+ />
409
+ {:else}
410
+ {#if registry.lastError}<p role="alert">
411
+ Refresh failed: {registry.lastError.message}
412
+ </p>{/if}
413
+ {#if !registry.catalogEntries.length}<PluginRegistryContentState
414
+ kind="empty"
415
+ heading="No plugins available"
416
+ description="Your enabled registries have no plugins."
417
+ actionLabel="Refresh"
418
+ onaction={() => void registry.refresh(true)}
419
+ />
420
+ {:else if navigation.route.page === "landing"}<PluginBrowserLanding
421
+ plugins={registry.catalogEntries}
422
+ {updateIds}
423
+ onOpenUpdates={() => {
424
+ navigation.directory = {
425
+ ...defaultDirectoryState(),
426
+ updatesOnly: true,
427
+ };
428
+ go("directory");
429
+ }}
430
+ onOpenDirectory={() => go("directory")}
431
+ onOpenPlugin={open}
432
+ onOpenCategory={(category) => go("directory", category)}
433
+ />
434
+ {:else if selected && resolved && selectedVersion !== undefined && !Object.hasOwn(resolved.detail.versions, selectedVersion)}<PluginRegistryContentState
435
+ kind="empty"
436
+ heading="Plugin version unavailable"
437
+ description={`Version ${selectedVersion} is not available from your enabled registry.`}
438
+ actionLabel="View latest release"
439
+ onaction={() => {
440
+ navigation.route = {
441
+ page: "plugin",
442
+ pluginId: selected.id,
443
+ };
444
+ persist();
445
+ }}
446
+ />
447
+ {:else if selected && resolved}<PluginBrowserDetail
448
+ {resolved}
449
+ downloadSummary={registry.downloadStats
450
+ ? downloadSummary
451
+ : undefined}
452
+ {related}
453
+ {installAction}
454
+ {selectedVersion}
455
+ currentVersion={selected.latestVersion}
456
+ overview={session.overview.status === "loaded" ||
457
+ session.overview.status === "error"
458
+ ? overviewContent
459
+ : undefined}
460
+ changelog={session.changelog.status === "loaded" ||
461
+ session.changelog.status === "error"
462
+ ? changelogContent
463
+ : undefined}
464
+ onInstall={(request) =>
465
+ update?.canUpdate &&
466
+ request.version === update.targetVersion &&
467
+ request.version === selected.latestVersion
468
+ ? registry.update(update)
469
+ : registry.install(selected, request.version)}
470
+ onOpenPlugin={open}
471
+ onOpenCategory={(category) => go("directory", category)}
472
+ onOpenPlatform={(platform) =>
473
+ go("directory", undefined, platform)}
474
+ onSelectVersion={(version) => {
475
+ navigation.route = {
476
+ page: "plugin",
477
+ pluginId: selected.id,
478
+ version,
479
+ };
480
+ persist();
481
+ }}
482
+ />
483
+ {:else if selected && session.loading}<PluginRegistryContentState
484
+ kind="loading"
485
+ heading="Loading plugin details"
486
+ description="Reading verified plugin metadata…"
487
+ />
488
+ {:else}<PluginRegistryContentState
489
+ kind="empty"
490
+ heading="Plugin unavailable"
491
+ description="This plugin is no longer in the enabled catalog."
492
+ actionLabel="Back to plugins"
493
+ onaction={() => go("directory")}
494
+ />{/if}
495
+ {/if}
496
+ </div>
497
+ </AppShell.Body.Content>
498
+ </div>
499
+ </AppShell.Body>
500
+ {/if}
501
+ </AppShell.Main>
502
+ </AppShell.Root>
503
+ </div>
504
+
505
+ <style>
506
+ .plugin-browser {
507
+ container: plugin-browser / inline-size;
508
+ min-width: 0;
509
+ height: 100%;
510
+ width: 100%;
511
+ min-height: 0;
512
+ background: var(--ui-workspace-view-background, var(--background));
513
+ color: var(--ui-workspace-view-foreground, var(--foreground));
514
+ }
515
+ :global(.registry-shell) {
516
+ height: 100%;
517
+ min-height: 0;
518
+ }
519
+ .registry-browser {
520
+ min-height: 100%;
521
+ color: var(--foreground);
522
+ background: var(--background);
523
+ }
524
+
525
+ .registry-pages-main {
526
+ display: flex;
527
+ flex: 1;
528
+ flex-direction: column;
529
+ min-width: 0;
530
+ min-height: 0;
531
+ }
532
+ .registry-detail-navigation {
533
+ display: flex;
534
+ align-items: center;
535
+ gap: 0.5rem;
536
+ padding: 0.5rem;
537
+ flex-shrink: 0;
538
+ }
539
+ .registry-detail-navigation :global(.registry-detail-back) {
540
+ border-radius: 50%;
541
+ flex-shrink: 0;
542
+ }
543
+ .registry-detail-breadcrumbs {
544
+ display: none;
545
+ width: min(100%, 70rem);
546
+ margin-inline: auto;
547
+ padding: clamp(1rem, 4cqi, 3rem);
548
+ padding-block-end: 0;
549
+ }
550
+
551
+ :global(
552
+ [data-ui-component="workspace-sidebar"]
553
+ [data-ui-part="sidebar-body"][data-content-kind="tab"]
554
+ )
555
+ .registry-detail-breadcrumbs {
556
+ display: block;
557
+ }
558
+ </style>
@@ -0,0 +1,17 @@
1
+ import { type App } from "@lapis-notes/api";
2
+ import type { PluginBrowserRoute, PluginDirectoryState } from "./plugin-browser-model";
3
+ import "./community-presentation/community-typography.css";
4
+ export interface PluginBrowserNavigation {
5
+ route: PluginBrowserRoute;
6
+ directory: PluginDirectoryState;
7
+ returnPage: "landing" | "directory";
8
+ scrollTop: number;
9
+ }
10
+ type $$ComponentProps = {
11
+ app: App;
12
+ navigation: PluginBrowserNavigation;
13
+ onstatechange?: () => void;
14
+ };
15
+ declare const PluginBrowser: import("svelte").Component<$$ComponentProps, {}, "">;
16
+ type PluginBrowser = ReturnType<typeof PluginBrowser>;
17
+ export default PluginBrowser;