@lapis-notes/workspace 0.2.1 → 0.3.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 (39) hide show
  1. package/CHANGELOG.md +123 -12
  2. package/README.md +6 -2
  3. package/dist/WorkspaceShell.svelte +19 -2
  4. package/dist/WorkspaceShell.svelte.d.ts +3 -1
  5. package/dist/desktop/DesktopRecoveryBanner.css +46 -0
  6. package/dist/desktop/DesktopRecoveryBanner.svelte +70 -0
  7. package/dist/desktop/DesktopRecoveryBanner.svelte.d.ts +15 -0
  8. package/dist/desktop/DesktopVaultLauncher.svelte +278 -0
  9. package/dist/desktop/DesktopVaultLauncher.svelte.d.ts +17 -0
  10. package/dist/desktop/DesktopVaultLauncherDialogs.svelte +136 -0
  11. package/dist/desktop/DesktopVaultLauncherDialogs.svelte.d.ts +9 -0
  12. package/dist/desktop/DesktopWorkspaceStartup.css +8 -0
  13. package/dist/desktop/DesktopWorkspaceStartup.svelte +34 -0
  14. package/dist/desktop/DesktopWorkspaceStartup.svelte.d.ts +9 -0
  15. package/dist/desktop/desktop-vault-launcher-controls.css +142 -0
  16. package/dist/desktop/desktop-vault-launcher.css +181 -0
  17. package/dist/desktop/desktop-vault-launcher.d.ts +30 -0
  18. package/dist/desktop/desktop-vault-launcher.js +26 -0
  19. package/dist/desktop/desktop-vault-launcher.svelte.d.ts +36 -0
  20. package/dist/desktop/desktop-vault-launcher.svelte.js +140 -0
  21. package/dist/desktop/lapis-logo.svg +471 -0
  22. package/dist/file-conflicts/ConflictReview.svelte +153 -0
  23. package/dist/file-conflicts/ConflictReview.svelte.d.ts +8 -0
  24. package/dist/file-conflicts/ConflictReviewControls.svelte +66 -0
  25. package/dist/file-conflicts/ConflictReviewControls.svelte.d.ts +12 -0
  26. package/dist/file-conflicts/conflict-review-state.svelte.d.ts +22 -0
  27. package/dist/file-conflicts/conflict-review-state.svelte.js +59 -0
  28. package/dist/file-conflicts/conflict-review-view.d.ts +19 -0
  29. package/dist/file-conflicts/conflict-review-view.js +50 -0
  30. package/dist/file-conflicts/register-conflict-reviews.d.ts +7 -0
  31. package/dist/file-conflicts/register-conflict-reviews.js +132 -0
  32. package/dist/index.d.ts +6 -0
  33. package/dist/index.js +4 -0
  34. package/dist/plugin-management/CorePluginsSettingsPage.svelte +47 -16
  35. package/dist/plugin-management/PluginBrowser.svelte +16 -1
  36. package/dist/plugin-management/PluginRegistryToolbar.svelte.d.ts +1 -1
  37. package/dist/plugin-management/plugin-registry-controller.svelte.js +5 -1
  38. package/dist/plugin-management/register-plugin-management-settings.js +7 -6
  39. package/package.json +5 -4
