@owdproject/core 3.3.0 → 3.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.
Files changed (92) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/DESKTOP_KERNEL.md +232 -0
  3. package/MIGRATION_3.3.2.md +23 -0
  4. package/MIGRATION_3.4.md +126 -0
  5. package/PLAYGROUND.md +91 -0
  6. package/README.md +13 -80
  7. package/dist/module.d.mts +2 -2
  8. package/dist/module.json +3 -3
  9. package/dist/module.mjs +86 -99
  10. package/index.ts +6 -3
  11. package/kit/authoring.ts +340 -0
  12. package/kit/defineDesktopApp.ts +56 -0
  13. package/module.ts +76 -108
  14. package/package.json +70 -40
  15. package/runtime/components/{Desktop/Application/DesktopApplicationRender.vue → Application/DesktopCoreApplicationRender.vue} +2 -2
  16. package/runtime/components/{Desktop/Application/DesktopApplicationWindowsRender.vue → Application/DesktopCoreApplicationWindowsRender.vue} +1 -1
  17. package/runtime/components/core/DesktopCore.vue +43 -0
  18. package/runtime/components/{Desktop/Window/DesktopWindow.vue → window/DesktopCoreWindow.vue} +17 -6
  19. package/runtime/components/window/DesktopCoreWindowSnapHintsBase.vue +74 -0
  20. package/runtime/components/workspace/DesktopCoreWorkspaceEdgeHintsBase.vue +84 -0
  21. package/runtime/composables/useBlockNonInputContextMenu.ts +30 -0
  22. package/runtime/composables/useDesktopConfig.ts +8 -0
  23. package/runtime/composables/useDesktopDialogs.ts +16 -0
  24. package/runtime/composables/useDesktopExtension.ts +8 -0
  25. package/runtime/composables/useDesktopManager.ts +2 -0
  26. package/runtime/composables/useDesktopManifest.ts +17 -0
  27. package/runtime/composables/useDesktopSession.ts +27 -0
  28. package/runtime/composables/useDesktopShellIdentity.ts +99 -0
  29. package/runtime/composables/useDesktopShellOptions.ts +36 -0
  30. package/runtime/composables/useDesktopShellReady.ts +7 -0
  31. package/runtime/composables/useDesktopWorkArea.ts +93 -0
  32. package/runtime/composables/useTerminalManager.ts +102 -9
  33. package/runtime/composables/useToggleWindowMaximize.ts +35 -0
  34. package/runtime/composables/useWindowDragHandlers.ts +47 -0
  35. package/runtime/composables/useWindowSnapDrop.ts +112 -0
  36. package/runtime/composables/useWorkspaceEdgeDrop.ts +176 -0
  37. package/runtime/composables/useWorkspaceManager.ts +26 -0
  38. package/runtime/composables/useWorkspaceOverviewCapture.ts +92 -0
  39. package/runtime/composables/useWorkspaceOverviewLiveScale.ts +151 -0
  40. package/runtime/constants/desktopShellKeys.ts +12 -0
  41. package/runtime/constants/shellLayout.ts +5 -0
  42. package/runtime/dialogs/desktopDialogProvider.ts +73 -0
  43. package/runtime/internal/controllers/ApplicationController.ts +8 -2
  44. package/runtime/internal/controllers/WindowController.ts +93 -8
  45. package/runtime/plugins/01.desktop-shell-init.client.ts +28 -0
  46. package/runtime/plugins/02.desktop-register-desktop-apps.client.ts +2 -6
  47. package/runtime/stores/storeApplicationMeta.ts +2 -1
  48. package/runtime/stores/storeApplicationWindows.ts +2 -1
  49. package/runtime/stores/storeDesktop.ts +17 -57
  50. package/runtime/stores/storeDesktopDefaultApps.ts +2 -1
  51. package/runtime/stores/storeDesktopVolume.ts +2 -1
  52. package/runtime/stores/storeDesktopWindow.ts +20 -2
  53. package/runtime/stores/storeDesktopWorkspace.ts +31 -1
  54. package/runtime/stores/storeIds.ts +35 -0
  55. package/runtime/types/desktopMenu.ts +10 -0
  56. package/runtime/utils/initDesktopShell.ts +80 -0
  57. package/runtime/utils/migratePersistedStoreIds.ts +57 -0
  58. package/runtime/utils/utilApp.ts +0 -12
  59. package/runtime/utils/{captureElementToCanvas.ts → utilCaptureElementToCanvas.ts} +1 -1
  60. package/runtime/utils/utilDetectSnapZone.ts +57 -0
  61. package/runtime/utils/utilWindowLayout.ts +82 -0
  62. package/runtime/utils/utilWindowMaximizeLayout.ts +44 -0
  63. package/runtime/utils/utilWorkspaceWindows.ts +33 -0
  64. package/types/index.d.ts +125 -14
  65. package/bin/cli.js +0 -395
  66. package/bin/desktop.js +0 -5
  67. package/bin/lib/catalog.js +0 -293
  68. package/bin/lib/config.js +0 -109
  69. package/bin/lib/desktopConfig.js +0 -56
  70. package/bin/lib/generateTemplate.js +0 -389
  71. package/bin/lib/install.js +0 -408
  72. package/bin/lib/migrateModulePlayground.config.mjs +0 -164
  73. package/bin/lib/migrateModulePlayground.mjs +0 -313
  74. package/bin/lib/npmVersions.js +0 -124
  75. package/bin/lib/playgroundContext.js +0 -188
  76. package/bin/lib/scaffold.js +0 -197
  77. package/bin/lib/status.js +0 -289
  78. package/bin/lib/validateModule.js +0 -697
  79. package/bin/lib/workspace.js +0 -182
  80. package/bin/owd.js +0 -5
  81. package/bin/tui.js +0 -1379
  82. package/runtime/components/Desktop/Core/DesktopCore.vue +0 -90
  83. package/runtime/utils/explorerEntryPath.ts +0 -10
  84. package/runtime/utils/resolveDesktopConfigPath.ts +0 -52
  85. package/runtime/utils/splitDesktopUserConfig.ts +0 -85
  86. package/runtime/utils/utilDesktop.ts +0 -35
  87. package/runtime/utils/validateDesktopUserConfig.ts +0 -44
  88. /package/runtime/components/{Desktop/Background/DesktopBackground.vue → background/DesktopCoreBackground.vue} +0 -0
  89. /package/runtime/components/{Desktop/Time/DesktopTime.vue → time/DesktopCoreTime.vue} +0 -0
  90. /package/runtime/components/{Desktop/Window/DesktopWindowContent.vue → window/DesktopCoreWindowContent.vue} +0 -0
  91. /package/runtime/components/{Desktop/Window/DesktopWindowNav.vue → window/DesktopCoreWindowNav.vue} +0 -0
  92. /package/runtime/utils/{utilWindow.ts → utilWindowControllerAdapter.ts} +0 -0
