@owdproject/nuxt 4.0.0-alpha.3 → 4.0.0-alpha.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.
@@ -51,21 +51,13 @@ export declare function setDesktopExtensionConfig(nuxt: Nuxt, configKey: string,
51
51
  * User values from desktop.config win over `partial` (defu: partial fills undefined only).
52
52
  */
53
53
  export declare function mergeDesktopExtensionConfig(nuxt: Nuxt, configKey: string, partial: Record<string, unknown>): void;
54
- /**
55
- * Reads the declarative extension contract (`kind`, `framework`, `adapter`, …) from a
56
- * package's Nuxt module `meta`. Build-time only — no runtime imports. Used to keep the
57
- * collection contract framework-agnostic across desktop distributions.
58
- */
59
- export declare function readExtensionMeta(modulePath: string): Promise<Partial<DesktopExtensionMeta>>;
60
- /**
61
- * Validates an extension contract and returns a normalized copy. Throws on an unknown
62
- * kind/framework so misconfigured extensions fail at build time instead of at runtime.
63
- */
54
+ /** Reads build-time extension metadata using Nuxt module resolution and getMeta(). */
55
+ export declare function readExtensionMeta(modulePath: string, nuxt?: Nuxt): Promise<Partial<DesktopExtensionMeta>>;
56
+ /** Validates the declared contract; this alone does not check the host integration. */
64
57
  export declare function assertDesktopExtensionMeta(meta: Partial<DesktopExtensionMeta>, packageName?: string): DesktopExtensionMeta;
65
58
  /**
66
- * Installs a desktop theme, module, or app package.
67
- * When the package exposes `meta.configKey`, passes `desktop[configKey]` to `installModule`
68
- * so Nuxt merges desktop.config with module defaults.
59
+ * Installs a desktop theme, module, or app after checking explicit extension metadata.
60
+ * Ordinary Nuxt modules and older name/configKey-only descriptors remain supported.
69
61
  */
70
62
  export declare function installDesktopPackage(nuxt: Nuxt, modulePath: string, desktop?: Record<string, unknown>): Promise<void>;
71
63
  type DesktopModuleMeta = {
@@ -3,7 +3,7 @@ import { join } from "node:path";
3
3
  import {
4
4
  defineNuxtModule,
5
5
  installModule,
6
- resolvePath
6
+ loadNuxtModuleInstance
7
7
  } from "@nuxt/kit";
8
8
  import { defu } from "defu";
9
9
  export function defineDesktopConfig(config) {
@@ -45,6 +45,11 @@ export function assertValidDesktopUserConfig(config, configFile = "desktop.confi
45
45
  );
46
46
  }
47
47
  const c = config;
48
+ if (c.id != null && (typeof c.id !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(c.id))) {
49
+ throw new Error(
50
+ `[@owdproject/nuxt] ${configFile}: \`id\` must be a stable identifier of 1\u201364 ASCII letters, digits, dots, underscores or hyphens, starting with a letter or digit (e.g. 'atproto-os').`
51
+ );
52
+ }
48
53
  if (c.theme != null && typeof c.theme !== "string") {
49
54
  throw new Error(
50
55
  `[@owdproject/nuxt] ${configFile}: \`theme\` must be a string (npm package name or local path of the theme module).`
@@ -139,33 +144,27 @@ export function mergeDesktopExtensionConfig(nuxt, configKey, partial) {
139
144
  const base = existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {};
140
145
  desktop[configKey] = defu(partial, base);
141
146
  }
142
- async function readConfigKey(mod) {
143
- if (!mod) return void 0;
144
- let meta = mod.meta ?? mod.default?.meta;
145
- if (!meta) {
146
- const getMeta = mod.getMeta ?? mod.default?.getMeta;
147
- if (typeof getMeta === "function") {
148
- try {
149
- meta = await getMeta();
150
- } catch (e) {
151
- console.warn("[@owdproject/nuxt] failed to call getMeta():", e);
152
- }
153
- }
147
+ const EXTENSION_META_KEYS = [
148
+ "kind",
149
+ "framework",
150
+ "packageName",
151
+ "configKey",
152
+ "entry",
153
+ "adapter",
154
+ "requires"
155
+ ];
156
+ async function loadModuleMeta(modulePath, nuxt) {
157
+ const { nuxtModule, buildTimeModuleMeta } = await loadNuxtModuleInstance(modulePath, nuxt);
158
+ const descriptor = nuxtModule;
159
+ const declared = typeof descriptor.getMeta === "function" ? await descriptor.getMeta() : descriptor.meta;
160
+ if (declared !== void 0 && (!declared || typeof declared !== "object" || Array.isArray(declared))) {
161
+ throw new Error(`[@owdproject/nuxt] ${modulePath}: module metadata must be an object.`);
154
162
  }
155
- const key = meta?.configKey;
156
- return typeof key === "string" && key.length > 0 ? key : void 0;
163
+ return { ...buildTimeModuleMeta, ...declared };
157
164
  }
158
- export async function readExtensionMeta(modulePath) {
159
- const meta = (await loadModuleDescriptor(modulePath))?.meta ?? {};
160
- return {
161
- kind: meta.kind,
162
- framework: meta.framework,
163
- packageName: meta.packageName,
164
- configKey: meta.configKey,
165
- entry: meta.entry,
166
- adapter: meta.adapter,
167
- requires: meta.requires
168
- };
165
+ export async function readExtensionMeta(modulePath, nuxt) {
166
+ const meta = await loadModuleMeta(modulePath, nuxt);
167
+ return Object.fromEntries(EXTENSION_META_KEYS.map((key) => [key, meta[key]]));
169
168
  }
170
169
  export function assertDesktopExtensionMeta(meta, packageName = "unknown") {
171
170
  if (!meta.kind || !KIND_SEGMENTS.includes(meta.kind)) {
@@ -178,28 +177,45 @@ export function assertDesktopExtensionMeta(meta, packageName = "unknown") {
178
177
  `[@owdproject/nuxt] ${packageName}: extension meta.framework must be one of ${FRAMEWORK_SEGMENTS.join(", ")}.`
179
178
  );
180
179
  }
181
- return meta;
180
+ for (const key of ["packageName", "configKey", "entry", "adapter"]) {
181
+ if (meta[key] !== void 0 && (typeof meta[key] !== "string" || !meta[key].trim())) {
182
+ throw new Error(`[@owdproject/nuxt] ${packageName}: extension meta.${key} must be a non-empty string.`);
183
+ }
184
+ }
185
+ if (meta.requires !== void 0 && (!Array.isArray(meta.requires) || !meta.requires.every(
186
+ (requirement) => typeof requirement === "string" && requirement.trim().length > 0
187
+ ))) {
188
+ throw new Error(`[@owdproject/nuxt] ${packageName}: extension meta.requires must be an array of non-empty module/kit names.`);
189
+ }
190
+ return { ...meta };
182
191
  }
183
- async function loadModuleDescriptor(modulePath) {
184
- try {
185
- const resolvedPath = await resolvePath(modulePath);
186
- const imported = await import(resolvedPath);
187
- return imported.default ?? imported;
188
- } catch (e) {
189
- console.warn(`[@owdproject/nuxt] loadModuleDescriptor failed for "${modulePath}":`, e);
190
- return void 0;
192
+ function assertDesktopExtensionHost(meta, modulePath, nuxt) {
193
+ if (meta.framework !== "nuxt") {
194
+ throw new Error(`[@owdproject/nuxt] ${modulePath}: framework "${meta.framework}" is incompatible with the Nuxt host.`);
195
+ }
196
+ if (meta.adapter !== void 0 && meta.adapter !== "@owdproject/nuxt") {
197
+ throw new Error(`[@owdproject/nuxt] ${modulePath}: adapter "${meta.adapter}" is incompatible with @owdproject/nuxt.`);
198
+ }
199
+ for (const requirement of meta.requires ?? []) {
200
+ const installed = nuxt.options._installedModules?.some(({ meta: meta2, entryPath }) => {
201
+ if (meta2.disabled) return false;
202
+ return [meta2.name, meta2.packageName, meta2.rawPath, entryPath].includes(requirement);
203
+ });
204
+ if (!installed) {
205
+ throw new Error(`[@owdproject/nuxt] ${modulePath}: required kit/module "${requirement}" is not active. Install it through the theme or host before this extension.`);
206
+ }
191
207
  }
192
208
  }
193
209
  export async function installDesktopPackage(nuxt, modulePath, desktop) {
194
- const descriptor = await loadModuleDescriptor(modulePath);
195
- const configKey = await readConfigKey(descriptor);
196
- if (configKey && desktop) {
197
- const slice = desktop[configKey];
198
- const inline = slice && typeof slice === "object" && !Array.isArray(slice) ? slice : {};
199
- await installModule(modulePath, inline);
200
- return;
210
+ const meta = await loadModuleMeta(modulePath, nuxt);
211
+ if (EXTENSION_META_KEYS.some((key) => key !== "configKey" && key in meta)) {
212
+ const extension = assertDesktopExtensionMeta(meta, modulePath);
213
+ assertDesktopExtensionHost(extension, modulePath, nuxt);
201
214
  }
202
- await installModule(modulePath);
215
+ const configKey = typeof meta.configKey === "string" && meta.configKey.length > 0 ? meta.configKey : void 0;
216
+ const slice = configKey && desktop ? desktop[configKey] : void 0;
217
+ const inline = slice && typeof slice === "object" && !Array.isArray(slice) ? slice : void 0;
218
+ await installModule(modulePath, configKey && desktop ? inline ?? {} : void 0, nuxt);
203
219
  }
204
220
  export function defineDesktopModule(definition) {
205
221
  const configKey = definition.meta.configKey;
package/dist/module.d.mts CHANGED
@@ -78,6 +78,12 @@ interface DesktopConfig {
78
78
  apps?: string[];
79
79
  modules?: string[];
80
80
  coreVersion?: string;
81
+ /**
82
+ * Stable identity of this desktop distribution (e.g. `'atproto-os'`).
83
+ * Namespaces the persisted profile (AT Protocol record rkeys and local
84
+ * IndexedDB keys) so two different OWD products never share state.
85
+ */
86
+ id?: string;
81
87
  /** Directories a desktop distribution collects extensions from (see {@link DesktopExtensionDirs}). */
82
88
  collect?: DesktopExtensionDirs;
83
89
  [key: string]: any;
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.0.0 || >=4.0.0"
6
6
  },
7
- "version": "4.0.0-alpha.3",
7
+ "version": "4.0.0-alpha.4",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -8,7 +8,7 @@ import { createDesktop, WindowManager, DesktopPointerSession, windowMachine, Ifr
8
8
  export * from '@owdproject/core';
9
9
  import { createActor } from 'xstate';
10
10
 
11
- const version = "4.0.0-alpha.3";
11
+ const version = "4.0.0-alpha.4";
12
12
  const pkg = {
13
13
  version: version};
14
14
 
@@ -71,7 +71,42 @@ onMounted(() => {
71
71
  if (windowRef.value) {
72
72
  dragHandleRef.value = windowRef.value.querySelector(".desktop-window-nav__draggable");
73
73
  }
74
+ clampIntoWorkarea();
74
75
  });
76
+ function clampIntoWorkarea() {
77
+ const area = desktopWorkspaceStore.workarea;
78
+ if (!area || area.width <= 0 || area.height <= 0) return;
79
+ const state = windowController?.state;
80
+ if (!state || state.maximized) return;
81
+ const size = state.size ?? {};
82
+ const rect = windowRef.value?.getBoundingClientRect();
83
+ const width2 = typeof size.width === "number" ? size.width : rect?.width;
84
+ const height2 = typeof size.height === "number" ? size.height : rect?.height;
85
+ if (!width2 || !height2 || !Number.isFinite(width2) || !Number.isFinite(height2)) return;
86
+ const nextWidth = Math.min(width2, area.width);
87
+ const nextHeight = Math.min(height2, area.height);
88
+ const pos = state.position ?? {};
89
+ const maxX = area.x + area.width - nextWidth;
90
+ const maxY = area.y + area.height - nextHeight;
91
+ const nextX = Math.min(Math.max(pos.x ?? area.x, area.x), maxX);
92
+ const nextY = Math.min(Math.max(pos.y ?? area.y, area.y), maxY);
93
+ if (nextWidth !== width2 || nextHeight !== height2) {
94
+ windowController.actions.setSize({ width: nextWidth, height: nextHeight });
95
+ }
96
+ if (nextX !== pos.x || nextY !== pos.y) {
97
+ windowController.actions.setPosition({ x: nextX, y: nextY });
98
+ }
99
+ }
100
+ watch(
101
+ () => [
102
+ desktopWorkspaceStore.workarea.x,
103
+ desktopWorkspaceStore.workarea.y,
104
+ desktopWorkspaceStore.workarea.width,
105
+ desktopWorkspaceStore.workarea.height
106
+ ],
107
+ () => clampIntoWorkarea(),
108
+ { immediate: true }
109
+ );
75
110
  const { x, y } = useDraggable(windowRef, {
76
111
  handle: dragHandleRef,
77
112
  initialValue: {
@@ -102,7 +102,7 @@ export declare function useDesktop(): {
102
102
  addWorkspace: (id?: string) => string;
103
103
  removeWorkspace: (workspaceId: string) => boolean;
104
104
  resolveWorkspaceFallback: (removedId: string) => string | null;
105
- }, "active" | "list" | "overview" | "workarea">, Pick<{
105
+ }, "list" | "active" | "overview" | "workarea">, Pick<{
106
106
  list: import("vue").Ref<string[], string[]>;
107
107
  active: import("vue").Ref<string, string>;
108
108
  overview: import("vue").Ref<boolean, boolean>;
@@ -4,7 +4,7 @@ import { useApplicationWindowsStore } from '../../stores/storeApplicationWindows
4
4
  export declare class ApplicationController implements IApplicationController {
5
5
  readonly id: string;
6
6
  readonly config: ApplicationConfig;
7
- readonly windows: Map<string, IWindowController>;
7
+ readonly windows: import("vue").ShallowReactive<Map<string, IWindowController>>;
8
8
  readonly openWindowCount: import("vue").ShallowRef<number, number>;
9
9
  storeWindows: ReturnType<typeof useApplicationWindowsStore> | undefined;
10
10
  storeMeta: any;
@@ -25,6 +25,13 @@ export declare class ApplicationController implements IApplicationController {
25
25
  get isInitializing(): boolean;
26
26
  get isReady(): boolean;
27
27
  initApplication(): Promise<void>;
28
+ /**
29
+ * Reopen the windows persisted in this application's window store. Returns
30
+ * `true` when at least one window was restored. No-op without a persistence
31
+ * adapter (the window store is then always empty at boot).
32
+ */
33
+ restoreApplication(): Promise<boolean>;
34
+ private restoreWindows;
28
35
  focusWindow(targetWin: IWindowController): void;
29
36
  openWindow(model: string, storedState?: WindowStoredState, meta?: Record<string, any>): IWindowController | undefined;
30
37
  closeWindow(windowId: string): void;
@@ -1,4 +1,4 @@
1
- import { markRaw, reactive, shallowRef } from "vue";
1
+ import { markRaw, reactive, shallowRef, shallowReactive } from "vue";
2
2
  import { createActor } from "xstate";
3
3
  import { nanoid } from "nanoid";
4
4
  import {
@@ -30,7 +30,7 @@ function formatAppSnapshotValue(value) {
30
30
  export class ApplicationController {
31
31
  id;
32
32
  config;
33
- windows = /* @__PURE__ */ new Map();
33
+ windows = shallowReactive(/* @__PURE__ */ new Map());
34
34
  openWindowCount = shallowRef(0);
35
35
  storeWindows;
36
36
  storeMeta;
@@ -156,6 +156,15 @@ export class ApplicationController {
156
156
  this.config.provides.entry
157
157
  );
158
158
  }
159
+ const storeWindows = this.storeWindows;
160
+ if (storeWindows?.$persistedState) {
161
+ await storeWindows.$persistedState.isReady();
162
+ }
163
+ const storeMeta = this.storeMeta;
164
+ if (storeMeta?.$persistedState) {
165
+ await storeMeta.$persistedState.isReady();
166
+ }
167
+ await this.restoreApplication();
159
168
  this.appActor.send({ type: "INITIALIZED" });
160
169
  await this.config.onReady?.(this);
161
170
  } catch (err) {
@@ -163,6 +172,33 @@ export class ApplicationController {
163
172
  console.error(`[@owdproject/nuxt] Failed to init app "${this.id}":`, err);
164
173
  }
165
174
  }
175
+ /**
176
+ * Reopen the windows persisted in this application's window store. Returns
177
+ * `true` when at least one window was restored. No-op without a persistence
178
+ * adapter (the window store is then always empty at boot).
179
+ */
180
+ async restoreApplication() {
181
+ if (typeof this.config.onRestore === "function") {
182
+ await this.config.onRestore(this);
183
+ }
184
+ const windows = this.storeWindows?.windows;
185
+ if (!windows || Object.keys(windows).length === 0) {
186
+ return false;
187
+ }
188
+ this.restoreWindows();
189
+ return true;
190
+ }
191
+ restoreWindows() {
192
+ const windows = this.storeWindows?.windows;
193
+ if (!windows) return;
194
+ for (const windowId of Object.keys(windows)) {
195
+ if (this.windows.has(windowId)) continue;
196
+ const storedState = windows[windowId];
197
+ if (storedState?.model) {
198
+ this.openWindow(storedState.model, storedState);
199
+ }
200
+ }
201
+ }
166
202
  focusWindow(targetWin) {
167
203
  const desktopStore = useDesktopStore();
168
204
  let previousFocused;
@@ -7,13 +7,23 @@ export function applicationMetaStoreId(appId) {
7
7
  export function useApplicationMetaStore(appId, customSetup) {
8
8
  let storeFactory = metaStoreRegistry.get(appId);
9
9
  if (!storeFactory) {
10
- storeFactory = defineStore(applicationMetaStoreId(appId), () => {
11
- if (customSetup) {
12
- return customSetup();
10
+ storeFactory = defineStore(
11
+ applicationMetaStoreId(appId),
12
+ () => {
13
+ if (customSetup) {
14
+ return customSetup();
15
+ }
16
+ const data = reactive({});
17
+ return { data };
18
+ },
19
+ {
20
+ // Inert unless an optional persistence module registers the adapter.
21
+ // @ts-expect-error pinia-plugin-persistedstate-2 store option
22
+ persistedState: {
23
+ persist: true
24
+ }
13
25
  }
14
- const data = reactive({});
15
- return { data };
16
- });
26
+ );
17
27
  metaStoreRegistry.set(appId, storeFactory);
18
28
  }
19
29
  return storeFactory;
@@ -7,26 +7,36 @@ export function applicationWindowsStoreId(appId) {
7
7
  export function useApplicationWindowsStore(appId) {
8
8
  let storeFactory = windowsStoreRegistry.get(appId);
9
9
  if (!storeFactory) {
10
- storeFactory = defineStore(applicationWindowsStoreId(appId), () => {
11
- const windows = reactive({});
12
- function setWindowState(windowId, stored) {
13
- windows[windowId] = stored;
14
- }
15
- function removeWindowState(windowId) {
16
- delete windows[windowId];
17
- }
18
- function clearWindows() {
19
- for (const key of Object.keys(windows)) {
20
- delete windows[key];
10
+ storeFactory = defineStore(
11
+ applicationWindowsStoreId(appId),
12
+ () => {
13
+ const windows = reactive({});
14
+ function setWindowState(windowId, stored) {
15
+ windows[windowId] = stored;
16
+ }
17
+ function removeWindowState(windowId) {
18
+ delete windows[windowId];
19
+ }
20
+ function clearWindows() {
21
+ for (const key of Object.keys(windows)) {
22
+ delete windows[key];
23
+ }
24
+ }
25
+ return {
26
+ windows,
27
+ setWindowState,
28
+ removeWindowState,
29
+ clearWindows
30
+ };
31
+ },
32
+ {
33
+ // Inert unless an optional persistence module registers the adapter.
34
+ // @ts-expect-error pinia-plugin-persistedstate-2 store option
35
+ persistedState: {
36
+ persist: true
21
37
  }
22
38
  }
23
- return {
24
- windows,
25
- setWindowState,
26
- removeWindowState,
27
- clearWindows
28
- };
29
- });
39
+ );
30
40
  windowsStoreRegistry.set(appId, storeFactory);
31
41
  }
32
42
  return storeFactory();
@@ -1,16 +1,29 @@
1
1
  import { reactive } from "vue";
2
2
  import { defineStore } from "pinia";
3
- export const useDesktopStore = defineStore("desktop", () => {
4
- const state = reactive({
5
- defaultApps: {},
6
- positionZ: 100
7
- });
8
- function incrementPositionZ() {
9
- state.positionZ += 1;
10
- return state.positionZ;
3
+ export const useDesktopStore = defineStore(
4
+ "desktop",
5
+ () => {
6
+ const state = reactive({
7
+ defaultApps: {},
8
+ positionZ: 100
9
+ });
10
+ function incrementPositionZ() {
11
+ state.positionZ += 1;
12
+ return state.positionZ;
13
+ }
14
+ return {
15
+ state,
16
+ incrementPositionZ
17
+ };
18
+ },
19
+ {
20
+ // Inert unless an optional persistence module (@owdproject/nuxt-module-pinia-persistence
21
+ // or @owdproject/nuxt-module-atproto-persistence) registers the adapter.
22
+ // @ts-expect-error pinia-plugin-persistedstate-2 store option
23
+ persistedState: {
24
+ persist: true,
25
+ // `defaultApps` is rebuilt from config on boot; persist only the z-index seed.
26
+ includePaths: ["state.positionZ"]
27
+ }
11
28
  }
12
- return {
13
- state,
14
- incrementPositionZ
15
- };
16
- });
29
+ );
@@ -8,84 +8,96 @@ export const EMPTY_DESKTOP_WORKAREA = {
8
8
  width: 0,
9
9
  height: 0
10
10
  };
11
- export const useDesktopWorkspaceStore = defineStore("desktop/workspace", () => {
12
- const list = ref([
13
- "workspace-1",
14
- "workspace-2",
15
- "workspace-3",
16
- "workspace-4"
17
- ]);
18
- const active = ref("workspace-1");
19
- const overview = ref(false);
20
- const workarea = ref({ ...EMPTY_DESKTOP_WORKAREA });
21
- const workspaceActiveIndex = computed(
22
- () => list.value.indexOf(active.value)
23
- );
24
- function setWorkspace(workspaceId) {
25
- if (!list.value.includes(workspaceId)) return false;
26
- const previous = active.value;
27
- if (previous === workspaceId) return true;
28
- const { callHook } = useDesktopHooks();
29
- const switchCtx = {
30
- from: previous,
31
- to: workspaceId,
32
- cancel: false
33
- };
34
- void callHook("workspace:before-switch", switchCtx);
35
- if (switchCtx.cancel) return false;
36
- active.value = switchCtx.to;
37
- void callHook("workspace:switched", { from: previous, to: switchCtx.to });
38
- useDesktopIPC().emit(
39
- "workspace:switched",
40
- { from: previous, to: switchCtx.to },
41
- { source: "workspace-store", severity: "info" }
11
+ export const useDesktopWorkspaceStore = defineStore(
12
+ "desktop/workspace",
13
+ () => {
14
+ const list = ref([
15
+ "workspace-1",
16
+ "workspace-2",
17
+ "workspace-3",
18
+ "workspace-4"
19
+ ]);
20
+ const active = ref("workspace-1");
21
+ const overview = ref(false);
22
+ const workarea = ref({ ...EMPTY_DESKTOP_WORKAREA });
23
+ const workspaceActiveIndex = computed(
24
+ () => list.value.indexOf(active.value)
42
25
  );
43
- return true;
44
- }
45
- function setOverview(value) {
46
- overview.value = value;
47
- }
48
- function toggleOverview() {
49
- overview.value = !overview.value;
50
- }
51
- function setWorkarea(rect) {
52
- workarea.value = { ...rect };
53
- }
54
- function addWorkspace(id) {
55
- const nextId = id || `workspace-${list.value.length + 1}`;
56
- if (!list.value.includes(nextId)) {
57
- list.value.push(nextId);
26
+ function setWorkspace(workspaceId) {
27
+ if (!list.value.includes(workspaceId)) return false;
28
+ const previous = active.value;
29
+ if (previous === workspaceId) return true;
30
+ const { callHook } = useDesktopHooks();
31
+ const switchCtx = {
32
+ from: previous,
33
+ to: workspaceId,
34
+ cancel: false
35
+ };
36
+ void callHook("workspace:before-switch", switchCtx);
37
+ if (switchCtx.cancel) return false;
38
+ active.value = switchCtx.to;
39
+ void callHook("workspace:switched", { from: previous, to: switchCtx.to });
40
+ useDesktopIPC().emit(
41
+ "workspace:switched",
42
+ { from: previous, to: switchCtx.to },
43
+ { source: "workspace-store", severity: "info" }
44
+ );
45
+ return true;
58
46
  }
59
- return nextId;
60
- }
61
- function resolveWorkspaceFallback(removedId) {
62
- const remaining = list.value.filter((id) => id !== removedId);
63
- if (remaining.length === 0) return null;
64
- return remaining[remaining.length - 1] ?? remaining[0] ?? null;
65
- }
66
- function removeWorkspace(workspaceId) {
67
- if (list.value.length <= 1) return false;
68
- const idx = list.value.indexOf(workspaceId);
69
- if (idx < 0) return false;
70
- const fallback = resolveWorkspaceFallback(workspaceId);
71
- list.value.splice(idx, 1);
72
- if (active.value === workspaceId && fallback) {
73
- active.value = fallback;
47
+ function setOverview(value) {
48
+ overview.value = value;
49
+ }
50
+ function toggleOverview() {
51
+ overview.value = !overview.value;
52
+ }
53
+ function setWorkarea(rect) {
54
+ workarea.value = { ...rect };
55
+ }
56
+ function addWorkspace(id) {
57
+ const nextId = id || `workspace-${list.value.length + 1}`;
58
+ if (!list.value.includes(nextId)) {
59
+ list.value.push(nextId);
60
+ }
61
+ return nextId;
62
+ }
63
+ function resolveWorkspaceFallback(removedId) {
64
+ const remaining = list.value.filter((id) => id !== removedId);
65
+ if (remaining.length === 0) return null;
66
+ return remaining[remaining.length - 1] ?? remaining[0] ?? null;
67
+ }
68
+ function removeWorkspace(workspaceId) {
69
+ if (list.value.length <= 1) return false;
70
+ const idx = list.value.indexOf(workspaceId);
71
+ if (idx < 0) return false;
72
+ const fallback = resolveWorkspaceFallback(workspaceId);
73
+ list.value.splice(idx, 1);
74
+ if (active.value === workspaceId && fallback) {
75
+ active.value = fallback;
76
+ }
77
+ return true;
78
+ }
79
+ return {
80
+ list,
81
+ active,
82
+ overview,
83
+ workarea,
84
+ workspaceActiveIndex,
85
+ setWorkspace,
86
+ setOverview,
87
+ toggleOverview,
88
+ setWorkarea,
89
+ addWorkspace,
90
+ removeWorkspace,
91
+ resolveWorkspaceFallback
92
+ };
93
+ },
94
+ {
95
+ // Inert unless an optional persistence module registers the adapter.
96
+ // @ts-expect-error pinia-plugin-persistedstate-2 store option
97
+ persistedState: {
98
+ persist: true,
99
+ // `overview` and `workarea` are runtime-only.
100
+ includePaths: ["list", "active"]
74
101
  }
75
- return true;
76
102
  }
77
- return {
78
- list,
79
- active,
80
- overview,
81
- workarea,
82
- workspaceActiveIndex,
83
- setWorkspace,
84
- setOverview,
85
- toggleOverview,
86
- setWorkarea,
87
- addWorkspace,
88
- removeWorkspace,
89
- resolveWorkspaceFallback
90
- };
91
- });
103
+ );
@@ -50,6 +50,13 @@ export const useDesktopNotificationStore = defineStore(
50
50
  markAsRead,
51
51
  markAllAsRead
52
52
  };
53
+ },
54
+ {
55
+ // Inert unless an optional persistence module registers the adapter.
56
+ // @ts-expect-error pinia-plugin-persistedstate-2 store option
57
+ persistedState: {
58
+ persist: true
59
+ }
53
60
  }
54
61
  );
55
62
  export const useNotificationStore = useDesktopNotificationStore;
@@ -76,6 +76,12 @@ export interface DesktopConfig {
76
76
  apps?: string[];
77
77
  modules?: string[];
78
78
  coreVersion?: string;
79
+ /**
80
+ * Stable identity of this desktop distribution (e.g. `'atproto-os'`).
81
+ * Namespaces the persisted profile (AT Protocol record rkeys and local
82
+ * IndexedDB keys) so two different OWD products never share state.
83
+ */
84
+ id?: string;
79
85
  /** Directories a desktop distribution collects extensions from (see {@link DesktopExtensionDirs}). */
80
86
  collect?: DesktopExtensionDirs;
81
87
  [key: string]: any;
@@ -115,7 +121,7 @@ export interface DesktopExtensionMeta {
115
121
  entry?: string;
116
122
  /** Framework adapter required at runtime, e.g. `@owdproject/nuxt`. */
117
123
  adapter?: string;
118
- /** Kits/modules the extension expects the active theme or desktop to provide. */
124
+ /** Exact identities of kits/modules already installed by the active Nuxt host; no version ranges. */
119
125
  requires?: string[];
120
126
  }
121
127
  export interface IWindowController extends IDesktopWindowController {
@@ -138,6 +144,8 @@ export interface IApplicationController {
138
144
  readonly isInitializing: boolean;
139
145
  readonly isReady: boolean;
140
146
  initApplication(): Promise<void>;
147
+ /** Restore saved windows without duplicating already-open controllers. */
148
+ restoreApplication(): Promise<boolean>;
141
149
  openWindow(model: string, storedState?: import('@owdproject/core').WindowStoredState, meta?: Record<string, any>): IWindowController | undefined;
142
150
  getFirstWindowByModel(model: string): IWindowController | undefined;
143
151
  closeWindow(windowId: string): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owdproject/nuxt",
3
- "version": "4.0.0-alpha.3",
3
+ "version": "4.0.0-alpha.4",
4
4
  "description": "Official Nuxt & Vue desktop engine for OpenWebDesktop",
5
5
  "homepage": "https://github.com/owdproject/nuxt#readme",
6
6
  "repository": {
@@ -112,7 +112,8 @@
112
112
  },
113
113
  "scripts": {
114
114
  "build": "nuxt-module-build build && node ./scripts/post-build-pkg-nuxt.mjs",
115
- "prepack": "nuxt-module-build build && node ./scripts/post-build-pkg-nuxt.mjs"
115
+ "prepack": "nuxt-module-build build && node ./scripts/post-build-pkg-nuxt.mjs",
116
+ "test:persistence": "vitest run test/config.test.ts test/persistence-windows.test.ts"
116
117
  },
117
118
  "dependencies": {
118
119
  "@nuxt/fonts": "^1.2.0",
@@ -143,5 +144,10 @@
143
144
  "nuxt": {
144
145
  "optional": true
145
146
  }
147
+ },
148
+ "devDependencies": {
149
+ "vitest": "^4.1.12",
150
+ "pinia-plugin-persistedstate-2": "^2.0.30",
151
+ "vue": "^3.5.44"
146
152
  }
147
153
  }