@valentinkolb/cloud 0.4.0 → 0.5.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 (193) hide show
  1. package/package.json +18 -6
  2. package/scripts/preload.ts +78 -23
  3. package/src/_internal/define-app.ts +53 -46
  4. package/src/api/accounts-entities.ts +4 -0
  5. package/src/api/admin-core-settings.ts +98 -0
  6. package/src/api/announcements.ts +131 -0
  7. package/src/api/auth/schemas.ts +24 -0
  8. package/src/api/auth.ts +113 -10
  9. package/src/api/index.ts +7 -2
  10. package/src/api/me.ts +203 -14
  11. package/src/api/search/schemas.ts +1 -0
  12. package/src/api/search.ts +62 -8
  13. package/src/config/ssr.ts +2 -9
  14. package/src/contracts/announcements.test.ts +37 -0
  15. package/src/contracts/announcements.ts +121 -0
  16. package/src/contracts/app.ts +2 -0
  17. package/src/contracts/index.ts +3 -2
  18. package/src/contracts/registry.ts +2 -0
  19. package/src/contracts/shared.ts +108 -1
  20. package/src/desktop/index.ts +704 -0
  21. package/src/desktop/solid.tsx +938 -0
  22. package/src/server/api/index.ts +1 -1
  23. package/src/server/api/respond.ts +50 -10
  24. package/src/server/index.ts +44 -38
  25. package/src/server/middleware/auth.ts +98 -9
  26. package/src/server/middleware/index.ts +2 -1
  27. package/src/server/middleware/settings.ts +26 -0
  28. package/src/server/services/access.test.ts +197 -0
  29. package/src/server/services/access.ts +254 -6
  30. package/src/server/services/index.ts +14 -11
  31. package/src/server/services/pagination.ts +22 -0
  32. package/src/server/time.ts +45 -0
  33. package/src/services/account-lifecycle/index.ts +142 -18
  34. package/src/services/accounts/app.ts +658 -170
  35. package/src/services/accounts/authz.test.ts +77 -0
  36. package/src/services/accounts/authz.ts +22 -0
  37. package/src/services/accounts/entities.ts +84 -5
  38. package/src/services/accounts/groups.ts +30 -24
  39. package/src/services/accounts/model.test.ts +30 -0
  40. package/src/services/accounts/switching.test.ts +14 -0
  41. package/src/services/accounts/switching.ts +15 -6
  42. package/src/services/accounts/users.ts +75 -52
  43. package/src/services/announcements/index.test.ts +32 -0
  44. package/src/services/announcements/index.ts +224 -0
  45. package/src/services/audit/index.test.ts +84 -0
  46. package/src/services/audit/index.ts +431 -0
  47. package/src/services/auth-flows/index.ts +9 -2
  48. package/src/services/auth-flows/ipa.ts +0 -2
  49. package/src/services/auth-flows/magic-link.ts +3 -2
  50. package/src/services/auth-flows/password-reset.ts +284 -0
  51. package/src/services/auth-flows/proxy-return.test.ts +24 -0
  52. package/src/services/auth-flows/proxy-return.ts +49 -0
  53. package/src/services/gateway.ts +162 -0
  54. package/src/services/index.ts +44 -2
  55. package/src/services/ipa/effective-groups.test.ts +33 -0
  56. package/src/services/ipa/effective-groups.ts +70 -0
  57. package/src/services/ipa/profile.ts +45 -3
  58. package/src/services/ipa/search.ts +3 -5
  59. package/src/services/ipa/service-account.ts +15 -0
  60. package/src/services/ipa/sync-planning.test.ts +32 -0
  61. package/src/services/ipa/sync-planning.ts +22 -0
  62. package/src/services/ipa/sync.ts +110 -38
  63. package/src/services/oauth-tokens.ts +104 -0
  64. package/src/services/postgres.ts +21 -6
  65. package/src/services/providers/local/auth.test.ts +22 -0
  66. package/src/services/providers/local/auth.ts +46 -3
  67. package/src/services/secrets.ts +10 -0
  68. package/src/services/service-account-credentials.test.ts +210 -0
  69. package/src/services/service-account-credentials.ts +715 -0
  70. package/src/services/service-accounts.ts +188 -0
  71. package/src/services/session/index.ts +7 -8
  72. package/src/services/settings/app.ts +4 -20
  73. package/src/services/settings/defaults.ts +64 -22
  74. package/src/services/settings/store.ts +47 -0
  75. package/src/services/weather/forecast.ts +40 -7
  76. package/src/services/webauthn.test.ts +36 -0
  77. package/src/services/webauthn.ts +384 -0
  78. package/src/shared/icons.ts +391 -100
  79. package/src/shared/index.ts +7 -0
  80. package/src/shared/markdown/extensions/code.ts +38 -1
  81. package/src/shared/markdown/extensions/images.ts +39 -3
  82. package/src/shared/markdown/extensions/info-blocks.ts +5 -5
  83. package/src/shared/markdown/extensions/mark.ts +48 -0
  84. package/src/shared/markdown/extensions/sub-sup.ts +60 -0
  85. package/src/shared/markdown/extensions/tables.ts +79 -58
  86. package/src/shared/markdown/formula.test.ts +1089 -0
  87. package/src/shared/markdown/formula.ts +1187 -0
  88. package/src/shared/markdown/index.ts +76 -2
  89. package/src/shared/mock-cover.ts +130 -0
  90. package/src/shared/redirect.test.ts +49 -0
  91. package/src/shared/redirect.ts +52 -0
  92. package/src/shared/theme.test.ts +24 -0
  93. package/src/shared/theme.ts +68 -0
  94. package/src/shared/time.ts +13 -0
  95. package/src/ssr/AdminLayout.tsx +7 -3
  96. package/src/ssr/AdminSidebar.tsx +115 -49
  97. package/src/ssr/AppLaunchpad.island.tsx +176 -0
  98. package/src/ssr/Footer.island.tsx +3 -8
  99. package/src/ssr/GlobalAnnouncements.island.tsx +141 -0
  100. package/src/ssr/GlobalSearchDialog.tsx +545 -117
  101. package/src/ssr/HotkeysHelpRail.island.tsx +3 -70
  102. package/src/ssr/Layout.tsx +74 -66
  103. package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
  104. package/src/ssr/LayoutHelp.tsx +266 -0
  105. package/src/ssr/NavMenu.island.tsx +0 -39
  106. package/src/ssr/ThemeToggleRail.island.tsx +3 -3
  107. package/src/ssr/TimezoneCookie.island.tsx +23 -0
  108. package/src/ssr/islands/index.ts +13 -0
  109. package/src/styles/base-popover.css +5 -2
  110. package/src/styles/effects.css +87 -6
  111. package/src/styles/global.css +146 -9
  112. package/src/styles/input.css +3 -1
  113. package/src/styles/utilities-buttons.css +133 -27
  114. package/src/styles/utilities-code-display.css +67 -0
  115. package/src/styles/utilities-completion.css +223 -0
  116. package/src/styles/utilities-detail.css +73 -0
  117. package/src/styles/utilities-feedback.css +16 -15
  118. package/src/styles/utilities-layout.css +42 -2
  119. package/src/styles/utilities-markdown-editor.css +472 -0
  120. package/src/styles/utilities-navigation.css +63 -8
  121. package/src/styles/utilities-script.css +84 -0
  122. package/src/styles/utilities-table-tile.css +229 -0
  123. package/src/types/ambient.d.ts +9 -0
  124. package/src/ui/completion/behaviors.test.ts +95 -0
  125. package/src/ui/completion/behaviors.ts +205 -0
  126. package/src/ui/completion/engine.ts +368 -0
  127. package/src/ui/completion/index.ts +40 -0
  128. package/src/ui/completion/overlay.ts +92 -0
  129. package/src/ui/dialog-core.ts +173 -45
  130. package/src/ui/filter/FilterChip.tsx +42 -40
  131. package/src/ui/index.ts +11 -12
  132. package/src/ui/input/AutocompleteEditor.tsx +656 -0
  133. package/src/ui/input/CheckboxCard.tsx +91 -0
  134. package/src/ui/input/Combobox.tsx +375 -0
  135. package/src/ui/input/DatePicker.tsx +846 -0
  136. package/src/ui/input/DateTimeInput.tsx +29 -4
  137. package/src/ui/input/FileDropzone.tsx +116 -0
  138. package/src/ui/input/IconInput.tsx +116 -0
  139. package/src/ui/input/ImageInput.tsx +19 -2
  140. package/src/ui/input/MultiSelectInput.tsx +448 -0
  141. package/src/ui/input/NumberInput.tsx +417 -61
  142. package/src/ui/input/SegmentedControl.tsx +2 -2
  143. package/src/ui/input/Select.tsx +172 -10
  144. package/src/ui/input/Slider.tsx +3 -4
  145. package/src/ui/input/Switch.tsx +3 -2
  146. package/src/ui/input/TemplateEditor.tsx +212 -0
  147. package/src/ui/input/TextInput.tsx +144 -13
  148. package/src/ui/input/index.ts +53 -8
  149. package/src/ui/input/markdown/MarkdownEditor.tsx +774 -0
  150. package/src/ui/input/markdown/Toolbar.tsx +90 -0
  151. package/src/ui/input/markdown/actions.ts +233 -0
  152. package/src/ui/input/markdown/active-formats.ts +94 -0
  153. package/src/ui/input/markdown/behaviors.ts +193 -0
  154. package/src/ui/input/markdown/code-zone.ts +23 -0
  155. package/src/ui/input/markdown/highlight.ts +316 -0
  156. package/src/ui/layout.ts +22 -0
  157. package/src/ui/misc/AppOverview.tsx +105 -0
  158. package/src/ui/misc/AppWorkspace.tsx +607 -0
  159. package/src/ui/misc/Calendar.tsx +1291 -0
  160. package/src/ui/misc/Chart.tsx +162 -0
  161. package/src/ui/misc/CodeDisplay.tsx +54 -0
  162. package/src/ui/misc/ContextMenu.tsx +2 -2
  163. package/src/ui/misc/DataTable.tsx +269 -0
  164. package/src/ui/misc/DockWorkspace.tsx +425 -0
  165. package/src/ui/misc/Docs.tsx +153 -0
  166. package/src/ui/misc/Dropdown.tsx +2 -2
  167. package/src/ui/misc/EntitySearch.tsx +260 -129
  168. package/src/ui/misc/LinkCard.tsx +14 -2
  169. package/src/ui/misc/LogEntriesTable.tsx +34 -31
  170. package/src/ui/misc/Pagination.tsx +31 -12
  171. package/src/ui/misc/PanelDialog.tsx +109 -0
  172. package/src/ui/misc/Panes.tsx +873 -0
  173. package/src/ui/misc/PermissionEditor.tsx +358 -262
  174. package/src/ui/misc/Placeholder.tsx +40 -0
  175. package/src/ui/misc/ProgressBar.tsx +1 -1
  176. package/src/ui/misc/ResourceApiKeys.tsx +260 -0
  177. package/src/ui/misc/SettingsModal.tsx +150 -0
  178. package/src/ui/misc/StatCell.tsx +182 -40
  179. package/src/ui/misc/StatGrid.tsx +149 -0
  180. package/src/ui/misc/StructuredDataPreview.tsx +107 -0
  181. package/src/ui/misc/code-highlight.ts +213 -0
  182. package/src/ui/misc/index.ts +93 -12
  183. package/src/ui/prompts.tsx +362 -312
  184. package/src/ui/toast.ts +384 -0
  185. package/src/ui/widgets/Widget.tsx +12 -4
  186. package/src/ssr/MoreAppsDropdown.island.tsx +0 -61
  187. package/src/ui/ipa/GroupView.tsx +0 -36
  188. package/src/ui/ipa/LoginBtn.tsx +0 -16
  189. package/src/ui/ipa/UserView.tsx +0 -58
  190. package/src/ui/ipa/index.ts +0 -4
  191. package/src/ui/navigation.ts +0 -32
  192. package/src/ui/sidebar.tsx +0 -468
  193. /package/src/ui/{ipa → misc}/Avatar.tsx +0 -0