@@ -1,43 +1,9 @@
1
1
  import { defineStore } from 'pinia'
2
2
  import { ref } from 'vue'
3
-
4
- export type DesktopWindowSurface = 'acrylic' | 'solid'
5
-
6
- export type DesktopAppearance = 'dark' | 'light'
7
-
8
- export type DesktopPersonalization = {
9
- windowSurface: DesktopWindowSurface
10
- /** Used when windowSurface is solid (CSS color string). */
11
- windowTint: string
12
- appearance: DesktopAppearance
13
- }
14
-
15
- export const defaultDesktopPersonalization: DesktopPersonalization = {
16
- windowSurface: 'acrylic',
17
- windowTint: '#2d2d30',
18
- appearance: 'dark',
19
- }
20
-
21
- export function mergePersonalization(
22
- raw: Partial<DesktopPersonalization> | undefined | null,
23
- ): DesktopPersonalization {
24
- const windowSurface =
25
- raw?.windowSurface === 'solid' || raw?.windowSurface === 'acrylic'
26
- ? raw.windowSurface
27
- : defaultDesktopPersonalization.windowSurface
28
- const appearance =
29
- raw?.appearance === 'light' || raw?.appearance === 'dark'
30
- ? raw.appearance
31
- : defaultDesktopPersonalization.appearance
32
- const tint =
33
- typeof raw?.windowTint === 'string' && raw.windowTint.trim()
34
- ? raw.windowTint.trim()
35
- : defaultDesktopPersonalization.windowTint
36
- return { windowSurface, windowTint: tint, appearance }
37
- }
3
+ import { DESKTOP_STORE_ID } from './storeIds'
38
4
 