@@ -0,0 +1,153 @@
1
+ <script lang="ts">
2
+ import { MergeEditor } from "@lapismd/design-core/diff";
3
+ import { Button } from "@lapismd/design-core/shadcn/button";
4
+ import FileCheckIcon from "@lucide/svelte/icons/file-check";
5
+ import Undo2Icon from "@lucide/svelte/icons/undo-2";
6
+ import ConflictReviewControls from "./ConflictReviewControls.svelte";
7
+ import RefreshCwIcon from "@lucide/svelte/icons/refresh-cw";
8
+ import type {
9
+ ConflictReviewState,
10
+ ConflictSelection,
11
+ } from "./conflict-review-state.svelte";
12
+
13
+ let {
14
+ model,
15
+ selection,
16
+ }: { model: ConflictReviewState; selection: ConflictSelection } = $props();
17
+ const conflict = $derived(
18
+ model.entries.find((entry) => entry.path === selection.path),
19
+ );
20
+ let options = $state({
21
+ wrap: false,
22
+ ignoreWhitespace: false,
23
+ ignoreCase: false,
24
+ syncHorizontalScroll: true,
25
+ });
26
+ const disabled = $derived(model.disabled() || model.busy !== null);
27
+ </script>
28
+
29
+ {#if conflict}
30
+ <section
31
+ class="conflict-review"
32
+ aria-label={`File conflict: ${conflict.path}`}
33
+ aria-busy={model.busy === conflict.path}
34
+ >
35
+ <div class="comparison">
36
+ {#key conflict}
37
+ {@const seed = selection.seed(conflict)}
38
+ <MergeEditor
39
+ path={conflict.path}
40
+ left={conflict.local}
41
+ base={seed}
42
+ right={conflict.external ?? ""}
43
+ leftLabel="Your version"
44
+ baseLabel="Merged version"
45
+ rightLabel="Disk version"
46
+ readOnly={disabled || conflict.external === undefined}
47
+ editable
48
+ editableSides={["base"]}
49
+ wrap={options.wrap}
50
+ ignoreWhitespace={options.ignoreWhitespace}
51
+ ignoreCase={options.ignoreCase}
52
+ syncHorizontalScroll={options.syncHorizontalScroll}
53
+ onResolvedChange={({ content }) => {
54
+ selection.result = content;
55
+ }}
56
+ >
57
+ {#snippet headerCenter()}
58
+ <ConflictReviewControls bind:options {disabled} />
59
+ {/snippet}
60
+ </MergeEditor>
61
+ {/key}
62
+ </div>
63
+ <footer>
64
+ <div class="explanation">
65
+ {#if model.error}<p role="alert">{model.error}</p>{/if}
66
+ {#if conflict.external === undefined}
67
+ <p role="status">
68
+ The disk version could not be read: {conflict.readError}
69
+ </p>
70
+ {:else if conflict.external === null}
71
+ <p>The file was deleted from disk.</p>
72
+ {:else}
73
+ <p>
74
+ Edit the centre or accept changes from either side, then save the
75
+ merged version.
76
+ </p>
77
+ {/if}
78
+ </div>
79
+ <div class="actions">
80
+ <Button
81
+ variant="ghost"
82
+ disabled={disabled || conflict.external === undefined}
83
+ onclick={() => model.recover(conflict.path, "use-external")}
84
+ >
85
+ <Undo2Icon aria-hidden="true" />Use disk version
86
+ </Button>
87
+ <Button
88
+ variant="outline"
89
+ disabled={disabled ||
90
+ conflict.external === undefined ||
91
+ selection.result === undefined}
92
+ onclick={() =>
93
+ model.recover(conflict.path, undefined, selection.result)}
94
+ >
95
+ <FileCheckIcon aria-hidden="true" />Save merged version
96
+ </Button>
97
+ <Button
98
+ variant="ghost"
99
+ {disabled}
100
+ onclick={() => model.recover(conflict.path)}
101
+ >
102
+ <RefreshCwIcon aria-hidden="true" />Check disk again
103
+ </Button>
104
+ </div>
105
+ </footer>
106
+ </section>
107
+ {/if}
108
+
109
+ <style>
110
+ .conflict-review {
111
+ display: flex;
112
+ flex-direction: column;
113
+ height: 100%;
114
+ min-height: 0;
115
+ color: var(--ui-workspace-view-foreground, var(--foreground));
116
+ background: var(--ui-workspace-view-background, var(--background));
117
+ }
118
+ .comparison {
119
+ --ui-diff-frame-border: 0;
120
+ --ui-diff-frame-radius: 0;
121
+ flex: 1;
122
+ min-height: 0;
123
+ overflow: hidden;
124
+ }
125
+ footer {
126
+ display: flex;
127
+ align-items: center;
128
+ flex-wrap: wrap;
129
+ gap: 0.75rem;
130
+ padding: 0.75rem 1rem;
131
+ border-top: 1px solid var(--ui-workspace-border);
132
+ background: var(--ui-workspace-background, var(--background));
133
+ }
134
+ .explanation {
135
+ flex: 1;
136
+ min-width: 12rem;
137
+ font-size: 0.8125rem;
138
+ }
139
+ p {
140
+ margin: 0;
141
+ }
142
+ .actions {
143
+ display: flex;
144
+ flex-wrap: wrap;
145
+ gap: 0.5rem;
146
+ }
147
+ [role="alert"] {
148
+ color: var(--destructive);
149
+ }
150
+ :global(.dark) [role="alert"] {
151
+ color: color-mix(in srgb, var(--destructive) 60%, var(--foreground));
152
+ }
153
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { ConflictReviewState, ConflictSelection } from "./conflict-review-state.svelte";
2
+ type $$ComponentProps = {
3
+ model: ConflictReviewState;
4
+ selection: ConflictSelection;
5
+ };
6
+ declare const ConflictReview: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type ConflictReview = ReturnType<typeof ConflictReview>;
8
+ export default ConflictReview;
@@ -0,0 +1,66 @@
1
+ <script lang="ts">
2
+ import { Toggle } from "@lapismd/design-core/shadcn/toggle";
3
+ import * as Tooltip from "@lapismd/design-core/shadcn/tooltip";
4
+ import WrapText from "@lucide/svelte/icons/wrap-text";
5
+ import Space from "@lucide/svelte/icons/space";
6
+ import CaseSensitive from "@lucide/svelte/icons/case-sensitive";
7
+ import ArrowRightLeft from "@lucide/svelte/icons/arrow-right-left";
8
+
9
+ let {
10
+ options = $bindable(),
11
+ disabled = false,
12
+ }: {
13
+ options: {
14
+ wrap: boolean;
15
+ ignoreWhitespace: boolean;
16
+ ignoreCase: boolean;
17
+ syncHorizontalScroll: boolean;
18
+ };
19
+ disabled?: boolean;
20
+ } = $props();
21
+ const actions = [
22
+ { key: "wrap", label: "Wrap text", icon: WrapText },
23
+ { key: "ignoreWhitespace", label: "Ignore whitespace", icon: Space },
24
+ { key: "ignoreCase", label: "Ignore case", icon: CaseSensitive },
25
+ {
26
+ key: "syncHorizontalScroll",
27
+ label: "Synchronize horizontal scrolling",
28
+ icon: ArrowRightLeft,
29
+ },
30
+ ] as const;
31
+ </script>
32
+
33
+ <div class="merge-controls" role="group" aria-label="Merge editor settings">
34
+ <Tooltip.Provider>
35
+ {#each actions as action (action.key)}
36
+ <Tooltip.Root>
37
+ <Tooltip.Trigger>
38
+ {#snippet child({ props })}
39
+ <Toggle
40
+ {...props}
41
+ size="sm"
42
+ {disabled}
43
+ aria-label={action.label}
44
+ pressed={options[action.key]}
45
+ onPressedChange={(pressed) =>
46
+ (options = { ...options, [action.key]: pressed })}
47
+ >
48
+ <action.icon aria-hidden="true" />
49
+ </Toggle>
50
+ {/snippet}
51
+ </Tooltip.Trigger>
52
+ <Tooltip.Content side="bottom">{action.label}</Tooltip.Content>
53
+ </Tooltip.Root>
54
+ {/each}
55
+ </Tooltip.Provider>
56
+ </div>
57
+
58
+ <style>
59
+ .merge-controls {
60
+ --spacing: 0.1875rem;
61
+ --muted: color-mix(in srgb, var(--primary) 22%, var(--background));
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 0.125rem;
65
+ }
66
+ </style>
@@ -0,0 +1,12 @@
1
+ type $$ComponentProps = {
2
+ options: {
3
+ wrap: boolean;
4
+ ignoreWhitespace: boolean;
5
+ ignoreCase: boolean;
6
+ syncHorizontalScroll: boolean;
7
+ };
8
+ disabled?: boolean;
9
+ };
10
+ declare const ConflictReviewControls: import("svelte").Component<$$ComponentProps, {}, "options">;
11
+ type ConflictReviewControls = ReturnType<typeof ConflictReviewControls>;
12
+ export default ConflictReviewControls;
@@ -0,0 +1,22 @@
1
+ import { SvelteMap } from "svelte/reactivity";
2
+ import type { FileTextConflict, FileTextConflictRegistry, FileTextResolution } from "@lapis-notes/api";
3
+ export declare class ConflictSelection {
4
+ #private;
5
+ private readonly results;
6
+ path: string;
7
+ constructor(results: SvelteMap<string, string>);
8
+ get result(): string | undefined;
9
+ set result(value: string | undefined);
10
+ seed(conflict: FileTextConflict): string;
11
+ }
12
+ /** Presentation state only; every recovery still uses the writer-owned decision. */
13
+ export declare class ConflictReviewState {
14
+ readonly registry: FileTextConflictRegistry;
15
+ readonly disabled: () => boolean;
16
+ readonly results: SvelteMap<string, string>;
17
+ entries: readonly FileTextConflict[];
18
+ busy: string | null;
19
+ error: string;
20
+ constructor(registry: FileTextConflictRegistry, disabled: () => boolean);
21
+ recover(path: string, choice?: FileTextResolution, merged?: string): Promise<void>;
22
+ }
@@ -0,0 +1,59 @@
1
+ import { SvelteMap } from "svelte/reactivity";
2
+ export class ConflictSelection {
3
+ results;
4
+ path = $state("");
5
+ constructor(results) {
6
+ this.results = results;
7
+ }
8
+ get result() {
9
+ return this.results.get(this.path);
10
+ }
11
+ set result(value) {
12
+ if (value === undefined)
13
+ this.results.delete(this.path);
14
+ else
15
+ this.results.set(this.path, value);
16
+ }
17
+ #seeds = new WeakMap();
18
+ seed(conflict) {
19
+ if (!this.#seeds.has(conflict))
20
+ this.#seeds.set(conflict, this.result ?? conflict.baseline ?? "");
21
+ return this.#seeds.get(conflict);
22
+ }
23
+ }
24
+ /** Presentation state only; every recovery still uses the writer-owned decision. */
25
+ export class ConflictReviewState {
26
+ registry;
27
+ disabled;
28
+ results = new SvelteMap();
29
+ entries = $state.raw([]);
30
+ busy = $state(null);
31
+ error = $state("");
32
+ constructor(registry, disabled) {
33
+ this.registry = registry;
34
+ this.disabled = disabled;
35
+ }
36
+ async recover(path, choice, merged) {
37
+ if (this.disabled() || this.busy !== null)
38
+ return;
39
+ const conflict = this.entries.find((entry) => entry.path === path);
40
+ if (!conflict)
41
+ return;
42
+ this.busy = path;
43
+ this.error = "";
44
+ try {
45
+ if (merged !== undefined)
46
+ await conflict.resolveMerged(merged);
47
+ else if (choice === undefined)
48
+ await conflict.refresh();
49
+ else
50
+ await conflict.resolve(choice);
51
+ }
52
+ catch (error) {
53
+ this.error = error instanceof Error ? error.message : String(error);
54
+ }
55
+ finally {
56
+ this.busy = null;
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,19 @@
1
+ import { View, type WorkspaceLeaf } from "@lapis-notes/api";
2
+ import { ConflictSelection, type ConflictReviewState } from "./conflict-review-state.svelte";
3
+ export declare const CONFLICT_REVIEW_VIEW = "desktop:file-conflict";
4
+ export declare class ConflictReviewView extends View {
5
+ #private;
6
+ private readonly model;
7
+ readonly selection: ConflictSelection;
8
+ constructor(leaf: WorkspaceLeaf, model: ConflictReviewState);
9
+ getViewType(): string;
10
+ getDisplayText(): string;
11
+ getIcon(): string;
12
+ getClosePersistenceScope(): "layout";
13
+ getState(): Record<string, unknown>;
14
+ setState(state: Record<string, unknown>): Promise<void>;
15
+ protected onOpen(): Promise<void>;
16
+ protected onClose(): Promise<void>;
17
+ onload(): void;
18
+ onunload(): void;
19
+ }
@@ -0,0 +1,50 @@
1
+ import { View } from "@lapis-notes/api";
2
+ import { mount, unmount } from "svelte";
3
+ import ConflictReview from "./ConflictReview.svelte";
4
+ import { ConflictSelection, } from "./conflict-review-state.svelte";
5
+ export const CONFLICT_REVIEW_VIEW = "desktop:file-conflict";
6
+ export class ConflictReviewView extends View {
7
+ model;
8
+ selection;
9
+ #component;
10
+ constructor(leaf, model) {
11
+ super(leaf);
12
+ this.model = model;
13
+ this.selection = new ConflictSelection(model.results);
14
+ }
15
+ getViewType() {
16
+ return CONFLICT_REVIEW_VIEW;
17
+ }
18
+ getDisplayText() {
19
+ return this.selection.path
20
+ ? `Resolve ${this.selection.path}`
21
+ : "File conflict";
22
+ }
23
+ getIcon() {
24
+ return "git-compare-arrows";
25
+ }
26
+ getClosePersistenceScope() {
27
+ return "layout";
28
+ }
29
+ getState() {
30
+ return { path: this.selection.path };
31
+ }
32
+ async setState(state) {
33
+ const path = typeof state.path === "string" ? state.path : "";
34
+ this.selection.path = path;
35
+ await super.setState(state);
36
+ }
37
+ async onOpen() { }
38
+ async onClose() { }
39
+ onload() {
40
+ this.#component = mount(ConflictReview, {
41
+ target: this.containerEl,
42
+ props: { model: this.model, selection: this.selection },
43
+ });
44
+ }
45
+ onunload() {
46
+ if (this.#component)
47
+ void unmount(this.#component);
48
+ this.#component = undefined;
49
+ }
50
+ }
@@ -0,0 +1,7 @@
1
+ import type { App } from "@lapis-notes/api";
2
+ export interface FileConflictReviewOptions {
3
+ /** Host-owned leave/startup inhibition, read on every action and render. */
4
+ disabled?: () => boolean;
5
+ }
6
+ /** One presentation owner per App, shared by early host and mounted shell leases. */
7
+ export declare function bindFileConflictReviews(app: App, options?: FileConflictReviewOptions): () => void;
@@ -0,0 +1,132 @@
1
+ import { SvelteSet } from "svelte/reactivity";
2
+ import { getWorkspaceHostBinding } from "@lapis-notes/api/workspace-host";
3
+ import { CONFLICT_REVIEW_VIEW, ConflictReviewView, } from "./conflict-review-view";
4
+ import { ConflictReviewState } from "./conflict-review-state.svelte";
5
+ const prefix = "lapis-file-conflict:";
6
+ const bindings = new WeakMap();
7
+ /** One presentation owner per App, shared by early host and mounted shell leases. */
8
+ export function bindFileConflictReviews(app, options = {}) {
9
+ let binding = bindings.get(app);
10
+ if (!binding) {
11
+ const disabled = new SvelteSet();
12
+ binding = {
13
+ references: 0,
14
+ disabled,
15
+ dispose: registerConflictReviews(app, () => [...disabled].some((read) => read())),
16
+ };
17
+ bindings.set(app, binding);
18
+ }
19
+ const owned = binding;
20
+ const disabled = () => options.disabled?.() ?? false;
21
+ owned.disabled.add(disabled);
22
+ owned.references++;
23
+ let held = true;
24
+ return () => {
25
+ if (!held)
26
+ return;
27
+ held = false;
28
+ owned.disabled.delete(disabled);
29
+ if (--owned.references === 0) {
30
+ owned.dispose();
31
+ bindings.delete(app);
32
+ }
33
+ };
34
+ }
35
+ function registerConflictReviews(app, disabled) {
36
+ const registry = app.fileEditingSessions.textConflicts;
37
+ const model = new ConflictReviewState(registry, disabled);
38
+ const controller = getWorkspaceHostBinding(app.workspace).controller;
39
+ const collection = app.workspace.diagnostics.createCollection("desktop:file-conflicts", { label: "File conflicts" });
40
+ app.workspace.registerView(CONFLICT_REVIEW_VIEW, (leaf) => new ConflictReviewView(leaf, model));
41
+ const stopNavigation = app.workspace.registerDiagnosticOpener(async (location) => {
42
+ if (!location.resource.uri.startsWith(prefix))
43
+ return false;
44
+ if (!active)
45
+ return true;
46
+ const path = decodeURIComponent(location.resource.uri.slice(prefix.length));
47
+ if (!model.entries.some((entry) => entry.path === path))
48
+ return true;
49
+ let leaf = app.workspace
50
+ .getLeavesOfType(CONFLICT_REVIEW_VIEW)
51
+ .find((leaf) => leaf.view.getState().path === path);
52
+ if (!leaf) {
53
+ leaf = app.workspace.getLeaf("tab");
54
+ await leaf.setViewState({
55
+ type: CONFLICT_REVIEW_VIEW,
56
+ state: { path },
57
+ });
58
+ }
59
+ if (!active)
60
+ return true;
61
+ app.workspace.setActiveLeaf(leaf);
62
+ if (!active)
63
+ return true;
64
+ await app.workspace.revealLeaf(leaf);
65
+ if (!active)
66
+ return true;
67
+ app.workspace.requestSaveLayout({
68
+ source: "api",
69
+ operation: "open-file-conflict",
70
+ });
71
+ return true;
72
+ });
73
+ let active = true;
74
+ let paths = new Set();
75
+ const stopRegistry = registry.subscribe((entries) => {
76
+ model.entries = entries;
77
+ if (!entries.length)
78
+ model.error = "";
79
+ const current = new Set(entries.map((entry) => entry.path));
80
+ for (const path of paths)
81
+ if (!current.has(path)) {
82
+ collection.delete({ uri: prefix + encodeURIComponent(path) });
83
+ model.results.delete(path);
84
+ }
85
+ for (const entry of entries) {
86
+ collection.set({
87
+ uri: prefix + encodeURIComponent(entry.path),
88
+ label: entry.path,
89
+ icon: "file-warning",
90
+ }, [
91
+ {
92
+ message: entry.external === undefined
93
+ ? "The disk version could not be read. Review the retained draft."
94
+ : entry.external === null
95
+ ? "The file was deleted on disk. Review the retained draft."
96
+ : "File changed on disk. Review your draft before saving.",
97
+ severity: "error",
98
+ source: "File conflicts",
99
+ code: "write-conflict",
100
+ },
101
+ ]);
102
+ }
103
+ const added = [...current].some((path) => !paths.has(path));
104
+ paths = current;
105
+ for (const leaf of app.workspace.getLeavesOfType(CONFLICT_REVIEW_VIEW)) {
106
+ if (!current.has(String(leaf.view.getState().path)))
107
+ controller.workspace.closeLeaf(leaf.id);
108
+ }
109
+ if (added)
110
+ void controller
111
+ .start()
112
+ .then(async () => {
113
+ if (active)
114
+ await controller.commands.execute("app-shell:show-problems");
115
+ })
116
+ .catch((error) => {
117
+ if (active)
118
+ app.notifications.notify({
119
+ severity: "error",
120
+ source: "files",
121
+ message: String(error),
122
+ });
123
+ });
124
+ });
125
+ return () => {
126
+ active = false;
127
+ stopRegistry();
128
+ stopNavigation();
129
+ collection.dispose();
130
+ app.workspace.unregisterView(CONFLICT_REVIEW_VIEW);
131
+ };
132
+ }
package/dist/index.d.ts CHANGED
@@ -7,3 +7,9 @@ export { default as CommunityPluginsSettingsPage } from "./plugin-management/Com
7
7
  export { PluginsView } from "./plugin-management/plugins-view.svelte";
8
8
  export { default as PluginBrowser } from "./plugin-management/PluginBrowser.svelte";
9
9
  export { openPluginBrowser } from "./plugin-management/plugin-browser-navigation";
10
+ export { default as DesktopVaultLauncher } from "./desktop/DesktopVaultLauncher.svelte";
11
+ export { default as DesktopWorkspaceStartup } from "./desktop/DesktopWorkspaceStartup.svelte";
12
+ export type { DesktopVaultLauncherHost, DesktopLauncherAppearance, LauncherStatus, } from "./desktop/desktop-vault-launcher";
13
+ export { default as DesktopRecoveryBanner } from "./desktop/DesktopRecoveryBanner.svelte";
14
+ export { bindFileConflictReviews } from "./file-conflicts/register-conflict-reviews";
15
+ export type { FileConflictReviewOptions } from "./file-conflicts/register-conflict-reviews";
package/dist/index.js CHANGED
@@ -6,3 +6,7 @@ export { default as CommunityPluginsSettingsPage } from "./plugin-management/Com
6
6
  export { PluginsView } from "./plugin-management/plugins-view.svelte";
7
7
  export { default as PluginBrowser } from "./plugin-management/PluginBrowser.svelte";
8
8
  export { openPluginBrowser } from "./plugin-management/plugin-browser-navigation";
9
+ export { default as DesktopVaultLauncher } from "./desktop/DesktopVaultLauncher.svelte";
10
+ export { default as DesktopWorkspaceStartup } from "./desktop/DesktopWorkspaceStartup.svelte";
11
+ export { default as DesktopRecoveryBanner } from "./desktop/DesktopRecoveryBanner.svelte";
12
+ export { bindFileConflictReviews } from "./file-conflicts/register-conflict-reviews";
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { WorkspaceSettingsPageProps } from "@lapismd/design-core/workspace/settings";
3
3
  import { onMount, tick, untrack } from "svelte";
4
+ import { getWorkspaceHostBinding } from "@lapis-notes/api/workspace-host";
4
5
  import { getPluginManagementContext } from "./plugin-management-context.svelte";
5
6
  import "./plugin-management.css";
6
7
  import PluginSettingsRow from "./PluginSettingsRow.svelte";
@@ -13,15 +14,34 @@
13
14
  let { controller }: WorkspaceSettingsPageProps = $props();
14
15
  const context = untrack(() => getPluginManagementContext(controller));
15
16
  const app = context.app;
17
+ const managed = untrack(
18
+ () => getWorkspaceHostBinding(app.workspace).controller.managedPlugins,
19
+ );
16
20
  let revision = $state(0);
17
21
  let expandedPluginIds = $state(new Set<string>());
18
22
  let entries = $derived.by(() => {
19
23
  revision;
20
- const seen = new Set<string>();
21
- return app.plugins.corePluginEntries.filter((entry) => {
22
- if (seen.has(entry.manifest.id)) return false;
23
- seen.add(entry.manifest.id);
24
- return true;
24
+ const staticEntries = app.plugins.corePluginEntries;
25
+ return managed.states.map((entry) => {
26
+ const plugin =
27
+ entry.sourceId === "lapis"
28
+ ? staticEntries.find((plugin) => plugin.manifest.id === entry.id)
29
+ : undefined;
30
+ return {
31
+ ...entry,
32
+ manifest: {
33
+ id: entry.id,
34
+ name: entry.name,
35
+ description: entry.description,
36
+ version: plugin?.manifest.version,
37
+ },
38
+ errorMessage:
39
+ entry.error instanceof Error
40
+ ? entry.error.message
41
+ : entry.error == null
42
+ ? null
43
+ : String(entry.error),
44
+ };
25
45
  });
26
46
  });
27
47
 
@@ -34,8 +54,13 @@
34
54
  ) {
35
55
  return;
36
56
  }
37
- const pluginId = context.reveal.entryId.slice("core:".length);
38
- expandedPluginIds = new Set([...expandedPluginIds, pluginId]);
57
+ const target = context.reveal.entryId.slice("core:".length);
58
+ const entry = entries.find(
59
+ (entry) => entry.key === target || entry.id === target,
60
+ );
61
+ if (!entry) return;
62
+ const pluginId = entry.id;
63
+ expandedPluginIds = new Set([...expandedPluginIds, entry.key]);
39
64
  void tick().then(() => {
40
65
  document
41
66
  .querySelector(`[data-settings-plugin-id="${CSS.escape(pluginId)}"]`)
@@ -56,7 +81,6 @@
56
81
  app.plugins.offref(error);
57
82
  };
58
83
  });
59
-
60
84
  </script>
61
85
 
62
86
  <section
@@ -77,7 +101,7 @@
77
101
  No core plugins are registered.
78
102
  </p>
79
103
  {/if}
80
- {#each entries as entry (entry.manifest.id)}
104
+ {#each entries as entry (entry.key)}
81
105
  <PluginSettingsRow
82
106
  pluginId={entry.manifest.id}
83
107
  name={entry.manifest.name}
@@ -92,18 +116,25 @@
92
116
  error={entry.errorMessage ?? undefined}
93
117
  enabled={entry.enabled}
94
118
  required={entry.required}
95
- expanded={expandedPluginIds.has(entry.manifest.id)}
119
+ expanded={expandedPluginIds.has(entry.key)}
96
120
  hasOptions={Boolean(
97
121
  pluginSettingsSectionId(controller, entry.manifest.id),
98
122
  )}
99
- diagnostics={pluginDiagnosticRows(app, entry.manifest.id)}
123
+ diagnostics={entry.sourceId === "lapis"
124
+ ? pluginDiagnosticRows(app, entry.manifest.id)
125
+ : [
126
+ { label: "State", value: entry.status },
127
+ { label: "Source", value: entry.sourceId },
128
+ ]}
100
129
  onOptions={() => openPluginSettings(controller, entry.manifest.id)}
101
- onRestart={async () => {
102
- await app.plugins.restartPlugin(entry.manifest.id);
103
- }}
130
+ onRestart={entry.sourceId === "lapis"
131
+ ? async () => {
132
+ await app.plugins.restartPlugin(entry.manifest.id);
133
+ }
134
+ : undefined}
104
135
  onToggle={async (enabled) => {
105
- if (enabled) await app.plugins.enablePlugin(entry.manifest.id);
106
- else await app.plugins.disablePlugin(entry.manifest.id);
136
+ if (enabled) await managed.enable(entry.key);
137
+ else await managed.disable(entry.key);
107
138
  }}
108
139
  />
109
140
  {/each}