@lapis-notes/workspace 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,62 @@
1
1
  # @lapis-notes/workspace
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#95](https://github.com/lapismd/lapis-notes/pull/95)
8
+ [`8a4d0e0`](https://github.com/lapismd/lapis-notes/commit/8a4d0e02d0642a65ec7441f4fea032739550a6ae)
9
+ Thanks [@stevejuma](https://github.com/stevejuma)! - Honor mobile layout
10
+ settings in application hosts and restore App-owned mobile Search, new-note
11
+ creation and disposable legacy navigation commands through Design Core's
12
+ public mobile actions. Require released Design Core 0.13.0 for the mobile
13
+ navigation contract.
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies
18
+ [[`0ce7e30`](https://github.com/lapismd/lapis-notes/commit/0ce7e3078af8b3f9b132e2b06655a0229e72e346),
19
+ [`8a4d0e0`](https://github.com/lapismd/lapis-notes/commit/8a4d0e02d0642a65ec7441f4fea032739550a6ae)]:
20
+ - @lapis-notes/api@0.3.1
21
+ - @lapis-notes/ui@0.1.9
22
+
23
+ ## 0.3.6
24
+
25
+ ### Patch Changes
26
+
27
+ - Updated dependencies
28
+ [[`0b7ac5a`](https://github.com/lapismd/lapis-notes/commit/0b7ac5a9d6f07dd8a35d7d9fd7de8d9cb076e07b),
29
+ [`84551f9`](https://github.com/lapismd/lapis-notes/commit/84551f9f10ec73ea02fcb71d53131758a9d2d419),
30
+ [`1d9f1d1`](https://github.com/lapismd/lapis-notes/commit/1d9f1d180b38e8248f49abaf8f523fe0cddc28fd)]:
31
+ - @lapis-notes/api@0.3.0
32
+
33
+ ## 0.3.5
34
+
35
+ ### Patch Changes
36
+
37
+ - [#85](https://github.com/lapismd/lapis-notes/pull/85)
38
+ [`8325b5e`](https://github.com/lapismd/lapis-notes/commit/8325b5eb7adef358ac577b457278a19637c13571)
39
+ Thanks [@stevejuma](https://github.com/stevejuma)! - Reuse scanned identities
40
+ during unchanged startup, exclude installed plugin assets from content
41
+ polling, avoid eagerly reading lazy ESM chunks, queue plugin operations with
42
+ independent progress and cancellation, and enable bundled AI by default on the
43
+ Rust server.
44
+
45
+ Renew the native AI feature session after disable/re-enable so startup
46
+ recovery cannot reuse a fenced session.
47
+
48
+ - [#82](https://github.com/lapismd/lapis-notes/pull/82)
49
+ [`e7b96fb`](https://github.com/lapismd/lapis-notes/commit/e7b96fb7f901c6ffff7e92815b29a82bea5c5264)
50
+ Thanks
51
+ [@lapismd-dependency-bot](https://github.com/apps/lapismd-dependency-bot)! -
52
+ Refresh first-party Lapis dependencies to the published versions recorded in
53
+ this pull request.
54
+ - Updated dependencies
55
+ [[`8325b5e`](https://github.com/lapismd/lapis-notes/commit/8325b5eb7adef358ac577b457278a19637c13571),
56
+ [`e7b96fb`](https://github.com/lapismd/lapis-notes/commit/e7b96fb7f901c6ffff7e92815b29a82bea5c5264)]:
57
+ - @lapis-notes/api@0.2.7
58
+ - @lapis-notes/ui@0.1.8
59
+
3
60
  ## 0.3.4
4
61
 
5
62
  ### Patch Changes
@@ -26,6 +26,10 @@
26
26
  import { getWorkspaceHostBinding } from "@lapis-notes/api/workspace-host";
27
27
  import { AppShell } from "@lapismd/design-core/workspace/app-shell";
28
28
  import { bindFileConflictReviews } from "./file-conflicts/register-conflict-reviews";
29
+ import {
30
+ bindMobileWorkspaceCommands,
31
+ mobileWorkspaceActions,
32
+ } from "./mobile-workspace";
29
33
  import "./workspace-shell.css";
30
34
 
31
35
  let {
@@ -44,6 +48,7 @@
44
48
  .getConfiguration()
45
49
  .get("editor.alwaysFocusNewTabs", false),
46
50
  );
51
+ let mobileActions = $derived(mobileWorkspaceActions(app));
47
52
 
48
53
  onMount(() => {
49
54
  // AppShell.Root does not own startup. By the time this shell is mounted,
@@ -51,7 +56,9 @@
51
56
  void controller.start();
52
57
  const stopNative = bindNativeEditingProblems(app);
53
58
  const stopConflicts = bindFileConflictReviews(app);
59
+ const stopMobile = bindMobileWorkspaceCommands(app, controller);
54
60
  return () => {
61
+ stopMobile();
55
62
  stopConflicts();
56
63
  stopNative();
57
64
  };
@@ -71,6 +78,12 @@
71
78
  disposeOnDestroy={false}
72
79
  theme="inherit"
73
80
  >
74
- <AppShell.Surface {displayMode} {workspaceLabel} {workspaceNavigation} />
81
+ <AppShell.Surface
82
+ {displayMode}
83
+ {workspaceLabel}
84
+ {workspaceNavigation}
85
+ mobileSearchAction={mobileActions.search}
86
+ mobileCreateAction={mobileActions.create}
87
+ />
75
88
  </AppShell.Root>
76
89
  </div>
@@ -0,0 +1,8 @@
1
+ import { type App } from "@lapis-notes/api";
2
+ import type { AppShellController, WorkspaceAction } from "@lapismd/design-core/workspace/core";
3
+ /** Lapis-owned policy; Design Core owns the transient mobile presentation. */
4
+ export declare function bindMobileWorkspaceCommands(app: App, controller: AppShellController): () => void;
5
+ export declare function mobileWorkspaceActions(app: App): {
6
+ search: WorkspaceAction;
7
+ create: WorkspaceAction;
8
+ };
@@ -0,0 +1,134 @@
1
+ import { Notice } from "@lapis-notes/api";
2
+ /** Lapis-owned policy; Design Core owns the transient mobile presentation. */
3
+ export function bindMobileWorkspaceCommands(app, controller) {
4
+ const navigation = controller.renderer.mobileNavigation;
5
+ let disposed = false;
6
+ let previousCreation = Promise.resolve();
7
+ const setMode = (mode) => app.configuration.getConfiguration().update("workspace.mobile.mode", mode);
8
+ const mobileOnly = (action) => {
9
+ if (controller.renderer.displayMode === "mobile")
10
+ action();
11
+ else
12
+ new Notice("This action is only available while the mobile workspace layout is active.", undefined, app);
13
+ };
14
+ const openSidebar = (side) => {
15
+ if (controller.renderer.displayMode === "mobile")
16
+ navigation.openSidebar(side);
17
+ else {
18
+ app.workspace.exitFocusMode();
19
+ (side === "left"
20
+ ? app.workspace.leftSplit
21
+ : app.workspace.rightSplit).expand();
22
+ }
23
+ };
24
+ const createNote = () => {
25
+ const previous = previousCreation;
26
+ const operation = app.fileEditingSessions.track(async () => {
27
+ await previous;
28
+ if (disposed)
29
+ throw new Error("The workspace has closed.");
30
+ const path = app.fileManager.getAvailablePathForAttachment("Untitled.md");
31
+ const file = await app.fileManager.createFile(path, "");
32
+ navigation.openEditor();
33
+ await app.openFile(file);
34
+ app.workspace.requestSaveLayout();
35
+ });
36
+ previousCreation = operation.catch(() => undefined);
37
+ return operation;
38
+ };
39
+ const commands = [
40
+ {
41
+ id: "app:new-note",
42
+ name: "Create new note",
43
+ icon: "file-plus",
44
+ callback: createNote,
45
+ },
46
+ {
47
+ id: "workspace:toggle-mobile-layout",
48
+ name: "Workspace: Cycle mobile layout mode",
49
+ icon: "smartphone",
50
+ callback: () => {
51
+ const mode = app.configuration
52
+ .getConfiguration()
53
+ .get("workspace.mobile.mode", "auto");
54
+ return setMode(mode === "auto" ? "always" : mode === "always" ? "never" : "auto");
55
+ },
56
+ },
57
+ {
58
+ id: "workspace:use-mobile-layout",
59
+ name: "Workspace: Use mobile layout",
60
+ icon: "smartphone",
61
+ callback: () => setMode("always"),
62
+ },
63
+ {
64
+ id: "workspace:use-desktop-layout",
65
+ name: "Workspace: Use desktop layout",
66
+ icon: "monitor",
67
+ callback: () => setMode("never"),
68
+ },
69
+ {
70
+ id: "workspace:open-mobile-tabs",
71
+ name: "Workspace: Open mobile tabs",
72
+ icon: "panel-top-open",
73
+ callback: () => mobileOnly(() => navigation.openTabs()),
74
+ },
75
+ {
76
+ id: "workspace:open-mobile-search",
77
+ name: "Workspace: Open mobile search",
78
+ icon: "search",
79
+ callback: async () => {
80
+ if (controller.renderer.displayMode !== "mobile")
81
+ return mobileOnly(() => { });
82
+ navigation.openEditor();
83
+ await app.commands.executeCommand("search:open-search");
84
+ },
85
+ },
86
+ {
87
+ id: "workspace:open-mobile-left-sidebar",
88
+ name: "Workspace: Open mobile left sidebar",
89
+ icon: "panel-left-open",
90
+ callback: () => openSidebar("left"),
91
+ },
92
+ {
93
+ id: "workspace:open-mobile-right-sidebar",
94
+ name: "Workspace: Open mobile right sidebar",
95
+ icon: "panel-right-open",
96
+ callback: () => openSidebar("right"),
97
+ },
98
+ ];
99
+ const owned = commands.filter((command) => !app.commands.getCommand(command.id));
100
+ owned.forEach((command) => app.commands.registerCommand(command));
101
+ const registered = owned.map((command) => app.commands.getCommand(command.id));
102
+ return () => {
103
+ disposed = true;
104
+ for (const command of registered) {
105
+ if (app.commands.getCommand(command.id) === command)
106
+ app.commands.unregisterCommand(command.id);
107
+ }
108
+ };
109
+ }
110
+ export function mobileWorkspaceActions(app) {
111
+ const execute = async (id) => {
112
+ try {
113
+ await app.commands.executeCommand(id);
114
+ }
115
+ catch (error) {
116
+ new Notice(error instanceof Error ? error.message : String(error), undefined, app);
117
+ }
118
+ };
119
+ return {
120
+ search: {
121
+ id: "mobile-search",
122
+ label: "Open search sidebar",
123
+ icon: "search",
124
+ disabled: !app.commands.getCommand("search:open-search"),
125
+ onSelect: () => execute("workspace:open-mobile-search"),
126
+ },
127
+ create: {
128
+ id: "mobile-new-note",
129
+ label: "Create new note",
130
+ icon: "plus",
131
+ onSelect: () => execute("app:new-note"),
132
+ },
133
+ };
134
+ }
@@ -131,10 +131,7 @@
131
131
  registry.actionFeedback?.pluginId === selected.id
132
132
  ? registry.actionFeedback
133
133
  : null;
134
- if (
135
- registry.runningAction === `install:${selected.id}` ||
136
- registry.runningAction === `update:${selected.id}`
137
- )
134
+ if (registry.isPluginActionPending(selected.id))
138
135
  return { state: "installing" };
139
136
  if (feedback?.state === "error")
140
137
  return { state: "rejected", reason: feedback.message };
@@ -771,8 +771,9 @@
771
771
  size="icon"
772
772
  variant="outline"
773
773
  aria-label={`Update ${installedName(record)}`}
774
- disabled={registry.runningAction ===
775
- `update:${record.pluginId}`}
774
+ disabled={registry.isPluginActionPending(
775
+ record.pluginId,
776
+ )}
776
777
  onclick={() => void updatePlugin(update)}
777
778
  ><ArrowUpCircle /></Button.Root
778
779
  >
@@ -922,8 +923,7 @@
922
923
  ><Check data-icon="inline-start" />Bundled</Button.Root
923
924
  >
924
925
  {:else}
925
- {@const installing =
926
- registry.runningAction === `install:${entry.id}`}
926
+ {@const installing = registry.isPluginActionPending(entry.id)}
927
927
  <Button.Root
928
928
  size="sm"
929
929
  aria-label={installing
@@ -1041,7 +1041,7 @@
1041
1041
  variant="outline"
1042
1042
  aria-label={`Update ${update.name}`}
1043
1043
  disabled={!update.canUpdate ||
1044
- registry.runningAction === `update:${update.id}`}
1044
+ registry.isPluginActionPending(update.id)}
1045
1045
  onclick={() => void updatePlugin(update)}
1046
1046
  ><ArrowUpCircle /></Button.Root
1047
1047
  >
@@ -14,6 +14,8 @@ export declare class PluginRegistryController {
14
14
  initialLoading: boolean;
15
15
  refreshing: boolean;
16
16
  runningAction: string | null;
17
+ pendingPluginActions: Record<string, true>;
18
+ isPluginActionPending(pluginId: string): boolean;
17
19
  actionFeedback: PluginRegistryActionFeedback | null;
18
20
  pluginLifecycleRevision: number;
19
21
  private references;
@@ -25,6 +25,10 @@ export class PluginRegistryController {
25
25
  initialLoading = $state(true);
26
26
  refreshing = $state(false);
27
27
  runningAction = $state(null);
28
+ pendingPluginActions = $state({});
29
+ isPluginActionPending(pluginId) {
30
+ return this.pendingPluginActions[pluginId] === true;
31
+ }
28
32
  actionFeedback = $state(null);
29
33
  pluginLifecycleRevision = $state(0);
30
34
  references = 0;
@@ -110,20 +114,32 @@ export class PluginRegistryController {
110
114
  this.refreshTask = null;
111
115
  }
112
116
  async runPluginAction(actionId, pluginId, title, action) {
113
- if (this.runningAction)
117
+ if (this.isPluginActionPending(pluginId))
114
118
  return;
115
- this.runningAction = actionId;
116
- this.actionFeedback = { pluginId, state: "running", message: title };
119
+ this.pendingPluginActions = {
120
+ ...this.pendingPluginActions,
121
+ [pluginId]: true,
122
+ };
117
123
  try {
118
- await withPluginInstallProgress(this.app, { pluginId, title }, action);
119
- await this.refresh(false);
120
- this.actionFeedback = {
121
- pluginId,
122
- state: "success",
123
- message: actionId.startsWith("update:")
124
- ? "Update installed"
125
- : "Installed",
126
- };
124
+ await withPluginInstallProgress(this.app, { pluginId, title }, async (signal) => {
125
+ this.runningAction = actionId;
126
+ this.actionFeedback = { pluginId, state: "running", message: title };
127
+ try {
128
+ await action(signal);
129
+ await this.refresh(false);
130
+ this.actionFeedback = {
131
+ pluginId,
132
+ state: "success",
133
+ message: actionId.startsWith("update:")
134
+ ? "Update installed"
135
+ : "Installed",
136
+ };
137
+ }
138
+ finally {
139
+ if (this.runningAction === actionId)
140
+ this.runningAction = null;
141
+ }
142
+ });
127
143
  }
128
144
  catch (error) {
129
145
  if (isAbortError(error)) {
@@ -139,7 +155,8 @@ export class PluginRegistryController {
139
155
  new Notice(`Plugin registry action failed: ${registryErrorMessage(error)}`, undefined, this.app);
140
156
  }
141
157
  finally {
142
- this.runningAction = null;
158
+ const { [pluginId]: _finished, ...pending } = this.pendingPluginActions;
159
+ this.pendingPluginActions = pending;
143
160
  }
144
161
  }
145
162
  install(entry, version) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lapis-notes/workspace",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "Lapis Notes workspace shell backed by design-core",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -38,9 +38,9 @@
38
38
  }
39
39
  },
40
40
  "dependencies": {
41
- "@lapis-notes/api": "^0.2.6",
42
- "@lapis-notes/ui": "^0.1.7",
43
- "@lapismd/design-core": "^0.8.0 || ^0.9.0 || ^0.10.0",
41
+ "@lapis-notes/api": "^0.3.1",
42
+ "@lapis-notes/ui": "^0.1.9",
43
+ "@lapismd/design-core": "^0.13.0",
44
44
  "@lucide/svelte": "^0.523.0"
45
45
  },
46
46
  "peerDependencies": {