@valentinkolb/cloud 0.3.1 → 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.
- package/package.json +18 -8
- package/scripts/preload.ts +78 -23
- package/src/_internal/define-app.ts +119 -47
- package/src/_internal/runtime-context.ts +1 -0
- package/src/api/accounts-entities.ts +4 -0
- package/src/api/admin-core-settings.ts +98 -0
- package/src/api/announcements.ts +131 -0
- package/src/api/auth/schemas.ts +24 -0
- package/src/api/auth.ts +113 -10
- package/src/api/index.ts +15 -25
- package/src/api/me.ts +203 -14
- package/src/api/search/schemas.ts +1 -0
- package/src/api/search.ts +62 -8
- package/src/config/ssr.ts +2 -9
- package/src/contracts/announcements.test.ts +37 -0
- package/src/contracts/announcements.ts +121 -0
- package/src/contracts/app.ts +4 -0
- package/src/contracts/index.ts +3 -2
- package/src/contracts/registry.ts +4 -0
- package/src/contracts/shared.ts +108 -1
- package/src/desktop/index.ts +704 -0
- package/src/desktop/solid.tsx +938 -0
- package/src/server/api/index.ts +1 -1
- package/src/server/api/respond.ts +50 -10
- package/src/server/index.ts +44 -38
- package/src/server/middleware/auth.ts +98 -9
- package/src/server/middleware/index.ts +2 -1
- package/src/server/middleware/settings.ts +26 -0
- package/src/server/services/access.test.ts +197 -0
- package/src/server/services/access.ts +254 -6
- package/src/server/services/index.ts +14 -11
- package/src/server/services/pagination.ts +22 -0
- package/src/server/time.ts +45 -0
- package/src/services/account-lifecycle/index.ts +142 -18
- package/src/services/accounts/app.ts +658 -170
- package/src/services/accounts/authz.test.ts +77 -0
- package/src/services/accounts/authz.ts +22 -0
- package/src/services/accounts/entities.ts +84 -5
- package/src/services/accounts/groups.ts +30 -24
- package/src/services/accounts/model.test.ts +30 -0
- package/src/services/accounts/switching.test.ts +14 -0
- package/src/services/accounts/switching.ts +15 -6
- package/src/services/accounts/users.ts +75 -52
- package/src/services/announcements/index.test.ts +32 -0
- package/src/services/announcements/index.ts +224 -0
- package/src/services/audit/index.test.ts +84 -0
- package/src/services/audit/index.ts +431 -0
- package/src/services/auth-flows/index.ts +9 -2
- package/src/services/auth-flows/ipa.ts +0 -2
- package/src/services/auth-flows/magic-link.ts +3 -2
- package/src/services/auth-flows/password-reset.ts +284 -0
- package/src/services/auth-flows/proxy-return.test.ts +24 -0
- package/src/services/auth-flows/proxy-return.ts +49 -0
- package/src/services/gateway.ts +162 -0
- package/src/services/index.ts +44 -2
- package/src/services/ipa/effective-groups.test.ts +33 -0
- package/src/services/ipa/effective-groups.ts +70 -0
- package/src/services/ipa/profile.ts +45 -3
- package/src/services/ipa/search.ts +3 -5
- package/src/services/ipa/service-account.ts +15 -0
- package/src/services/ipa/sync-planning.test.ts +32 -0
- package/src/services/ipa/sync-planning.ts +22 -0
- package/src/services/ipa/sync.ts +110 -38
- package/src/services/oauth-tokens.ts +104 -0
- package/src/services/postgres.ts +21 -6
- package/src/services/providers/local/auth.test.ts +22 -0
- package/src/services/providers/local/auth.ts +46 -3
- package/src/services/secrets.ts +10 -0
- package/src/services/service-account-credentials.test.ts +210 -0
- package/src/services/service-account-credentials.ts +715 -0
- package/src/services/service-accounts.ts +188 -0
- package/src/services/session/index.ts +7 -8
- package/src/services/settings/app.ts +4 -20
- package/src/services/settings/defaults.ts +64 -22
- package/src/services/settings/store.ts +47 -0
- package/src/services/weather/forecast.ts +40 -7
- package/src/services/webauthn.test.ts +36 -0
- package/src/services/webauthn.ts +384 -0
- package/src/shared/icons.ts +391 -100
- package/src/shared/index.ts +7 -0
- package/src/shared/markdown/extensions/code.ts +38 -1
- package/src/shared/markdown/extensions/images.ts +39 -3
- package/src/shared/markdown/extensions/info-blocks.ts +5 -5
- package/src/shared/markdown/extensions/mark.ts +48 -0
- package/src/shared/markdown/extensions/sub-sup.ts +60 -0
- package/src/shared/markdown/extensions/tables.ts +79 -58
- package/src/shared/markdown/formula.test.ts +1089 -0
- package/src/shared/markdown/formula.ts +1187 -0
- package/src/shared/markdown/index.ts +76 -2
- package/src/shared/mock-cover.ts +130 -0
- package/src/shared/redirect.test.ts +49 -0
- package/src/shared/redirect.ts +52 -0
- package/src/shared/theme.test.ts +24 -0
- package/src/shared/theme.ts +68 -0
- package/src/shared/time.ts +13 -0
- package/src/ssr/AdminLayout.tsx +7 -3
- package/src/ssr/AdminSidebar.tsx +115 -49
- package/src/ssr/AppLaunchpad.island.tsx +176 -0
- package/src/ssr/Footer.island.tsx +3 -8
- package/src/ssr/GlobalAnnouncements.island.tsx +141 -0
- package/src/ssr/GlobalSearchDialog.tsx +545 -117
- package/src/ssr/HotkeysHelpRail.island.tsx +3 -70
- package/src/ssr/Layout.tsx +74 -66
- package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
- package/src/ssr/LayoutHelp.tsx +266 -0
- package/src/ssr/NavMenu.island.tsx +0 -39
- package/src/ssr/ThemeToggleRail.island.tsx +3 -3
- package/src/ssr/TimezoneCookie.island.tsx +23 -0
- package/src/ssr/islands/index.ts +13 -0
- package/src/styles/base-popover.css +5 -2
- package/src/styles/effects.css +87 -6
- package/src/styles/global.css +146 -9
- package/src/styles/input.css +3 -1
- package/src/styles/utilities-buttons.css +133 -27
- package/src/styles/utilities-code-display.css +67 -0
- package/src/styles/utilities-completion.css +223 -0
- package/src/styles/utilities-detail.css +73 -0
- package/src/styles/utilities-feedback.css +16 -15
- package/src/styles/utilities-layout.css +42 -2
- package/src/styles/utilities-markdown-editor.css +472 -0
- package/src/styles/utilities-navigation.css +63 -8
- package/src/styles/utilities-script.css +84 -0
- package/src/styles/utilities-table-tile.css +229 -0
- package/src/types/ambient.d.ts +9 -0
- package/src/ui/completion/behaviors.test.ts +95 -0
- package/src/ui/completion/behaviors.ts +205 -0
- package/src/ui/completion/engine.ts +368 -0
- package/src/ui/completion/index.ts +40 -0
- package/src/ui/completion/overlay.ts +92 -0
- package/src/ui/dialog-core.ts +173 -45
- package/src/ui/filter/FilterChip.tsx +42 -40
- package/src/ui/index.ts +11 -12
- package/src/ui/input/AutocompleteEditor.tsx +656 -0
- package/src/ui/input/CheckboxCard.tsx +91 -0
- package/src/ui/input/Combobox.tsx +375 -0
- package/src/ui/input/DatePicker.tsx +846 -0
- package/src/ui/input/DateTimeInput.tsx +29 -4
- package/src/ui/input/FileDropzone.tsx +116 -0
- package/src/ui/input/IconInput.tsx +116 -0
- package/src/ui/input/ImageInput.tsx +19 -2
- package/src/ui/input/MultiSelectInput.tsx +448 -0
- package/src/ui/input/NumberInput.tsx +417 -61
- package/src/ui/input/SegmentedControl.tsx +2 -2
- package/src/ui/input/Select.tsx +172 -10
- package/src/ui/input/Slider.tsx +3 -4
- package/src/ui/input/Switch.tsx +3 -2
- package/src/ui/input/TemplateEditor.tsx +212 -0
- package/src/ui/input/TextInput.tsx +144 -13
- package/src/ui/input/index.ts +53 -8
- package/src/ui/input/markdown/MarkdownEditor.tsx +774 -0
- package/src/ui/input/markdown/Toolbar.tsx +90 -0
- package/src/ui/input/markdown/actions.ts +233 -0
- package/src/ui/input/markdown/active-formats.ts +94 -0
- package/src/ui/input/markdown/behaviors.ts +193 -0
- package/src/ui/input/markdown/code-zone.ts +23 -0
- package/src/ui/input/markdown/highlight.ts +316 -0
- package/src/ui/layout.ts +22 -0
- package/src/ui/misc/AppOverview.tsx +105 -0
- package/src/ui/misc/AppWorkspace.tsx +607 -0
- package/src/ui/misc/Calendar.tsx +1291 -0
- package/src/ui/misc/Chart.tsx +162 -0
- package/src/ui/misc/CodeDisplay.tsx +54 -0
- package/src/ui/misc/ContextMenu.tsx +2 -2
- package/src/ui/misc/DataTable.tsx +269 -0
- package/src/ui/misc/DockWorkspace.tsx +425 -0
- package/src/ui/misc/Docs.tsx +153 -0
- package/src/ui/misc/Dropdown.tsx +2 -2
- package/src/ui/misc/EntitySearch.tsx +260 -129
- package/src/ui/misc/LinkCard.tsx +14 -2
- package/src/ui/misc/LogEntriesTable.tsx +34 -31
- package/src/ui/misc/Pagination.tsx +31 -12
- package/src/ui/misc/PanelDialog.tsx +109 -0
- package/src/ui/misc/Panes.tsx +873 -0
- package/src/ui/misc/PermissionEditor.tsx +358 -262
- package/src/ui/misc/Placeholder.tsx +40 -0
- package/src/ui/misc/ProgressBar.tsx +1 -1
- package/src/ui/misc/ResourceApiKeys.tsx +260 -0
- package/src/ui/misc/SettingsModal.tsx +150 -0
- package/src/ui/misc/StatCell.tsx +182 -40
- package/src/ui/misc/StatGrid.tsx +149 -0
- package/src/ui/misc/StructuredDataPreview.tsx +107 -0
- package/src/ui/misc/code-highlight.ts +213 -0
- package/src/ui/misc/index.ts +93 -12
- package/src/ui/prompts.tsx +362 -312
- package/src/ui/toast.ts +384 -0
- package/src/ui/widgets/Widget.tsx +12 -4
- package/src/ssr/MoreAppsDropdown.island.tsx +0 -61
- package/src/ui/ipa/GroupView.tsx +0 -36
- package/src/ui/ipa/LoginBtn.tsx +0 -16
- package/src/ui/ipa/UserView.tsx +0 -58
- package/src/ui/ipa/index.ts +0 -4
- package/src/ui/navigation.ts +0 -32
- package/src/ui/sidebar.tsx +0 -468
- /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
|
+
}
|