@lapis-notes/workspace 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE.md +4 -0
  3. package/README.md +36 -0
  4. package/dist/WorkspaceShell.svelte +59 -0
  5. package/dist/WorkspaceShell.svelte.d.ts +14 -0
  6. package/dist/index.d.ts +6 -0
  7. package/dist/index.js +5 -0
  8. package/dist/plugin-management/CommunityPluginsSettingsPage.svelte +182 -0
  9. package/dist/plugin-management/CommunityPluginsSettingsPage.svelte.d.ts +5 -0
  10. package/dist/plugin-management/CorePluginsSettingsPage.svelte +111 -0
  11. package/dist/plugin-management/CorePluginsSettingsPage.svelte.d.ts +5 -0
  12. package/dist/plugin-management/PluginReadmeRenderer.svelte +47 -0
  13. package/dist/plugin-management/PluginReadmeRenderer.svelte.d.ts +9 -0
  14. package/dist/plugin-management/PluginRegistrySettingsPage.svelte +751 -0
  15. package/dist/plugin-management/PluginRegistrySettingsPage.svelte.d.ts +5 -0
  16. package/dist/plugin-management/PluginSettingsRow.svelte +152 -0
  17. package/dist/plugin-management/PluginSettingsRow.svelte.d.ts +24 -0
  18. package/dist/plugin-management/plugin-management-context.svelte.d.ts +15 -0
  19. package/dist/plugin-management/plugin-management-context.svelte.js +24 -0
  20. package/dist/plugin-management/plugin-management.css +477 -0
  21. package/dist/plugin-management/plugin-readme.d.ts +26 -0
  22. package/dist/plugin-management/plugin-readme.js +34 -0
  23. package/dist/plugin-management/plugin-settings-navigation.d.ts +8 -0
  24. package/dist/plugin-management/plugin-settings-navigation.js +37 -0
  25. package/dist/plugin-management/register-plugin-management-settings.d.ts +3 -0
  26. package/dist/plugin-management/register-plugin-management-settings.js +95 -0
  27. package/dist/workspace-shell.css +18 -0
  28. package/package.json +69 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # @lapis-notes/workspace
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - First public npm release of the Lapis Notes workspace shell that composes the
8
+ API workspace model with Design Core layout presentation.
package/LICENSE.md ADDED
@@ -0,0 +1,4 @@
1
+ AGPL-3.0-or-later
2
+
3
+ This package is licensed under the GNU Affero General Public License v3.0 or later.
4
+ See <https://www.gnu.org/licenses/agpl-3.0.en.html>.
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # @lapis-notes/workspace
2
+
3
+ Workspace shell adapter for Lapis Notes package consumers.
4
+
5
+ This package exposes the public Svelte workspace shell that connects a host-owned
6
+ `App` to Design Core layout presentation. It stays shell-only: hosts own vault
7
+ selection, plugin boot, persistence, routing, profile management, and production
8
+ backend setup.
9
+
10
+ ## Public surface
11
+
12
+ - `WorkspaceShell`, the Svelte component used by hosts and demos.
13
+ - `WorkspaceShellProps`, the public prop contract for typed Svelte consumers.
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import { WorkspaceShell } from "@lapis-notes/workspace";
19
+ ```
20
+
21
+ Install `@lapis-notes/api`, `@lapismd/design-core`, and Svelte from npm with
22
+ compatible versions. `@lapis-notes/workspace@0.1.0` expects
23
+ `@lapis-notes/api@^0.1.0`.
24
+
25
+ ## Common Scripts
26
+
27
+ ```sh
28
+ pnpm --filter @lapis-notes/workspace build
29
+ pnpm --filter @lapis-notes/workspace check
30
+ pnpm --filter @lapis-notes/workspace test
31
+ ```
32
+
33
+ ## References
34
+
35
+ - [Package spec](spec.md)
36
+ - [Canonical spec](../../spec/src/20-packages/workspace/index.md)
@@ -0,0 +1,59 @@
1
+ <script lang="ts" module>
2
+ import type { App } from "@lapis-notes/api";
3
+ import type { WorkspaceNavigation } from "@lapismd/design-core/workspace/app-shell";
4
+ import type { WorkspaceRequestedDisplayMode } from "@lapismd/design-core/workspace/core";
5
+
6
+ export interface WorkspaceShellProps {
7
+ app: App;
8
+ displayMode?: WorkspaceRequestedDisplayMode;
9
+ workspaceLabel?: string;
10
+ workspaceNavigation?: WorkspaceNavigation;
11
+ class?: string;
12
+ }
13
+ </script>
14
+
15
+ <script lang="ts">
16
+ import { onMount, untrack } from "svelte";
17
+ import { provideApplicationState } from "@lapis-notes/api";
18
+ import { getWorkspaceHostBinding } from "@lapis-notes/api/workspace-host";
19
+ import { AppShell } from "@lapismd/design-core/workspace/app-shell";
20
+ import "./workspace-shell.css";
21
+
22
+ let {
23
+ app,
24
+ displayMode,
25
+ workspaceLabel = "Lapis Notes",
26
+ workspaceNavigation,
27
+ class: className = "",
28
+ }: WorkspaceShellProps = $props();
29
+
30
+ provideApplicationState(untrack(() => app));
31
+ const { controller } = untrack(() => getWorkspaceHostBinding(app.workspace));
32
+ let activateNewTabs = $derived(
33
+ app.configuration
34
+ .getConfiguration()
35
+ .get("editor.alwaysFocusNewTabs", false),
36
+ );
37
+
38
+ onMount(() => {
39
+ // AppShell.Root does not own startup. By the time this shell is mounted,
40
+ // the consumer has loaded the vault and restored the Lapis workspace.
41
+ void controller.start();
42
+ });
43
+ </script>
44
+
45
+ <div
46
+ class={`lapis-workspace-shell ${className}`}
47
+ data-ui-component="lapis-workspace-shell"
48
+ data-ui-part="root"
49
+ >
50
+ <AppShell.Root
51
+ {controller}
52
+ autoStart={false}
53
+ {activateNewTabs}
54
+ disposeOnDestroy={false}
55
+ theme="inherit"
56
+ >
57
+ <AppShell.Surface {displayMode} {workspaceLabel} {workspaceNavigation} />
58
+ </AppShell.Root>
59
+ </div>
@@ -0,0 +1,14 @@
1
+ import type { App } from "@lapis-notes/api";
2
+ import type { WorkspaceNavigation } from "@lapismd/design-core/workspace/app-shell";
3
+ import type { WorkspaceRequestedDisplayMode } from "@lapismd/design-core/workspace/core";
4
+ export interface WorkspaceShellProps {
5
+ app: App;
6
+ displayMode?: WorkspaceRequestedDisplayMode;
7
+ workspaceLabel?: string;
8
+ workspaceNavigation?: WorkspaceNavigation;
9
+ class?: string;
10
+ }
11
+ import "./workspace-shell.css";
12
+ declare const WorkspaceShell: import("svelte").Component<WorkspaceShellProps, {}, "">;
13
+ type WorkspaceShell = ReturnType<typeof WorkspaceShell>;
14
+ export default WorkspaceShell;
@@ -0,0 +1,6 @@
1
+ export { default as WorkspaceShell } from "./WorkspaceShell.svelte";
2
+ export type { WorkspaceShellProps } from "./WorkspaceShell.svelte";
3
+ export { registerPluginManagementSettings } from "./plugin-management/register-plugin-management-settings";
4
+ export { default as PluginRegistrySettingsPage } from "./plugin-management/PluginRegistrySettingsPage.svelte";
5
+ export { default as CorePluginsSettingsPage } from "./plugin-management/CorePluginsSettingsPage.svelte";
6
+ export { default as CommunityPluginsSettingsPage } from "./plugin-management/CommunityPluginsSettingsPage.svelte";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { default as WorkspaceShell } from "./WorkspaceShell.svelte";
2
+ export { registerPluginManagementSettings } from "./plugin-management/register-plugin-management-settings";
3
+ export { default as PluginRegistrySettingsPage } from "./plugin-management/PluginRegistrySettingsPage.svelte";
4
+ export { default as CorePluginsSettingsPage } from "./plugin-management/CorePluginsSettingsPage.svelte";
5
+ export { default as CommunityPluginsSettingsPage } from "./plugin-management/CommunityPluginsSettingsPage.svelte";
@@ -0,0 +1,182 @@
1
+ <script lang="ts">
2
+ import * as Button from "@lapismd/design-core/shadcn/button";
3
+ import type { WorkspaceSettingsPageProps } from "@lapismd/design-core/workspace/settings";
4
+ import { onMount, tick, untrack } from "svelte";
5
+ import { getPluginManagementContext } from "./plugin-management-context.svelte";
6
+ import "./plugin-management.css";
7
+ import PluginSettingsRow from "./PluginSettingsRow.svelte";
8
+ import {
9
+ openPluginSettings,
10
+ pluginDiagnosticRows,
11
+ pluginSettingsSectionId,
12
+ } from "./plugin-settings-navigation";
13
+
14
+ let { controller }: WorkspaceSettingsPageProps = $props();
15
+ const context = untrack(() => getPluginManagementContext(controller));
16
+ const app = context.app;
17
+ let revision = $state(0);
18
+ let trustBusy = $state(false);
19
+ let trustState = $derived.by(() => {
20
+ revision;
21
+ return app.workspaceTrust.getState();
22
+ });
23
+ let expandedPluginIds = $state(new Set<string>());
24
+ let plugins = $derived.by(() => {
25
+ revision;
26
+ return app.plugins.communityPlugins;
27
+ });
28
+ let indexedDiagnostics = $derived.by(() => {
29
+ revision;
30
+ const loaded = new Set(plugins.map((plugin) => plugin.manifest.id));
31
+ const core = new Set(
32
+ app.plugins.corePluginEntries.map((entry) => entry.manifest.id),
33
+ );
34
+ return app.plugins.communityPluginDiagnostics.filter(
35
+ (entry) => !loaded.has(entry.pluginId) && !core.has(entry.pluginId),
36
+ );
37
+ });
38
+
39
+ $effect(() => {
40
+ const revealRevision = context.reveal.revision;
41
+ if (
42
+ !revealRevision ||
43
+ context.reveal.sectionId !== "community-plugins" ||
44
+ !context.reveal.entryId.startsWith("community:")
45
+ ) {
46
+ return;
47
+ }
48
+ const pluginId = context.reveal.entryId.slice("community:".length);
49
+ expandedPluginIds = new Set([...expandedPluginIds, pluginId]);
50
+ void tick().then(() => {
51
+ document
52
+ .querySelector(`[data-settings-plugin-id="${CSS.escape(pluginId)}"]`)
53
+ ?.scrollIntoView({ block: "center" });
54
+ });
55
+ });
56
+
57
+ onMount(() => {
58
+ const refresh = () => (revision += 1);
59
+ const loaded = app.plugins.on("plugins-loaded", refresh);
60
+ const enabled = app.plugins.on("plugin-enabled", refresh);
61
+ const disabled = app.plugins.on("plugin-disabled", refresh);
62
+ const error = app.plugins.on("plugin-error", refresh);
63
+ void app.workspaceTrust.ready().then(refresh);
64
+ return () => {
65
+ app.plugins.offref(loaded);
66
+ app.plugins.offref(enabled);
67
+ app.plugins.offref(disabled);
68
+ app.plugins.offref(error);
69
+ };
70
+ });
71
+
72
+ async function updateTrust(trusted: boolean): Promise<void> {
73
+ if (trustBusy) return;
74
+ trustBusy = true;
75
+ try {
76
+ if (trusted) await app.workspaceTrust.grant();
77
+ else await app.workspaceTrust.revoke();
78
+ revision += 1;
79
+ } finally {
80
+ trustBusy = false;
81
+ }
82
+ }
83
+ </script>
84
+
85
+ <section
86
+ class="lapis-plugin-management"
87
+ data-ui-component="lapis-plugin-management"
88
+ data-ui-part="community-plugins"
89
+ >
90
+ <div class="lapis-plugin-management__trust">
91
+ <div>
92
+ <h1>Workspace Trust</h1>
93
+ <p>
94
+ {trustState.trusted
95
+ ? "This vault is trusted. Trusted desktop plugins and brokered capabilities may run for this workspace identity."
96
+ : "This vault is untrusted. Trusted desktop plugins, brokered capabilities, and community plugin settings surfaces stay blocked until you trust this workspace."}
97
+ </p>
98
+ </div>
99
+ <div class="lapis-plugin-management__actions">
100
+ <Button.Root
101
+ disabled={trustState.trusted || trustBusy}
102
+ onclick={() => void updateTrust(true)}
103
+ >Trust workspace</Button.Root>
104
+ <Button.Root
105
+ variant="destructive"
106
+ disabled={!trustState.trusted || trustBusy}
107
+ onclick={() => void updateTrust(false)}
108
+ >Revoke trust</Button.Root>
109
+ </div>
110
+ </div>
111
+
112
+ <header class="lapis-plugin-management__header">
113
+ <div>
114
+ <h1>Installed Plugins</h1>
115
+ <p>Manage installed community and registry plugins for this vault.</p>
116
+ </div>
117
+ <Button.Root
118
+ variant="outline"
119
+ onclick={() => globalThis.location?.reload()}
120
+ >Reload plugins</Button.Root>
121
+ </header>
122
+
123
+ <div class="lapis-plugin-management__rows">
124
+ {#if plugins.length === 0 && indexedDiagnostics.length === 0}
125
+ <p class="lapis-plugin-management__empty">
126
+ No community plugins found in .obsidian/plugins.
127
+ </p>
128
+ {/if}
129
+
130
+ {#each plugins as plugin (plugin.manifest.id)}
131
+ <PluginSettingsRow
132
+ pluginId={plugin.manifest.id}
133
+ name={plugin.manifest.name}
134
+ description={plugin.manifest.description}
135
+ version={plugin.manifest.version}
136
+ provenance={plugin.provenance}
137
+ status={plugin.state === "failed"
138
+ ? "Failed"
139
+ : plugin.enabled
140
+ ? "Enabled"
141
+ : "Disabled"}
142
+ error={plugin.lastFailureMessage ?? plugin.errorMessage ?? undefined}
143
+ enabled={plugin.enabled}
144
+ expanded={expandedPluginIds.has(plugin.manifest.id)}
145
+ hasOptions={Boolean(
146
+ pluginSettingsSectionId(controller, plugin.manifest.id),
147
+ )}
148
+ diagnostics={pluginDiagnosticRows(app, plugin.manifest.id)}
149
+ onOptions={() => openPluginSettings(controller, plugin.manifest.id)}
150
+ onRestart={async () => {
151
+ await app.plugins.restartPlugin(plugin.manifest.id);
152
+ }}
153
+ onToggle={async (enabled) => {
154
+ if (enabled) await app.plugins.enablePlugin(plugin.manifest.id);
155
+ else await app.plugins.disablePlugin(plugin.manifest.id);
156
+ }}
157
+ />
158
+ {/each}
159
+
160
+ {#each indexedDiagnostics as diagnostics (diagnostics.pluginId)}
161
+ <PluginSettingsRow
162
+ pluginId={diagnostics.pluginId}
163
+ name={diagnostics.name ?? diagnostics.pluginId}
164
+ description={diagnostics.description ?? undefined}
165
+ version={diagnostics.version ?? undefined}
166
+ provenance={diagnostics.provenance}
167
+ status={diagnostics.state}
168
+ error={diagnostics.lastFailureMessage ?? undefined}
169
+ enabled={false}
170
+ expanded={expandedPluginIds.has(diagnostics.pluginId)}
171
+ hasOptions={Boolean(
172
+ pluginSettingsSectionId(controller, diagnostics.pluginId),
173
+ )}
174
+ diagnostics={pluginDiagnosticRows(app, diagnostics.pluginId)}
175
+ onOptions={() => openPluginSettings(controller, diagnostics.pluginId)}
176
+ onRestart={async () => {
177
+ await app.plugins.restartPlugin(diagnostics.pluginId);
178
+ }}
179
+ />
180
+ {/each}
181
+ </div>
182
+ </section>
@@ -0,0 +1,5 @@
1
+ import type { WorkspaceSettingsPageProps } from "@lapismd/design-core/workspace/settings";
2
+ import "./plugin-management.css";
3
+ declare const CommunityPluginsSettingsPage: import("svelte").Component<WorkspaceSettingsPageProps, {}, "">;
4
+ type CommunityPluginsSettingsPage = ReturnType<typeof CommunityPluginsSettingsPage>;
5
+ export default CommunityPluginsSettingsPage;
@@ -0,0 +1,111 @@
1
+ <script lang="ts">
2
+ import type { WorkspaceSettingsPageProps } from "@lapismd/design-core/workspace/settings";
3
+ import { onMount, tick, untrack } from "svelte";
4
+ import { getPluginManagementContext } from "./plugin-management-context.svelte";
5
+ import "./plugin-management.css";
6
+ import PluginSettingsRow from "./PluginSettingsRow.svelte";
7
+ import {
8
+ openPluginSettings,
9
+ pluginDiagnosticRows,
10
+ pluginSettingsSectionId,
11
+ } from "./plugin-settings-navigation";
12
+
13
+ let { controller }: WorkspaceSettingsPageProps = $props();
14
+ const context = untrack(() => getPluginManagementContext(controller));
15
+ const app = context.app;
16
+ let revision = $state(0);
17
+ let expandedPluginIds = $state(new Set<string>());
18
+ let entries = $derived.by(() => {
19
+ 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;
25
+ });
26
+ });
27
+
28
+ $effect(() => {
29
+ const revealRevision = context.reveal.revision;
30
+ if (
31
+ !revealRevision ||
32
+ context.reveal.sectionId !== "core-plugins" ||
33
+ !context.reveal.entryId.startsWith("core:")
34
+ ) {
35
+ return;
36
+ }
37
+ const pluginId = context.reveal.entryId.slice("core:".length);
38
+ expandedPluginIds = new Set([...expandedPluginIds, pluginId]);
39
+ void tick().then(() => {
40
+ document
41
+ .querySelector(`[data-settings-plugin-id="${CSS.escape(pluginId)}"]`)
42
+ ?.scrollIntoView({ block: "center" });
43
+ });
44
+ });
45
+
46
+ onMount(() => {
47
+ const refresh = () => (revision += 1);
48
+ const loaded = app.plugins.on("plugins-loaded", refresh);
49
+ const enabled = app.plugins.on("plugin-enabled", refresh);
50
+ const disabled = app.plugins.on("plugin-disabled", refresh);
51
+ const error = app.plugins.on("plugin-error", refresh);
52
+ return () => {
53
+ app.plugins.offref(loaded);
54
+ app.plugins.offref(enabled);
55
+ app.plugins.offref(disabled);
56
+ app.plugins.offref(error);
57
+ };
58
+ });
59
+
60
+ </script>
61
+
62
+ <section
63
+ class="lapis-plugin-management"
64
+ data-ui-component="lapis-plugin-management"
65
+ data-ui-part="core-plugins"
66
+ >
67
+ <header class="lapis-plugin-management__header">
68
+ <div>
69
+ <h1>Installed Core Plugins</h1>
70
+ <p>Enable, inspect, and configure this app's static plugins.</p>
71
+ </div>
72
+ </header>
73
+
74
+ <div class="lapis-plugin-management__rows">
75
+ {#if entries.length === 0}
76
+ <p class="lapis-plugin-management__empty">
77
+ No core plugins are registered.
78
+ </p>
79
+ {/if}
80
+ {#each entries as entry (entry.manifest.id)}
81
+ <PluginSettingsRow
82
+ pluginId={entry.manifest.id}
83
+ name={entry.manifest.name}
84
+ description={entry.manifest.description}
85
+ version={entry.manifest.version}
86
+ provenance={entry.distribution}
87
+ status={entry.errorMessage
88
+ ? "Failed"
89
+ : entry.enabled
90
+ ? "Enabled"
91
+ : "Disabled"}
92
+ error={entry.errorMessage ?? undefined}
93
+ enabled={entry.enabled}
94
+ required={entry.required}
95
+ expanded={expandedPluginIds.has(entry.manifest.id)}
96
+ hasOptions={Boolean(
97
+ pluginSettingsSectionId(controller, entry.manifest.id),
98
+ )}
99
+ diagnostics={pluginDiagnosticRows(app, entry.manifest.id)}
100
+ onOptions={() => openPluginSettings(controller, entry.manifest.id)}
101
+ onRestart={async () => {
102
+ await app.plugins.restartPlugin(entry.manifest.id);
103
+ }}
104
+ onToggle={async (enabled) => {
105
+ if (enabled) await app.plugins.enablePlugin(entry.manifest.id);
106
+ else await app.plugins.disablePlugin(entry.manifest.id);
107
+ }}
108
+ />
109
+ {/each}
110
+ </div>
111
+ </section>
@@ -0,0 +1,5 @@
1
+ import type { WorkspaceSettingsPageProps } from "@lapismd/design-core/workspace/settings";
2
+ import "./plugin-management.css";
3
+ declare const CorePluginsSettingsPage: import("svelte").Component<WorkspaceSettingsPageProps, {}, "">;
4
+ type CorePluginsSettingsPage = ReturnType<typeof CorePluginsSettingsPage>;
5
+ export default CorePluginsSettingsPage;
@@ -0,0 +1,47 @@
1
+ <script lang="ts">
2
+ import { Component, MarkdownRenderer, type App } from "@lapis-notes/api";
3
+ import { onDestroy } from "svelte";
4
+
5
+ class PluginReadmeLifecycle extends Component {}
6
+
7
+ let {
8
+ app,
9
+ markdown,
10
+ sourcePath,
11
+ }: { app: App; markdown: string; sourcePath: string } = $props();
12
+
13
+ let host = $state<HTMLElement | null>(null);
14
+ let renderRevision = 0;
15
+ let lifecycle: Component | null = null;
16
+
17
+ $effect(() => {
18
+ const target = host;
19
+ const value = markdown;
20
+ const path = sourcePath;
21
+ if (!target) return;
22
+ const revision = ++renderRevision;
23
+ lifecycle?.unload();
24
+ lifecycle = new PluginReadmeLifecycle();
25
+ lifecycle.load();
26
+ target.replaceChildren();
27
+ void MarkdownRenderer.render(app, value, target, path, lifecycle).catch(
28
+ (error) => {
29
+ if (revision !== renderRevision) return;
30
+ target.textContent =
31
+ error instanceof Error ? error.message : String(error);
32
+ },
33
+ );
34
+ });
35
+
36
+ onDestroy(() => {
37
+ renderRevision += 1;
38
+ lifecycle?.unload();
39
+ lifecycle = null;
40
+ });
41
+ </script>
42
+
43
+ <div
44
+ bind:this={host}
45
+ class="lapis-plugin-readme"
46
+ data-ui-component="lapis-plugin-readme"
47
+ ></div>
@@ -0,0 +1,9 @@
1
+ import { type App } from "@lapis-notes/api";
2
+ type $$ComponentProps = {
3
+ app: App;
4
+ markdown: string;
5
+ sourcePath: string;
6
+ };
7
+ declare const PluginReadmeRenderer: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type PluginReadmeRenderer = ReturnType<typeof PluginReadmeRenderer>;
9
+ export default PluginReadmeRenderer;