@@ -0,0 +1,938 @@
1
+ import type { Component, JSX, ParentProps } from "solid-js";
2
+ import { children, createContext, createEffect, createMemo, createSignal, For, onCleanup, onMount, Show, useContext } from "solid-js";
3
+ import { Dynamic, Portal } from "solid-js/web";
4
+ import { deserialize, serialize } from "seroval";
5
+ import {
6
+ desktop,
7
+ desktopWindowDescriptorKind,
8
+ readDesktopWindowDescriptor,
9
+ type DesktopEnvironment,
10
+ type DesktopWindowDescriptor,
11
+ } from "./index";
12
+
13
+ export type DesktopRouteProps<Params extends Record<string, string> = Record<string, string>> = {
14
+ params: Params;
15
+ path: string;
16
+ searchParams: URLSearchParams;
17
+ };
18
+
19
+ export type DesktopRouteDefinition = {
20
+ path: string;
21
+ component: Component<DesktopRouteProps>;
22
+ };
23
+
24
+ export type DesktopRouterProps = ParentProps<{
25
+ routes?: DesktopRouteDefinition[];
26
+ fallback?: Component<{ path: string }>;
27
+ }>;
28
+
29
+ type DesktopRouteMarker = DesktopRouteDefinition & { kind: "desktop-route" };
30
+
31
+ type DesktopWindowComponent = Component<any>;
32
+
33
+ type DesktopWindowRegistry<Definitions extends Record<string, DesktopWindowComponent>> = {
34
+ readonly __desktopWindowDefinitions: Definitions;
35
+ } & {
36
+ [Name in keyof Definitions]: Definitions[Name] extends Component<infer Props> ? (props: Props) => JSX.Element : never;
37
+ };
38
+
39
+ export const defineDesktopWindows = <Definitions extends Record<string, DesktopWindowComponent>>(
40
+ definitions: Definitions,
41
+ ): DesktopWindowRegistry<Definitions> => {
42
+ const registry: Record<string, unknown> = {};
43
+ Object.defineProperty(registry, "__desktopWindowDefinitions", { value: definitions, enumerable: false });
44
+
45
+ for (const name of Object.keys(definitions)) {
46
+ registry[name] = (props: Record<string, unknown>) =>
47
+ ({
48
+ kind: desktopWindowDescriptorKind,
49
+ name,
50
+ props: serialize(props ?? {}),
51
+ }) satisfies DesktopWindowDescriptor as unknown as JSX.Element;
52
+ }
53
+
54
+ return registry as DesktopWindowRegistry<Definitions>;
55
+ };
56
+
57
+ export function DesktopWindowHost<Definitions extends Record<string, DesktopWindowComponent>>(
58
+ props: ParentProps<{ windows: DesktopWindowRegistry<Definitions> }>,
59
+ ) {
60
+ const [nativeDescriptor, setNativeDescriptor] = createSignal<DesktopWindowDescriptor | null>(readDesktopWindowDescriptor());
61
+ const [ready, setReady] = createSignal(Boolean(nativeDescriptor()));
62
+ onMount(() => {
63
+ void desktop.window
64
+ .current()
65
+ .then(setNativeDescriptor)
66
+ .finally(() => setReady(true));
67
+ });
68
+ const descriptor = createMemo(() => nativeDescriptor());
69
+ const component = createMemo(() => {
70
+ const current = descriptor();
71
+ return current ? props.windows.__desktopWindowDefinitions[current.name] : null;
72
+ });
73
+ const windowProps = createMemo(() => {
74
+ const current = descriptor();
75
+ if (!current) return {};
76
+ return deserialize<Record<string, unknown>>(current.props);
77
+ });
78
+
79
+ return createMemo(() => {
80
+ if (!ready()) return null;
81
+ const WindowComponent = component();
82
+ return WindowComponent ? <Dynamic component={WindowComponent} {...windowProps()} /> : props.children;
83
+ }) as unknown as JSX.Element;
84
+ }
85
+
86
+ const routeEvent = "cloud-desktop:navigation";
87
+
88
+ const currentPath = () => {
89
+ if (typeof window === "undefined") return "/";
90
+ return `${window.location.pathname}${window.location.search}`;
91
+ };
92
+
93
+ const routeScore = (path: string) =>
94
+ path
95
+ .split("/")
96
+ .filter(Boolean)
97
+ .filter((segment) => !segment.startsWith(":")).length;
98
+
99
+ const matchRoute = (pattern: string, pathname: string): Record<string, string> | null => {
100
+ const patternSegments = pattern.split("/").filter(Boolean);
101
+ const pathSegments = pathname.split("/").filter(Boolean);
102
+ if (patternSegments.length !== pathSegments.length) return null;
103
+
104
+ const params: Record<string, string> = {};
105
+ for (let index = 0; index < patternSegments.length; index++) {
106
+ const patternSegment = patternSegments[index]!;
107
+ const pathSegment = pathSegments[index]!;
108
+ if (patternSegment.startsWith(":")) {
109
+ params[patternSegment.slice(1)] = decodeURIComponent(pathSegment);
110
+ continue;
111
+ }
112
+ if (patternSegment !== pathSegment) return null;
113
+ }
114
+ return params;
115
+ };
116
+
117
+ const readLocation = () => {
118
+ const url = typeof window === "undefined" ? new URL("http://desktop.local/") : new URL(window.location.href);
119
+ return {
120
+ pathname: url.pathname,
121
+ search: url.search,
122
+ path: `${url.pathname}${url.search}`,
123
+ searchParams: url.searchParams,
124
+ };
125
+ };
126
+
127
+ export const useDesktopLocation = () => {
128
+ const [location, setLocation] = createSignal(readLocation());
129
+
130
+ onMount(() => {
131
+ const update = () => setLocation(readLocation());
132
+ window.addEventListener("popstate", update);
133
+ window.addEventListener(routeEvent, update);
134
+ onCleanup(() => {
135
+ window.removeEventListener("popstate", update);
136
+ window.removeEventListener(routeEvent, update);
137
+ });
138
+ });
139
+
140
+ return location;
141
+ };
142
+
143
+ export function Route(props: DesktopRouteDefinition): JSX.Element {
144
+ return { kind: "desktop-route", path: props.path, component: props.component } satisfies DesktopRouteMarker as unknown as JSX.Element;
145
+ }
146
+
147
+ const isRouteMarker = (value: unknown): value is DesktopRouteMarker =>
148
+ Boolean(value && typeof value === "object" && (value as { kind?: unknown }).kind === "desktop-route");
149
+
150
+ export function DesktopRouter(props: DesktopRouterProps) {
151
+ const location = useDesktopLocation();
152
+ const resolved = children(() => props.children);
153
+ const routeDefinitions = createMemo(() => {
154
+ const childRoutes = [resolved()].flat(Number.POSITIVE_INFINITY).filter(isRouteMarker) as unknown as DesktopRouteDefinition[];
155
+ return ([...(props.routes ?? []), ...childRoutes] satisfies DesktopRouteDefinition[]).sort(
156
+ (a, b) => routeScore(b.path) - routeScore(a.path),
157
+ );
158
+ });
159
+
160
+ const match = createMemo(() => {
161
+ const loc = location();
162
+ for (const route of routeDefinitions()) {
163
+ const params = matchRoute(route.path, loc.pathname);
164
+ if (params) return { route, params, location: loc };
165
+ }
166
+ return null;
167
+ });
168
+
169
+ return createMemo(() => {
170
+ const selected = match();
171
+ if (!selected) {
172
+ return props.fallback ? <Dynamic component={props.fallback} path={currentPath()} /> : null;
173
+ }
174
+ return (
175
+ <Dynamic
176
+ component={selected.route.component}
177
+ params={selected.params}
178
+ path={selected.location.path}
179
+ searchParams={selected.location.searchParams}
180
+ />
181
+ );
182
+ }) as unknown as JSX.Element;
183
+ }
184
+
185
+ export type DesktopLinkProps = Omit<JSX.AnchorHTMLAttributes<HTMLAnchorElement>, "href" | "onClick"> & {
186
+ href: string;
187
+ replace?: boolean;
188
+ onClick?: JSX.EventHandlerUnion<HTMLAnchorElement, MouseEvent>;
189
+ };
190
+
191
+ const shouldHandleClick = (event: MouseEvent, anchor: HTMLAnchorElement): boolean => {
192
+ if (event.defaultPrevented || event.button !== 0) return false;
193
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return false;
194
+ if (anchor.target && anchor.target !== "_self") return false;
195
+ if (anchor.hasAttribute("download")) return false;
196
+ return new URL(anchor.href, window.location.href).origin === window.location.origin;
197
+ };
198
+
199
+ export function Link(props: DesktopLinkProps) {
200
+ const rest = () => {
201
+ const { href: _href, replace: _replace, onClick: _onClick, ...anchorProps } = props;
202
+ return anchorProps;
203
+ };
204
+
205
+ return (
206
+ <a
207
+ {...rest()}
208
+ href={props.href}
209
+ onClick={(event) => {
210
+ if (typeof props.onClick === "function") {
211
+ props.onClick(event as MouseEvent & { currentTarget: HTMLAnchorElement; target: Element });
212
+ }
213
+ if (!shouldHandleClick(event, event.currentTarget)) return;
214
+ event.preventDefault();
215
+ desktop.navigate(props.href, { replace: props.replace });
216
+ }}
217
+ />
218
+ );
219
+ }
220
+
221
+ type ContextMenuState = {
222
+ open: (event: MouseEvent) => void;
223
+ close: () => void;
224
+ isOpen: () => boolean;
225
+ x: () => number;
226
+ y: () => number;
227
+ };
228
+
229
+ const ContextMenuContext = createContext<ContextMenuState>();
230
+
231
+ type ContextMenuComponent = ((props: ParentProps) => JSX.Element) & {
232
+ Trigger: (props: ParentProps<{ class?: string }>) => JSX.Element;
233
+ Content: (props: ParentProps<{ class?: string }>) => JSX.Element;
234
+ Item: (props: ParentProps<{ onSelect?: () => void; destructive?: boolean; disabled?: boolean; icon?: string }>) => JSX.Element;
235
+ Divider: () => JSX.Element;
236
+ };
237
+
238
+ export const ContextMenu: ContextMenuComponent = ((props: ParentProps) => {
239
+ const [open, setOpen] = createSignal(false);
240
+ const [coords, setCoords] = createSignal({ x: 0, y: 0 });
241
+ const close = () => setOpen(false);
242
+
243
+ onMount(() => {
244
+ const onPointer = () => close();
245
+ const onKey = (event: KeyboardEvent) => {
246
+ if (event.key === "Escape") close();
247
+ };
248
+ document.addEventListener("mousedown", onPointer);
249
+ document.addEventListener("keydown", onKey);
250
+ onCleanup(() => {
251
+ document.removeEventListener("mousedown", onPointer);
252
+ document.removeEventListener("keydown", onKey);
253
+ });
254
+ });
255
+
256
+ return (
257
+ <ContextMenuContext.Provider
258
+ value={{
259
+ open: (event) => {
260
+ event.preventDefault();
261
+ event.stopPropagation();
262
+ setCoords({ x: event.clientX, y: event.clientY });
263
+ setOpen(true);
264
+ },
265
+ close,
266
+ isOpen: open,
267
+ x: () => coords().x,
268
+ y: () => coords().y,
269
+ }}
270
+ >
271
+ {props.children}
272
+ </ContextMenuContext.Provider>
273
+ );
274
+ }) as ContextMenuComponent;
275
+
276
+ const useContextMenu = () => {
277
+ const value = useContext(ContextMenuContext);
278
+ if (!value) throw new Error("ContextMenu components must be used inside <ContextMenu>.");
279
+ return value;
280
+ };
281
+
282
+ ContextMenu.Trigger = (props) => {
283
+ const menu = useContextMenu();
284
+ return (
285
+ <div class={props.class} role="group" onContextMenu={menu.open}>
286
+ {props.children}
287
+ </div>
288
+ );
289
+ };
290
+
291
+ ContextMenu.Content = (props) => {
292
+ const menu = useContextMenu();
293
+ return (
294
+ <Show when={menu.isOpen()}>
295
+ <Portal>
296
+ <div
297
+ role="menu"
298
+ class={`fixed z-50 w-56 max-w-[min(22rem,calc(100vw-1rem))] overflow-hidden rounded-lg border border-zinc-300/60 bg-white/95 py-1 text-sm text-zinc-900 shadow-lg ring-1 ring-black/5 backdrop-blur-sm dark:border-zinc-600/50 dark:bg-zinc-950/95 dark:text-zinc-100 ${props.class ?? ""}`}
299
+ style={{
300
+ left: `${Math.min(menu.x(), window.innerWidth - 232)}px`,
301
+ top: `${Math.min(menu.y(), window.innerHeight - 320)}px`,
302
+ }}
303
+ onMouseDown={(event) => event.stopPropagation()}
304
+ >
305
+ {props.children}
306
+ </div>
307
+ </Portal>
308
+ </Show>
309
+ );
310
+ };
311
+
312
+ ContextMenu.Item = (props) => {
313
+ const menu = useContextMenu();
314
+ return (
315
+ <button
316
+ type="button"
317
+ role="menuitem"
318
+ disabled={props.disabled}
319
+ class={`flex w-full items-center gap-2 px-3 py-2 text-left transition-colors hover:bg-zinc-100 disabled:pointer-events-none disabled:opacity-50 dark:hover:bg-zinc-900 ${
320
+ props.destructive ? "text-red-600 dark:text-red-400" : "text-zinc-800 dark:text-zinc-100"
321
+ }`}
322
+ onClick={() => {
323
+ props.onSelect?.();
324
+ menu.close();
325
+ }}
326
+ >
327
+ <Show when={props.icon}>
328
+ <i class={`${props.icon} text-sm`} />
329
+ </Show>
330
+ <span>{props.children}</span>
331
+ </button>
332
+ );
333
+ };
334
+
335
+ ContextMenu.Divider = () => <hr class="my-1 border-zinc-200 dark:border-zinc-800" />;
336
+
337
+ const DESKTOP_WORKSPACE_SIDEBAR = Symbol("DesktopWorkspace.Sidebar");
338
+ const DESKTOP_WORKSPACE_TOPBAR = Symbol("DesktopWorkspace.TopBar");
339
+ const DESKTOP_WORKSPACE_MAIN = Symbol("DesktopWorkspace.Main");
340
+ const DESKTOP_WORKSPACE_RIGHT = Symbol("DesktopWorkspace.Right");
341
+ const DESKTOP_WORKSPACE_BOTTOM = Symbol("DesktopWorkspace.Bottom");
342
+ const DESKTOP_WORKSPACE_SIDEBAR_RAIL = Symbol("DesktopWorkspace.SidebarRail");
343
+ const DESKTOP_WORKSPACE_RIGHT_RAIL = Symbol("DesktopWorkspace.RightRail");
344
+ const DESKTOP_WORKSPACE_BOTTOM_RAIL = Symbol("DesktopWorkspace.BottomRail");
345
+
346
+ type DesktopWorkspaceSlotKind =
347
+ | typeof DESKTOP_WORKSPACE_SIDEBAR
348
+ | typeof DESKTOP_WORKSPACE_TOPBAR
349
+ | typeof DESKTOP_WORKSPACE_MAIN
350
+ | typeof DESKTOP_WORKSPACE_RIGHT
351
+ | typeof DESKTOP_WORKSPACE_BOTTOM
352
+ | typeof DESKTOP_WORKSPACE_SIDEBAR_RAIL
353
+ | typeof DESKTOP_WORKSPACE_RIGHT_RAIL
354
+ | typeof DESKTOP_WORKSPACE_BOTTOM_RAIL;
355
+
356
+ type DesktopWorkspaceSlot = {
357
+ readonly kind: DesktopWorkspaceSlotKind;
358
+ children?: JSX.Element;
359
+ };
360
+
361
+ export type DesktopWorkspacePanel = "left" | "right" | "bottom";
362
+ export type DesktopWorkspacePanelMode = "open" | "rail" | "hidden";
363
+
364
+ export type DesktopWorkspacePanelController = {
365
+ open: () => void;
366
+ rail: () => void;
367
+ hide: () => void;
368
+ toggle: () => void;
369
+ mode: () => DesktopWorkspacePanelMode;
370
+ };
371
+
372
+ export type DesktopWorkspaceResizablePaneProps = ParentProps<{
373
+ class?: string;
374
+ defaultSize?: number;
375
+ minSize?: number;
376
+ maxSize?: number;
377
+ resizable?: boolean;
378
+ railAt?: number;
379
+ restoreSize?: number;
380
+ }>;
381
+
382
+ export type DesktopWorkspacePaneProps = ParentProps<{
383
+ class?: string;
384
+ }>;
385
+
386
+ export type DesktopWorkspaceRailProps = ParentProps<{
387
+ class?: string;
388
+ size?: number;
389
+ }>;
390
+
391
+ export type DesktopWorkspaceTopBarProps = ParentProps<{
392
+ class?: string;
393
+ drag?: boolean;
394
+ }>;
395
+
396
+ export type DesktopWorkspaceSidebarProps = DesktopWorkspaceResizablePaneProps & {
397
+ trafficLightsInset?: boolean;
398
+ title?: string;
399
+ };
400
+
401
+ type DesktopWorkspaceSidebarSlot = DesktopWorkspaceSlot &
402
+ Omit<DesktopWorkspaceSidebarProps, "children"> & {
403
+ readonly kind: typeof DESKTOP_WORKSPACE_SIDEBAR;
404
+ };
405
+
406
+ type DesktopWorkspaceTopBarSlot = DesktopWorkspaceSlot &
407
+ Omit<DesktopWorkspaceTopBarProps, "children"> & {
408
+ readonly kind: typeof DESKTOP_WORKSPACE_TOPBAR;
409
+ };
410
+
411
+ type DesktopWorkspaceMainSlot = DesktopWorkspaceSlot &
412
+ Omit<DesktopWorkspacePaneProps, "children"> & {
413
+ readonly kind: typeof DESKTOP_WORKSPACE_MAIN;
414
+ };
415
+
416
+ type DesktopWorkspaceRightSlot = DesktopWorkspaceSlot &
417
+ Omit<DesktopWorkspaceResizablePaneProps, "children"> & {
418
+ readonly kind: typeof DESKTOP_WORKSPACE_RIGHT;
419
+ };
420
+
421
+ type DesktopWorkspaceBottomSlot = DesktopWorkspaceSlot &
422
+ Omit<DesktopWorkspaceResizablePaneProps, "children"> & {
423
+ readonly kind: typeof DESKTOP_WORKSPACE_BOTTOM;
424
+ };
425
+
426
+ type DesktopWorkspaceSidebarRailSlot = DesktopWorkspaceSlot &
427
+ Omit<DesktopWorkspaceRailProps, "children"> & {
428
+ readonly kind: typeof DESKTOP_WORKSPACE_SIDEBAR_RAIL;
429
+ };
430
+
431
+ type DesktopWorkspaceRightRailSlot = DesktopWorkspaceSlot &
432
+ Omit<DesktopWorkspaceRailProps, "children"> & {
433
+ readonly kind: typeof DESKTOP_WORKSPACE_RIGHT_RAIL;
434
+ };
435
+
436
+ type DesktopWorkspaceBottomRailSlot = DesktopWorkspaceSlot &
437
+ Omit<DesktopWorkspaceRailProps, "children"> & {
438
+ readonly kind: typeof DESKTOP_WORKSPACE_BOTTOM_RAIL;
439
+ };
440
+
441
+ export type DesktopWorkspaceProps = ParentProps<{
442
+ class?: string;
443
+ storageKey?: string;
444
+ topBarHeight?: number;
445
+ }>;
446
+
447
+ type DesktopWorkspaceComponent = ((props: DesktopWorkspaceProps) => JSX.Element) & {
448
+ Sidebar: (props: DesktopWorkspaceSidebarProps) => JSX.Element;
449
+ TopBar: (props: DesktopWorkspaceTopBarProps) => JSX.Element;
450
+ Main: (props: DesktopWorkspacePaneProps) => JSX.Element;
451
+ Right: (props: DesktopWorkspaceResizablePaneProps) => JSX.Element;
452
+ Bottom: (props: DesktopWorkspaceResizablePaneProps) => JSX.Element;
453
+ SidebarRail: (props: DesktopWorkspaceRailProps) => JSX.Element;
454
+ RightRail: (props: DesktopWorkspaceRailProps) => JSX.Element;
455
+ BottomRail: (props: DesktopWorkspaceRailProps) => JSX.Element;
456
+ DragRegion: (props: ParentProps<{ class?: string }>) => JSX.Element;
457
+ NoDrag: (props: ParentProps<{ class?: string }>) => JSX.Element;
458
+ };
459
+
460
+ const isDesktopWorkspaceSlot = (value: unknown): value is DesktopWorkspaceSlot =>
461
+ Boolean(value && typeof value === "object" && "kind" in value);
462
+
463
+ const collectDesktopWorkspaceSlots = (value: unknown): DesktopWorkspaceSlot[] => {
464
+ if (Array.isArray(value)) return value.flatMap(collectDesktopWorkspaceSlots);
465
+ return isDesktopWorkspaceSlot(value) ? [value] : [];
466
+ };
467
+
468
+ const clamp = (value: number, min: number, max: number): number => Math.min(max, Math.max(min, value));
469
+
470
+ const readStoredPaneSize = (storageKey: string | undefined, pane: string, fallback: number): number => {
471
+ if (!storageKey || typeof window === "undefined") return fallback;
472
+ const stored = Number(window.localStorage.getItem(`cloud-desktop-workspace:${storageKey}:${pane}`));
473
+ return Number.isFinite(stored) && stored > 0 ? stored : fallback;
474
+ };
475
+
476
+ const writeStoredPaneSize = (storageKey: string | undefined, pane: string, value: number) => {
477
+ if (!storageKey || typeof window === "undefined") return;
478
+ window.localStorage.setItem(`cloud-desktop-workspace:${storageKey}:${pane}`, String(Math.round(value)));
479
+ };
480
+
481
+ const startDesktopResize = (event: PointerEvent, onMove: (dx: number, dy: number) => void) => {
482
+ event.preventDefault();
483
+ const startX = event.clientX;
484
+ const startY = event.clientY;
485
+ const move = (next: PointerEvent) => onMove(next.clientX - startX, next.clientY - startY);
486
+ const stop = () => {
487
+ window.removeEventListener("pointermove", move);
488
+ window.removeEventListener("pointerup", stop);
489
+ document.body.style.userSelect = "";
490
+ document.body.style.cursor = "";
491
+ };
492
+ window.addEventListener("pointermove", move);
493
+ window.addEventListener("pointerup", stop);
494
+ };
495
+
496
+ const desktopWorkspaceGap = 8;
497
+ const desktopResizeHandleClass = "absolute z-20 transition-colors hover:bg-blue-400/20 active:bg-blue-400/30 dark:hover:bg-blue-400/15";
498
+ const desktopRestoreHandleClass =
499
+ "absolute z-30 opacity-0 transition-[opacity,background-color] hover:opacity-100 hover:bg-blue-400/25 active:bg-blue-400/35 dark:hover:bg-blue-400/20";
500
+
501
+ let currentPanels: Partial<Record<DesktopWorkspacePanel, DesktopWorkspacePanelController>> = {};
502
+
503
+ const requirePanel = (panel: DesktopWorkspacePanel): DesktopWorkspacePanelController => {
504
+ const controller = currentPanels[panel];
505
+ if (!controller) throw new Error(`No active DesktopWorkspace panel registered for "${panel}".`);
506
+ return controller;
507
+ };
508
+
509
+ export const workspace = {
510
+ panel(panel: DesktopWorkspacePanel): DesktopWorkspacePanelController {
511
+ return {
512
+ open: () => requirePanel(panel).open(),
513
+ rail: () => requirePanel(panel).rail(),
514
+ hide: () => requirePanel(panel).hide(),
515
+ toggle: () => requirePanel(panel).toggle(),
516
+ mode: () => currentPanels[panel]?.mode() ?? "hidden",
517
+ };
518
+ },
519
+ };
520
+
521
+ export const DesktopWorkspace = ((props: DesktopWorkspaceProps) => {
522
+ const resolved = children(() => props.children);
523
+ const slots = createMemo(() => collectDesktopWorkspaceSlots(resolved()));
524
+ const sidebar = createMemo(() => slots().find((slot): slot is DesktopWorkspaceSidebarSlot => slot.kind === DESKTOP_WORKSPACE_SIDEBAR));
525
+ const topBar = createMemo(() => slots().find((slot): slot is DesktopWorkspaceTopBarSlot => slot.kind === DESKTOP_WORKSPACE_TOPBAR));
526
+ const main = createMemo(() => slots().find((slot): slot is DesktopWorkspaceMainSlot => slot.kind === DESKTOP_WORKSPACE_MAIN));
527
+ const right = createMemo(() => slots().find((slot): slot is DesktopWorkspaceRightSlot => slot.kind === DESKTOP_WORKSPACE_RIGHT));
528
+ const bottom = createMemo(() => slots().find((slot): slot is DesktopWorkspaceBottomSlot => slot.kind === DESKTOP_WORKSPACE_BOTTOM));
529
+ const sidebarRail = createMemo(() =>
530
+ slots().find((slot): slot is DesktopWorkspaceSidebarRailSlot => slot.kind === DESKTOP_WORKSPACE_SIDEBAR_RAIL),
531
+ );
532
+ const rightRail = createMemo(() =>
533
+ slots().find((slot): slot is DesktopWorkspaceRightRailSlot => slot.kind === DESKTOP_WORKSPACE_RIGHT_RAIL),
534
+ );
535
+ const bottomRail = createMemo(() =>
536
+ slots().find((slot): slot is DesktopWorkspaceBottomRailSlot => slot.kind === DESKTOP_WORKSPACE_BOTTOM_RAIL),
537
+ );
538
+
539
+ const topBarHeight = () => props.topBarHeight ?? 44;
540
+ const [sidebarSize, setSidebarSize] = createSignal(readStoredPaneSize(props.storageKey, "sidebar", sidebar()?.defaultSize ?? 280));
541
+ const [rightSize, setRightSize] = createSignal(readStoredPaneSize(props.storageKey, "right", right()?.defaultSize ?? 320));
542
+ const [bottomSize, setBottomSize] = createSignal(readStoredPaneSize(props.storageKey, "bottom", bottom()?.defaultSize ?? 180));
543
+ const [sidebarMode, setSidebarMode] = createSignal<DesktopWorkspacePanelMode>(sidebar() ? "open" : "hidden");
544
+ const [rightMode, setRightMode] = createSignal<DesktopWorkspacePanelMode>(right() ? "open" : "hidden");
545
+ const [bottomMode, setBottomMode] = createSignal<DesktopWorkspacePanelMode>(bottom() ? "open" : "hidden");
546
+
547
+ const sidebarOpen = () => sidebarMode() === "open" && Boolean(sidebar());
548
+ const rightOpen = () => rightMode() === "open" && Boolean(right());
549
+ const bottomOpen = () => bottomMode() === "open" && Boolean(bottom());
550
+ const sidebarRailOpen = () => sidebarMode() === "rail" && Boolean(sidebarRail());
551
+ const rightRailOpen = () => rightMode() === "rail" && Boolean(rightRail());
552
+ const bottomRailOpen = () => bottomMode() === "rail" && Boolean(bottomRail());
553
+ const sidebarColumnSize = () => (sidebarOpen() ? sidebarSize() : sidebarRailOpen() ? (sidebarRail()?.size ?? 52) : 0);
554
+ const rightColumnSize = () => (rightOpen() ? rightSize() : rightRailOpen() ? (rightRail()?.size ?? 48) : 0);
555
+ const bottomRowSize = () => (bottomOpen() ? bottomSize() : bottomRailOpen() ? (bottomRail()?.size ?? 40) : 0);
556
+
557
+ const gridStyle = () =>
558
+ [
559
+ `grid-template-columns:${sidebarColumnSize()}px minmax(0,1fr) ${rightColumnSize()}px`,
560
+ `grid-template-rows:${topBar() ? `${topBarHeight()}px` : "0px"} minmax(0,1fr) ${bottomRowSize()}px`,
561
+ ].join(";");
562
+ const mainGridColumn = () => `${sidebarColumnSize() > 0 ? "2" : "1"} / ${rightColumnSize() > 0 ? "3" : "4"}`;
563
+ const bottomGridColumn = () => `${sidebarColumnSize() > 0 ? "2" : "1"} / 4`;
564
+
565
+ const topBarTrafficLightInset = () => Boolean(sidebar() && !sidebarOpen() && (sidebar()?.trafficLightsInset ?? true));
566
+
567
+ const shouldRail = (pane: DesktopWorkspaceResizablePaneProps | undefined, size: number) =>
568
+ Boolean(pane?.railAt !== undefined && size <= pane.railAt);
569
+
570
+ const restoreSidebar = () => {
571
+ const pane = sidebar();
572
+ if (!pane) return;
573
+ const next = clamp(pane.restoreSize ?? sidebarSize(), pane.minSize ?? 200, pane.maxSize ?? 480);
574
+ setSidebarSize(next);
575
+ writeStoredPaneSize(props.storageKey, "sidebar", next);
576
+ setSidebarMode("open");
577
+ };
578
+
579
+ const restoreRight = () => {
580
+ const pane = right();
581
+ if (!pane) return;
582
+ const next = clamp(pane.restoreSize ?? rightSize(), pane.minSize ?? 240, pane.maxSize ?? 560);
583
+ setRightSize(next);
584
+ writeStoredPaneSize(props.storageKey, "right", next);
585
+ setRightMode("open");
586
+ };
587
+
588
+ const restoreBottom = () => {
589
+ const pane = bottom();
590
+ if (!pane) return;
591
+ const next = clamp(pane.restoreSize ?? bottomSize(), pane.minSize ?? 120, pane.maxSize ?? 420);
592
+ setBottomSize(next);
593
+ writeStoredPaneSize(props.storageKey, "bottom", next);
594
+ setBottomMode("open");
595
+ };
596
+
597
+ const railSidebar = () => setSidebarMode(sidebarRail() ? "rail" : "hidden");
598
+ const railRight = () => setRightMode(rightRail() ? "rail" : "hidden");
599
+ const railBottom = () => setBottomMode(bottomRail() ? "rail" : "hidden");
600
+ const hideSidebar = () => setSidebarMode("hidden");
601
+ const hideRight = () => setRightMode("hidden");
602
+ const hideBottom = () => setBottomMode("hidden");
603
+
604
+ currentPanels = {
605
+ left: {
606
+ open: restoreSidebar,
607
+ rail: railSidebar,
608
+ hide: hideSidebar,
609
+ toggle: () => (sidebarOpen() ? railSidebar() : restoreSidebar()),
610
+ mode: sidebarMode,
611
+ },
612
+ right: {
613
+ open: restoreRight,
614
+ rail: railRight,
615
+ hide: hideRight,
616
+ toggle: () => (rightOpen() ? railRight() : restoreRight()),
617
+ mode: rightMode,
618
+ },
619
+ bottom: {
620
+ open: restoreBottom,
621
+ rail: railBottom,
622
+ hide: hideBottom,
623
+ toggle: () => (bottomOpen() ? railBottom() : restoreBottom()),
624
+ mode: bottomMode,
625
+ },
626
+ };
627
+
628
+ onCleanup(() => {
629
+ if (currentPanels.left?.open === restoreSidebar) currentPanels = {};
630
+ });
631
+
632
+ const resizeSidebar = (event: PointerEvent) => {
633
+ const pane = sidebar();
634
+ if (!pane?.resizable) return;
635
+ const start = sidebarOpen() ? sidebarSize() : sidebarColumnSize();
636
+ document.body.style.cursor = "col-resize";
637
+ document.body.style.userSelect = "none";
638
+ startDesktopResize(event, (dx) => {
639
+ const raw = start + dx;
640
+ if (shouldRail(pane, raw)) {
641
+ railSidebar();
642
+ return;
643
+ }
644
+ setSidebarMode("open");
645
+ const next = clamp(raw, pane.minSize ?? 200, pane.maxSize ?? 480);
646
+ setSidebarSize(next);
647
+ writeStoredPaneSize(props.storageKey, "sidebar", next);
648
+ });
649
+ };
650
+
651
+ const resizeRight = (event: PointerEvent) => {
652
+ const pane = right();
653
+ if (!pane?.resizable) return;
654
+ const start = rightOpen() ? rightSize() : rightColumnSize();
655
+ document.body.style.cursor = "col-resize";
656
+ document.body.style.userSelect = "none";
657
+ startDesktopResize(event, (dx) => {
658
+ const raw = start - dx;
659
+ if (shouldRail(pane, raw)) {
660
+ railRight();
661
+ return;
662
+ }
663
+ setRightMode("open");
664
+ const next = clamp(raw, pane.minSize ?? 240, pane.maxSize ?? 560);
665
+ setRightSize(next);
666
+ writeStoredPaneSize(props.storageKey, "right", next);
667
+ });
668
+ };
669
+
670
+ const resizeBottom = (event: PointerEvent) => {
671
+ const pane = bottom();
672
+ if (!pane?.resizable) return;
673
+ const start = bottomOpen() ? bottomSize() : bottomRowSize();
674
+ document.body.style.cursor = "row-resize";
675
+ document.body.style.userSelect = "none";
676
+ startDesktopResize(event, (_dx, dy) => {
677
+ const raw = start - dy;
678
+ if (shouldRail(pane, raw)) {
679
+ railBottom();
680
+ return;
681
+ }
682
+ setBottomMode("open");
683
+ const next = clamp(raw, pane.minSize ?? 120, pane.maxSize ?? 420);
684
+ setBottomSize(next);
685
+ writeStoredPaneSize(props.storageKey, "bottom", next);
686
+ });
687
+ };
688
+
689
+ return (
690
+ <div
691
+ class={`desktop-workspace relative grid h-full min-h-0 gap-x-2 overflow-hidden bg-zinc-100 dark:bg-zinc-950 ${props.class ?? ""}`}
692
+ style={gridStyle()}
693
+ >
694
+ <Show when={sidebar()}>
695
+ {(slot) => (
696
+ <aside
697
+ class={`${sidebarOpen() ? "flex" : "hidden"} desktop-workspace-sidebar row-span-3 min-h-0 min-w-0 flex-col overflow-hidden bg-white dark:bg-zinc-950 ${slot().class ?? ""}`}
698
+ style="grid-column:1;grid-row:1 / 4"
699
+ >
700
+ <Show when={slot().trafficLightsInset ?? true}>
701
+ <DesktopWorkspace.DragRegion class="flex h-12 shrink-0 items-center pl-24 pr-3">
702
+ <Show when={slot().title}>
703
+ <p class="min-w-0 truncate text-sm font-semibold text-primary">{slot().title}</p>
704
+ </Show>
705
+ </DesktopWorkspace.DragRegion>
706
+ </Show>
707
+ {slot().children}
708
+ </aside>
709
+ )}
710
+ </Show>
711
+
712
+ <Show when={sidebarRailOpen() ? sidebarRail() : undefined}>
713
+ {(slot) => (
714
+ <aside
715
+ class={`desktop-workspace-sidebar-rail min-h-0 min-w-0 overflow-hidden bg-white dark:bg-zinc-950 ${slot().class ?? ""}`}
716
+ style="grid-column:1;grid-row:2 / 4"
717
+ >
718
+ {slot().children}
719
+ </aside>
720
+ )}
721
+ </Show>
722
+
723
+ <Show when={topBar()}>
724
+ {(slot) => (
725
+ <header
726
+ class={`desktop-workspace-topbar mr-2 min-w-0 ${slot().class ?? ""}`}
727
+ style={`grid-column:${sidebarOpen() ? "2 / 4" : "1 / 4"};grid-row:1;padding-left:${topBarTrafficLightInset() ? "76px" : "0px"};${
728
+ slot().drag ? "-webkit-app-region:drag" : ""
729
+ }`}
730
+ >
731
+ {slot().children}
732
+ </header>
733
+ )}
734
+ </Show>
735
+
736
+ <main
737
+ class={`desktop-workspace-main min-h-0 min-w-0 overflow-hidden ${topBar() ? "" : "mt-2"} ${bottomOpen() ? "" : "mb-2"} ${main()?.class ?? ""}`}
738
+ style={`grid-column:${mainGridColumn()};grid-row:2`}
739
+ >
740
+ {main()?.children}
741
+ </main>
742
+
743
+ <Show when={rightOpen() ? right() : undefined}>
744
+ {(slot) => (
745
+ <aside
746
+ class={`desktop-workspace-right mr-2 min-h-0 min-w-0 overflow-hidden ${topBar() ? "" : "mt-2"} ${bottomOpen() ? "" : "mb-2"} ${
747
+ slot().class ?? ""
748
+ }`}
749
+ style="grid-column:3;grid-row:2"
750
+ >
751
+ {slot().children}
752
+ </aside>
753
+ )}
754
+ </Show>
755
+
756
+ <Show when={rightRailOpen() ? rightRail() : undefined}>
757
+ {(slot) => (
758
+ <aside
759
+ class={`desktop-workspace-right-rail min-h-0 min-w-0 overflow-hidden ${topBar() ? "" : "mt-2"} ${bottomOpen() ? "" : "mb-2"} ${
760
+ slot().class ?? ""
761
+ }`}
762
+ style="grid-column:3;grid-row:2"
763
+ >
764
+ {slot().children}
765
+ </aside>
766
+ )}
767
+ </Show>
768
+
769
+ <Show when={bottomOpen() ? bottom() : undefined}>
770
+ {(slot) => (
771
+ <section
772
+ class={`desktop-workspace-bottom mt-2 mr-2 mb-2 min-h-0 min-w-0 overflow-hidden ${slot().class ?? ""}`}
773
+ style={`grid-column:${bottomGridColumn()};grid-row:3`}
774
+ >
775
+ {slot().children}
776
+ </section>
777
+ )}
778
+ </Show>
779
+
780
+ <Show when={bottomRailOpen() ? bottomRail() : undefined}>
781
+ {(slot) => (
782
+ <section
783
+ class={`desktop-workspace-bottom-rail min-h-0 min-w-0 overflow-hidden ${slot().class ?? ""}`}
784
+ style={`grid-column:${bottomGridColumn()};grid-row:3`}
785
+ >
786
+ {slot().children}
787
+ </section>
788
+ )}
789
+ </Show>
790
+
791
+ <Show when={sidebar()?.resizable && (sidebarOpen() || sidebarRailOpen())}>
792
+ <div
793
+ class={`${desktopResizeHandleClass} top-0 bottom-0 w-2 cursor-col-resize ${sidebarRailOpen() ? "rounded-tr-full" : ""}`}
794
+ style={{
795
+ left: `${sidebarColumnSize()}px`,
796
+ top: sidebarRailOpen() ? `${topBar() ? topBarHeight() : 0}px` : "0px",
797
+ }}
798
+ onPointerDown={resizeSidebar}
799
+ />
800
+ </Show>
801
+ <Show when={sidebarMode() === "hidden"}>
802
+ <button
803
+ type="button"
804
+ aria-label="Show sidebar"
805
+ title="Show sidebar"
806
+ class={`${desktopRestoreHandleClass} top-0 bottom-0 left-0 w-3`}
807
+ style="cursor:e-resize"
808
+ onClick={restoreSidebar}
809
+ />
810
+ </Show>
811
+ <Show when={right()?.resizable && (rightOpen() || rightRailOpen())}>
812
+ <div
813
+ class={`${desktopResizeHandleClass} w-2 rounded-full cursor-col-resize`}
814
+ style={{
815
+ right: `${rightColumnSize()}px`,
816
+ top: `${topBar() ? topBarHeight() : 0}px`,
817
+ bottom: `${bottomRowSize()}px`,
818
+ }}
819
+ onPointerDown={resizeRight}
820
+ />
821
+ </Show>
822
+ <Show when={rightMode() === "hidden" && Boolean(right())}>
823
+ <button
824
+ type="button"
825
+ aria-label="Show right panel"
826
+ title="Show right panel"
827
+ class={`${desktopRestoreHandleClass} w-3 rounded-full`}
828
+ style={{
829
+ right: "0px",
830
+ top: `${topBar() ? topBarHeight() : 0}px`,
831
+ bottom: `${bottomRowSize()}px`,
832
+ cursor: "w-resize",
833
+ }}
834
+ onClick={restoreRight}
835
+ />
836
+ </Show>
837
+ <Show when={bottom()?.resizable && (bottomOpen() || bottomRailOpen())}>
838
+ <div
839
+ class={`${desktopResizeHandleClass} h-2 rounded-full cursor-row-resize`}
840
+ style={{
841
+ left: `${sidebarColumnSize() > 0 ? sidebarColumnSize() + desktopWorkspaceGap : 0}px`,
842
+ right: "0px",
843
+ bottom: `${bottomOpen() ? bottomSize() - desktopWorkspaceGap : bottomRowSize()}px`,
844
+ }}
845
+ onPointerDown={resizeBottom}
846
+ />
847
+ </Show>
848
+ <Show when={bottomMode() === "hidden" && Boolean(bottom())}>
849
+ <button
850
+ type="button"
851
+ aria-label="Show bottom panel"
852
+ title="Show bottom panel"
853
+ class={`${desktopRestoreHandleClass} h-3 rounded-full`}
854
+ style={{
855
+ left: `${sidebarColumnSize() > 0 ? sidebarColumnSize() + desktopWorkspaceGap : 0}px`,
856
+ right: "0px",
857
+ bottom: "0px",
858
+ cursor: "n-resize",
859
+ }}
860
+ onClick={restoreBottom}
861
+ />
862
+ </Show>
863
+ </div>
864
+ );
865
+ }) as DesktopWorkspaceComponent;
866
+
867
+ DesktopWorkspace.Sidebar = (props: DesktopWorkspaceSidebarProps): JSX.Element =>
868
+ ({ kind: DESKTOP_WORKSPACE_SIDEBAR, ...props }) satisfies DesktopWorkspaceSidebarSlot as unknown as JSX.Element;
869
+
870
+ DesktopWorkspace.TopBar = (props: DesktopWorkspaceTopBarProps): JSX.Element =>
871
+ ({ kind: DESKTOP_WORKSPACE_TOPBAR, ...props }) satisfies DesktopWorkspaceTopBarSlot as unknown as JSX.Element;
872
+
873
+ DesktopWorkspace.Main = (props: DesktopWorkspacePaneProps): JSX.Element =>
874
+ ({ kind: DESKTOP_WORKSPACE_MAIN, ...props }) satisfies DesktopWorkspaceMainSlot as unknown as JSX.Element;
875
+
876
+ DesktopWorkspace.Right = (props: DesktopWorkspaceResizablePaneProps): JSX.Element =>
877
+ ({ kind: DESKTOP_WORKSPACE_RIGHT, ...props }) satisfies DesktopWorkspaceRightSlot as unknown as JSX.Element;
878
+
879
+ DesktopWorkspace.Bottom = (props: DesktopWorkspaceResizablePaneProps): JSX.Element =>
880
+ ({ kind: DESKTOP_WORKSPACE_BOTTOM, ...props }) satisfies DesktopWorkspaceBottomSlot as unknown as JSX.Element;
881
+
882
+ DesktopWorkspace.SidebarRail = (props: DesktopWorkspaceRailProps): JSX.Element =>
883
+ ({ kind: DESKTOP_WORKSPACE_SIDEBAR_RAIL, ...props }) satisfies DesktopWorkspaceSidebarRailSlot as unknown as JSX.Element;
884
+
885
+ DesktopWorkspace.RightRail = (props: DesktopWorkspaceRailProps): JSX.Element =>
886
+ ({ kind: DESKTOP_WORKSPACE_RIGHT_RAIL, ...props }) satisfies DesktopWorkspaceRightRailSlot as unknown as JSX.Element;
887
+
888
+ DesktopWorkspace.BottomRail = (props: DesktopWorkspaceRailProps): JSX.Element =>
889
+ ({ kind: DESKTOP_WORKSPACE_BOTTOM_RAIL, ...props }) satisfies DesktopWorkspaceBottomRailSlot as unknown as JSX.Element;
890
+
891
+ DesktopWorkspace.DragRegion = (props: ParentProps<{ class?: string }>) => (
892
+ <div class={props.class} style="-webkit-app-region:drag">
893
+ {props.children}
894
+ </div>
895
+ );
896
+
897
+ DesktopWorkspace.NoDrag = (props: ParentProps<{ class?: string }>) => (
898
+ <div class={props.class} style="-webkit-app-region:no-drag">
899
+ {props.children}
900
+ </div>
901
+ );
902
+
903
+ export function TitleBar(props: ParentProps<{ title?: string; class?: string }>) {
904
+ return (
905
+ <div class={`flex min-h-10 items-center gap-2 border-b border-zinc-200 px-3 dark:border-zinc-800 ${props.class ?? ""}`}>
906
+ <WindowControls />
907
+ <Show when={props.title}>
908
+ <p class="min-w-0 flex-1 truncate text-sm font-medium text-primary">{props.title}</p>
909
+ </Show>
910
+ {props.children}
911
+ </div>
912
+ );
913
+ }
914
+
915
+ export function WindowControls(props: { class?: string }) {
916
+ const env = () => desktop.env as DesktopEnvironment;
917
+ return (
918
+ <div class={`flex items-center gap-2 ${props.class ?? ""}`}>
919
+ <For
920
+ each={[
921
+ { label: "Close", class: "bg-red-500", action: () => desktop.window.close() },
922
+ { label: "Minimize", class: "bg-amber-400", action: () => desktop.window.minimize() },
923
+ { label: "Maximize", class: "bg-emerald-500", action: () => desktop.window.maximize() },
924
+ ]}
925
+ >
926
+ {(item) => (
927
+ <button
928
+ type="button"
929
+ aria-label={item.label}
930
+ title={item.label}
931
+ class={`h-3 w-3 rounded-full ${item.class} ${env().runtime === "browser" ? "opacity-50" : ""}`}
932
+ onClick={() => void item.action().catch(() => undefined)}
933
+ />
934
+ )}
935
+ </For>
936
+ </div>
937
+ );
938
+ }