39
5
  export const useDesktopStore = defineStore(
40
- 'owd/desktop',
6
+ DESKTOP_STORE_ID,
41
7
  () => {
42
8
  const state = ref<{
43
9
  workspace: {
@@ -52,7 +18,11 @@ export const useDesktopStore = defineStore(
52
18
  positionZ: number
53
19
  }
54
20
  defaultApps: Record<string, { applicationId: string; entry: string }>
55
- personalization: DesktopPersonalization
21
+ personalization: {
22
+ windowSurface: 'acrylic' | 'solid'
23
+ windowTint: string
24
+ appearance: 'dark' | 'light'
25
+ }
56
26
  }>({
57
27
  workspace: {
58
28
  overview: false,
@@ -66,33 +36,23 @@ export const useDesktopStore = defineStore(
66
36
  positionZ: 0,
67
37
  },
68
38
  defaultApps: {},
69
- personalization: { ...defaultDesktopPersonalization },
39
+ personalization: {
40
+ windowSurface: 'acrylic',
41
+ windowTint: '#2d2d30',
42
+ appearance: 'dark',
43
+ },
70
44
  })
71
45
 
72
- function setWindowSurface(value: DesktopWindowSurface) {
73
- const base = mergePersonalization(state.value.personalization)
74
- state.value.personalization = mergePersonalization({
75
- ...base,
76
- windowSurface: value,
77
- })
46
+ function setWindowSurface(value: 'acrylic' | 'solid') {
47
+ state.value.personalization.windowSurface = value
78
48
  }
79
49
 
80
50
  function setWindowTint(value: string) {
81
- const v = value.trim()
82
- if (!v) return
83
- const base = mergePersonalization(state.value.personalization)
84
- state.value.personalization = mergePersonalization({
85
- ...base,
86
- windowTint: v,
87
- })
51
+ state.value.personalization.windowTint = value
88
52
  }
89
53
 
90
- function setAppearance(value: DesktopAppearance) {
91
- const base = mergePersonalization(state.value.personalization)
92
- state.value.personalization = mergePersonalization({
93
- ...base,
94
- appearance: value,
95
- })
54
+ function setAppearance(value: 'dark' | 'light') {
55
+ state.value.personalization.appearance = value
96
56
  }
97
57
 
98
58
  return {
@@ -1,8 +1,9 @@
1
1
  import { defineStore } from 'pinia'
2
2
  import { useDesktopStore } from './storeDesktop'
3
+ import { DESKTOP_DEFAULT_APPS_STORE_ID } from './storeIds'
3
4
 
4
5
  export const useDesktopDefaultAppsStore = defineStore(
5
- 'owd/desktop/defaultApps',
6
+ DESKTOP_DEFAULT_APPS_STORE_ID,
6
7
  () => {
7
8
  const desktopStore = useDesktopStore()
8
9
 
@@ -1,8 +1,9 @@
1
1
  import { defineStore } from 'pinia'
2
2
  import { computed } from 'vue'
3
3
  import { useDesktopStore } from './storeDesktop'
4
+ import { DESKTOP_VOLUME_STORE_ID } from './storeIds'
4
5
 
5
- export const useDesktopVolumeStore = defineStore('owd/desktop/volume', () => {
6
+ export const useDesktopVolumeStore = defineStore(DESKTOP_VOLUME_STORE_ID, () => {
6
7
  const desktopStore = useDesktopStore()
7
8
 
8
9
  const master = computed(() => desktopStore.state.volume.master)
@@ -1,9 +1,21 @@
1
+ import type { DesktopWorkAreaRect } from '@owdproject/core'
1
2
  import { defineStore } from 'pinia'
2
- import { computed } from 'vue'
3
+ import { computed, readonly, shallowRef } from 'vue'
3
4
  import { useDesktopStore } from './storeDesktop'
5
+ import { DESKTOP_WINDOW_STORE_ID } from './storeIds'
4
6
 
5
- export const useDesktopWindowStore = defineStore('owd/desktop/window', () => {
7
+ export const EMPTY_DESKTOP_WORK_AREA: DesktopWorkAreaRect = {
8
+ x: 0,
9
+ y: 0,
10
+ width: 0,
11
+ height: 0,
12
+ }
13
+
14
+ export const useDesktopWindowStore = defineStore(DESKTOP_WINDOW_STORE_ID, () => {
6
15
  const desktopStore = useDesktopStore()
16
+ const workArea = shallowRef<DesktopWorkAreaRect>({
17
+ ...EMPTY_DESKTOP_WORK_AREA,
18
+ })
7
19
 
8
20
  const positionZ = computed(() => desktopStore.state.window.positionZ)
9
21
 
@@ -13,8 +25,14 @@ export const useDesktopWindowStore = defineStore('owd/desktop/window', () => {
13
25
  return desktopStore.state.window.positionZ
14
26
  }
15
27
 
28
+ function setWorkArea(rect: DesktopWorkAreaRect) {
29
+ workArea.value = { ...rect }
30
+ }
31
+
16
32
  return {
17
33
  positionZ,
18
34
  incrementPositionZ,
35
+ workArea: readonly(workArea),
36
+ setWorkArea,
19
37
  }
20
38
  })
@@ -1,10 +1,11 @@
1
1
  import { defineStore } from 'pinia'
2
2
  import { computed } from 'vue'
3
3
  import { useDesktopStore } from './storeDesktop'
4
+ import { DESKTOP_WORKSPACE_STORE_ID } from './storeIds'
4
5
  import { nanoid } from 'nanoid'
5
6
 
6
7
  export const useDesktopWorkspaceStore = defineStore(
7
- 'owd/desktop/workspace',
8
+ DESKTOP_WORKSPACE_STORE_ID,
8
9
  () => {
9
10
  const desktopStore = useDesktopStore()
10
11
 
@@ -54,6 +55,33 @@ export const useDesktopWorkspaceStore = defineStore(
54
55
  desktopStore.state.workspace.list.push(nanoid(8))
55
56
  }
56
57
 
58
+ /** Last workspace in list excluding the given id (window migration target). */
59
+ function resolveWorkspaceFallback(workspaceId: string): string | null {
60
+ const remaining = desktopStore.state.workspace.list.filter(
61
+ (id) => id !== workspaceId,
62
+ )
63
+ return remaining.at(-1) ?? null
64
+ }
65
+
66
+ function removeWorkspace(workspaceId: string): boolean {
67
+ const list = desktopStore.state.workspace.list
68
+ if (list.length <= 2) return false
69
+
70
+ const index = list.indexOf(workspaceId)
71
+ if (index === -1) return false
72
+
73
+ const fallbackId = resolveWorkspaceFallback(workspaceId)
74
+ if (!fallbackId) return false
75
+
76
+ list.splice(index, 1)
77
+
78
+ if (desktopStore.state.workspace.active === workspaceId) {
79
+ desktopStore.state.workspace.active = fallbackId
80
+ }
81
+
82
+ return true
83
+ }
84
+
57
85
  const workspaceActiveIndex = computed(() => {
58
86
  return desktopStore.state.workspace.list.findIndex(
59
87
  (workspace) => workspace === desktopStore.state.workspace.active,
@@ -70,6 +98,8 @@ export const useDesktopWorkspaceStore = defineStore(
70
98
  setOverview,
71
99
  setWorkspace,
72
100
  createWorkspace,
101
+ resolveWorkspaceFallback,
102
+ removeWorkspace,
73
103
  }
74
104
  },
75
105
  )
@@ -0,0 +1,35 @@
1
+ export const DESKTOP_STORE_ID = 'desktop'
2
+ export const DESKTOP_WORKSPACE_STORE_ID = 'desktop/workspace'
3
+ export const DESKTOP_WINDOW_STORE_ID = 'desktop/window'
4
+ export const DESKTOP_VOLUME_STORE_ID = 'desktop/volume'
5
+ export const DESKTOP_DEFAULT_APPS_STORE_ID = 'desktop/defaultApps'
6
+
7
+ export const applicationWindowsStoreId = (appId: string) =>
8
+ `desktop/application/${appId}/windows`
9
+
10
+ export const applicationMetaStoreId = (appId: string) =>
11
+ `desktop/application/${appId}/meta`
12
+
13
+ export const STORE_ID_MIGRATION_FLAG = 'desktop/store-id-migration/v1'
14
+
15
+ /** Static legacy Pinia store ids mapped to their 3.4 replacements. */
16
+ export const LEGACY_STORE_ID_MAP: Record<string, string> = {
17
+ 'owd/desktop': DESKTOP_STORE_ID,
18
+ 'owd/desktop/workspace': DESKTOP_WORKSPACE_STORE_ID,
19
+ 'owd/desktop/window': DESKTOP_WINDOW_STORE_ID,
20
+ 'owd/desktop/volume': DESKTOP_VOLUME_STORE_ID,
21
+ 'owd/desktop/defaultApps': DESKTOP_DEFAULT_APPS_STORE_ID,
22
+ }
23
+
24
+ export const LEGACY_APPLICATION_STORE_PATTERN =
25
+ /^owd\/application\/([^/]+)\/(windows|meta)$/
26
+
27
+ export function legacyApplicationStoreIdToNew(
28
+ legacyId: string,
29
+ ): string | null {
30
+ const match = legacyId.match(LEGACY_APPLICATION_STORE_PATTERN)
31
+ if (!match) return null
32
+
33
+ const [, appId, kind] = match
34
+ return `desktop/application/${appId}/${kind}`
35
+ }
@@ -0,0 +1,10 @@
1
+ /** Neutral menu model for window chrome (Menubar-compatible shape). */
2
+ export interface DesktopMenuItem {
3
+ label?: string
4
+ icon?: string
5
+ command?: () => void
6
+ separator?: boolean
7
+ disabled?: boolean
8
+ items?: DesktopMenuItem[]
9
+ [key: string]: unknown
10
+ }
@@ -0,0 +1,80 @@
1
+ import { setActivePinia } from 'pinia'
2
+ import type { Pinia } from 'pinia'
3
+ import { useAppConfig, useRuntimeConfig } from 'nuxt/app'
4
+ import { toRaw } from 'vue'
5
+ import { useDesktopManager } from '../composables/useDesktopManager'
6
+ import { useDesktopStore } from '../stores/storeDesktop'
7
+ import { useDesktopWorkspaceStore } from '../stores/storeDesktopWorkspace'
8
+ import { useDesktopDefaultAppsStore } from '../stores/storeDesktopDefaultApps'
9
+ import { registerDesktopPluginPinia } from '../../kit/defineDesktopApp'
10
+
11
+ /** Bind Nuxt's Pinia instance for store composables outside component setup timing. */
12
+ export function bindDesktopPinia(pinia: Pinia | undefined) {
13
+ if (pinia) {
14
+ setActivePinia(pinia)
15
+ registerDesktopPluginPinia(pinia)
16
+ }
17
+ }
18
+
19
+ /** Sync `appConfig.desktop` from runtime when the composable is not ready yet (early client plugins). */
20
+ export function ensureAppConfigDesktop() {
21
+ const runtimeConfig = useRuntimeConfig()
22
+ const publicDesktop = runtimeConfig.public?.desktop
23
+ if (!publicDesktop || typeof publicDesktop !== 'object') return undefined
24
+
25
+ let appConfig: ReturnType<typeof useAppConfig> | undefined
26
+ try {
27
+ appConfig = useAppConfig()
28
+ } catch {
29
+ return publicDesktop as DesktopConfig
30
+ }
31
+
32
+ if (!appConfig) return publicDesktop as DesktopConfig
33
+
34
+ if (!appConfig.desktop) {
35
+ appConfig.desktop = publicDesktop as DesktopConfig
36
+ }
37
+
38
+ return appConfig.desktop as DesktopConfig
39
+ }
40
+
41
+ function mergeShellKeysOntoAppConfig() {
42
+ if (import.meta.server) return
43
+
44
+ const desktopManager = useDesktopManager()
45
+ const desktop = ensureAppConfigDesktop()
46
+ if (!desktop) return
47
+
48
+ desktopManager.setConfig({
49
+ windows: desktop.windows ? toRaw(desktop.windows) : undefined,
50
+ systemBar: desktop.systemBar ? toRaw(desktop.systemBar) : undefined,
51
+ dockBar: desktop.dockBar ? toRaw(desktop.dockBar) : undefined,
52
+ workspaces: desktop.workspaces ? toRaw(desktop.workspaces) : undefined,
53
+ })
54
+ }
55
+
56
+ /** Re-run appConfig shell merge when `useAppConfig()` was not ready during early plugins. */
57
+ export function syncDesktopShellAppConfig() {
58
+ mergeShellKeysOntoAppConfig()
59
+ }
60
+
61
+ /** Kernel shell bootstrap: workspaces, default apps, merged shell config on appConfig. */
62
+ export function initDesktopShell() {
63
+ if (import.meta.server) return
64
+
65
+ const desktopStore = useDesktopStore()
66
+ const desktopWorkspaceStore = useDesktopWorkspaceStore()
67
+ const runtimeConfig = useRuntimeConfig()
68
+ const desktopDefaultAppsStore = useDesktopDefaultAppsStore()
69
+
70
+ if (desktopStore.$persistedState) {
71
+ desktopStore.$persistedState.isReady().then(() => {
72
+ desktopWorkspaceStore.setupWorkspaces()
73
+ })
74
+ } else {
75
+ desktopWorkspaceStore.setupWorkspaces()
76
+ }
77
+
78
+ desktopDefaultAppsStore.loadDefaultAppsFromConfig(runtimeConfig)
79
+ mergeShellKeysOntoAppConfig()
80
+ }
@@ -0,0 +1,57 @@
1
+ import {
2
+ LEGACY_STORE_ID_MAP,
3
+ STORE_ID_MIGRATION_FLAG,
4
+ legacyApplicationStoreIdToNew,
5
+ } from '../stores/storeIds'
6
+
7
+ function migrateStorageKey(
8
+ storage: Storage,
9
+ legacyId: string,
10
+ newId: string,
11
+ ) {
12
+ const legacyValue = storage.getItem(legacyId)
13
+ if (legacyValue === null) return
14
+
15
+ if (storage.getItem(newId) !== null) {
16
+ storage.removeItem(legacyId)
17
+ return
18
+ }
19
+
20
+ storage.setItem(newId, legacyValue)
21
+ storage.removeItem(legacyId)
22
+ }
23
+
24
+ function collectLegacyApplicationKeys(storage: Storage) {
25
+ const keys: string[] = []
26
+
27
+ for (let index = 0; index < storage.length; index++) {
28
+ const key = storage.key(index)
29
+ if (key && legacyApplicationStoreIdToNew(key)) {
30
+ keys.push(key)
31
+ }
32
+ }
33
+
34
+ return keys
35
+ }
36
+
37
+ /**
38
+ * One-shot migration of persisted Pinia state from legacy `owd/*` store ids
39
+ * to `desktop/*`. Runs before shell init so module-persistence hydrates new keys.
40
+ */
41
+ export function migratePersistedStoreIds(storage: Storage = localStorage) {
42
+ if (typeof storage?.getItem !== 'function') return
43
+ if (storage.getItem(STORE_ID_MIGRATION_FLAG) === 'done') return
44
+
45
+ for (const [legacyId, newId] of Object.entries(LEGACY_STORE_ID_MAP)) {
46
+ migrateStorageKey(storage, legacyId, newId)
47
+ }
48
+
49
+ for (const legacyId of collectLegacyApplicationKeys(storage)) {
50
+ const newId = legacyApplicationStoreIdToNew(legacyId)
51
+ if (newId) {
52
+ migrateStorageKey(storage, legacyId, newId)
53
+ }
54
+ }
55
+
56
+ storage.setItem(STORE_ID_MIGRATION_FLAG, 'done')
57
+ }
@@ -1,5 +1,3 @@
1
- import type { Nuxt } from '@nuxt/schema'
2
-
3
1
  export function normalizeApplicationConfig(
4
2
  config: ApplicationConfig,
5
3
  ): ApplicationConfig {
@@ -25,13 +23,3 @@ export function normalizeApplicationConfig(
25
23
  entries: normalizedEntries,
26
24
  }
27
25
  }
28
-
29
- export function registerTailwindPath(nuxt: Nuxt, path: string) {
30
- nuxt.options.runtimeConfig.app.owd = nuxt.options.runtimeConfig.app.owd || {}
31
- nuxt.options.runtimeConfig.app.owd.tailwindPaths =
32
- nuxt.options.runtimeConfig.app.owd.tailwindPaths || []
33
-
34
- if (!nuxt.options.runtimeConfig.app.owd.tailwindPaths.includes(path)) {
35
- nuxt.options.runtimeConfig.app.owd.tailwindPaths.push(path)
36
- }
37
- }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Renders a DOM subtree to a canvas via html2canvas (optional dependency).
3
- * Used for workspace thumbnails / previews; returns null if capture fails or html2canvas is missing.
3
+ * Used for workspace overview thumbnails; returns null if capture fails or html2canvas is missing.
4
4
  */
5
5
  export async function captureElementToCanvas(
6
6
  element: HTMLElement | null | undefined,
@@ -0,0 +1,57 @@
1
+ import type { DesktopWorkAreaRect, WindowLayout } from '@owdproject/core'
2
+ import {
3
+ WINDOW_SNAP_EDGE_PX,
4
+ WORKSPACE_EDGE_DROP_PX,
5
+ } from '../constants/shellLayout'
6
+ import { workAreaBottom, workAreaRight } from './utilWindowLayout'
7
+
8
+ export type SnapZoneHint = Exclude<WindowLayout, 'normal'>
9
+
10
+ export type DetectSnapZoneOptions = {
11
+ edgePx?: number
12
+ /** When true, left/right half snap is suppressed in the outer workspace-edge band. */
13
+ deferSideSnapForWorkspaceEdge?: boolean
14
+ }
15
+
16
+ /**
17
+ * Resolve snap zone from pointer position and shell work area.
18
+ * Returns null when the pointer is not in a snap band.
19
+ */
20
+ export function detectSnapZone(
21
+ clientX: number,
22
+ clientY: number,
23
+ workArea: DesktopWorkAreaRect,
24
+ options?: DetectSnapZoneOptions,
25
+ ): SnapZoneHint | null {
26
+ const edge = options?.edgePx ?? WINDOW_SNAP_EDGE_PX
27
+ const { x, y, width, height } = workArea
28
+
29
+ if (width <= 0 || height <= 0) {
30
+ return null
31
+ }
32
+
33
+ const bottom = workAreaBottom(workArea)
34
+ const right = workAreaRight(workArea)
35
+
36
+ const nearTop = clientY <= y + edge
37
+ const nearBottom = clientY >= bottom - edge
38
+ const nearLeft = clientX <= x + edge
39
+ const nearRight = clientX >= right - edge
40
+
41
+ const inWorkspaceEdgeBand =
42
+ options?.deferSideSnapForWorkspaceEdge === true &&
43
+ typeof globalThis !== 'undefined' &&
44
+ (clientX <= WORKSPACE_EDGE_DROP_PX ||
45
+ clientX >= globalThis.innerWidth - WORKSPACE_EDGE_DROP_PX)
46
+
47
+ if (nearTop && nearLeft) return 'top-left'
48
+ if (nearTop && nearRight) return 'top-right'
49
+ if (nearBottom && nearLeft) return 'bottom-left'
50
+ if (nearBottom && nearRight) return 'bottom-right'
51
+ if (nearTop) return 'maximize'
52
+ if (nearBottom) return 'bottom-half'
53
+ if (nearLeft && !inWorkspaceEdgeBand) return 'left-half'
54
+ if (nearRight && !inWorkspaceEdgeBand) return 'right-half'
55
+
56
+ return null
57
+ }
@@ -0,0 +1,82 @@
1
+ import type {
2
+ DesktopWorkAreaRect,
3
+ IWindowController,
4
+ WindowBounds,
5
+ WindowLayout,
6
+ } from '@owdproject/core'
7
+
8
+ export type { WindowLayout, WindowBounds, DesktopWorkAreaRect }
9
+
10
+ export function workAreaBottom(workArea: DesktopWorkAreaRect): number {
11
+ return workArea.y + workArea.height
12
+ }
13
+
14
+ export function workAreaRight(workArea: DesktopWorkAreaRect): number {
15
+ return workArea.x + workArea.width
16
+ }
17
+
18
+ /**
19
+ * Viewport rectangle for a snap zone inside the shell work area.
20
+ */
21
+ export function computeSnapRect(
22
+ zone: Exclude<WindowLayout, 'normal'>,
23
+ workArea: DesktopWorkAreaRect,
24
+ ): WindowBounds {
25
+ const { x, y, width, height } = workArea
26
+ const halfW = Math.floor(width / 2)
27
+ const halfH = Math.floor(height / 2)
28
+
29
+ switch (zone) {
30
+ case 'maximize':
31
+ return { x, y, width, height }
32
+ case 'left-half':
33
+ return { x, y, width: halfW, height }
34
+ case 'right-half':
35
+ return { x: x + halfW, y, width: width - halfW, height }
36
+ case 'bottom-half':
37
+ return { x, y: y + halfH, width, height: height - halfH }
38
+ case 'top-left':
39
+ return { x, y, width: halfW, height: halfH }
40
+ case 'top-right':
41
+ return { x: x + halfW, y, width: width - halfW, height: halfH }
42
+ case 'bottom-left':
43
+ return { x, y: y + halfH, width: halfW, height: height - halfH }
44
+ case 'bottom-right':
45
+ return {
46
+ x: x + halfW,
47
+ y: y + halfH,
48
+ width: width - halfW,
49
+ height: height - halfH,
50
+ }
51
+ default: {
52
+ const _exhaustive: never = zone
53
+ return _exhaustive
54
+ }
55
+ }
56
+ }
57
+
58
+ export function applyWindowBounds(
59
+ win: IWindowController,
60
+ bounds: WindowBounds,
61
+ ): void {
62
+ win.actions.setPosition({ x: bounds.x, y: bounds.y })
63
+ win.actions.setSize({ width: bounds.width, height: bounds.height })
64
+ }
65
+
66
+ export function applySnapToWindow(
67
+ win: IWindowController,
68
+ zone: Exclude<WindowLayout, 'normal'>,
69
+ workArea: DesktopWorkAreaRect,
70
+ ): boolean {
71
+ if (!win.isMaximizable) {
72
+ return false
73
+ }
74
+
75
+ win.actions.setLayout(zone)
76
+ applyWindowBounds(win, computeSnapRect(zone, workArea))
77
+ return true
78
+ }
79
+
80
+ export function restoreWindowBounds(win: IWindowController): boolean {
81
+ return win.actions.clearLayout()
82
+ }
@@ -0,0 +1,44 @@
1
+ function computeMaximizeRect(workArea: DesktopWorkAreaRect): WindowBounds {
2
+ const { x, y, width, height } = workArea
3
+ return { x, y, width, height }
4
+ }
5
+
6
+ function applyWindowBounds(
7
+ win: IWindowController,
8
+ bounds: WindowBounds,
9
+ ): void {
10
+ win.actions.setPosition({ x: bounds.x, y: bounds.y })
11
+ win.actions.setSize({ width: bounds.width, height: bounds.height })
12
+ }
13
+
14
+ function applyMaximizeToWindow(
15
+ win: IWindowController,
16
+ workArea: DesktopWorkAreaRect,
17
+ ): boolean {
18
+ if (!win.isMaximizable) {
19
+ return false
20
+ }
21
+
22
+ win.actions.setLayout('maximize')
23
+ applyWindowBounds(win, computeMaximizeRect(workArea))
24
+ return true
25
+ }
26
+
27
+ export function toggleWindowMaximizeLayout(
28
+ win: IWindowController,
29
+ workArea: DesktopWorkAreaRect,
30
+ ): boolean {
31
+ if (!win.isMaximizable) {
32
+ return false
33
+ }
34
+
35
+ if (win.isMaximized || (win.state.layout && win.state.layout !== 'normal')) {
36
+ return win.actions.clearLayout()
37
+ }
38
+
39
+ if (workArea.width <= 0 || workArea.height <= 0) {
40
+ return win.actions.setLayout('maximize')
41
+ }
42
+
43
+ return applyMaximizeToWindow(win, workArea)
44
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Collect open windows that belong to a workspace (explicit id or implicit active desktop).
3
+ */
4
+ export function collectWindowsOnWorkspace(
5
+ apps: Iterable<IApplicationController>,
6
+ workspaceId: string,
7
+ activeWorkspaceId: string,
8
+ ): IWindowController[] {
9
+ const result: IWindowController[] = []
10
+
11
+ for (const app of apps) {
12
+ for (const win of app.windows.values()) {
13
+ const ws = win.state.workspace
14
+ if (ws === workspaceId) {
15
+ result.push(win)
16
+ continue
17
+ }
18
+ if (!ws && workspaceId === activeWorkspaceId) {
19
+ result.push(win)
20
+ }
21
+ }
22
+ }
23
+
24
+ return result
25
+ }
26
+
27
+ export function countWindowsOnWorkspace(
28
+ apps: Iterable<IApplicationController>,
29
+ workspaceId: string,
30
+ activeWorkspaceId: string,
31
+ ): number {
32
+ return collectWindowsOnWorkspace(apps, workspaceId, activeWorkspaceId).length
33
+ }