@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,425 @@
1
+ import { children, createEffect, createMemo, createSignal, For, onMount, Show, type JSX } from "solid-js";
2
+
3
+ const RESULT_SLOT = Symbol("DockWorkspace.Result");
4
+ const PANE_SLOT = Symbol("DockWorkspace.Pane");
5
+
6
+ const DEFAULT_RESULT_SIZE = 58;
7
+ const MIN_RESULT_SIZE = 22;
8
+ const MAX_RESULT_SIZE = 82;
9
+ const MIN_SECTION_SIZE = 12;
10
+
11
+ type ResultSlot = {
12
+ readonly kind: typeof RESULT_SLOT;
13
+ title?: string;
14
+ icon?: string;
15
+ children: JSX.Element;
16
+ };
17
+
18
+ type PaneSlot = {
19
+ readonly kind: typeof PANE_SLOT;
20
+ id: string;
21
+ title: string;
22
+ icon?: string;
23
+ section: string;
24
+ children: JSX.Element;
25
+ };
26
+
27
+ type DockSlot = ResultSlot | PaneSlot;
28
+
29
+ export type DockWorkspaceSectionState = {
30
+ id: string;
31
+ size: number;
32
+ paneIds: string[];
33
+ activePaneId: string;
34
+ };
35
+
36
+ export type DockWorkspaceState = {
37
+ resultSize: number;
38
+ sections: DockWorkspaceSectionState[];
39
+ };
40
+
41
+ export type DockWorkspacePaneDescriptor = {
42
+ id: string;
43
+ section?: string | null;
44
+ };
45
+
46
+ export type DockWorkspaceProps = {
47
+ storageKey?: string;
48
+ initialState?: DockWorkspaceState | null;
49
+ defaultResultSize?: number;
50
+ class?: string;
51
+ children: JSX.Element;
52
+ };
53
+
54
+ export type DockWorkspaceResultProps = {
55
+ title?: string;
56
+ icon?: string;
57
+ children: JSX.Element;
58
+ };
59
+
60
+ export type DockWorkspacePaneProps = {
61
+ id: string;
62
+ title: string;
63
+ icon?: string;
64
+ section?: string;
65
+ children: JSX.Element;
66
+ };
67
+
68
+ type DockWorkspaceComponent = ((props: DockWorkspaceProps) => JSX.Element) & {
69
+ Result: (props: DockWorkspaceResultProps) => JSX.Element;
70
+ Pane: (props: DockWorkspacePaneProps) => JSX.Element;
71
+ };
72
+
73
+ const isDockSlot = (value: unknown): value is DockSlot => !!value && typeof value === "object" && "kind" in value;
74
+
75
+ const collectDockSlots = (value: unknown): DockSlot[] => {
76
+ if (Array.isArray(value)) return value.flatMap(collectDockSlots);
77
+ return isDockSlot(value) ? [value] : [];
78
+ };
79
+
80
+ const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
81
+
82
+ const normalizeSizes = <T extends { size: number }>(items: T[]): T[] => {
83
+ if (items.length === 0) return items;
84
+ const sanitized = items.map((item) => ({ ...item, size: Number.isFinite(item.size) ? Math.max(0, item.size) : 0 }));
85
+ const total = sanitized.reduce((sum, item) => sum + item.size, 0);
86
+ if (total <= 0) return sanitized.map((item) => ({ ...item, size: 100 / sanitized.length }));
87
+ return sanitized.map((item) => ({ ...item, size: (item.size / total) * 100 }));
88
+ };
89
+
90
+ const defaultState = (panes: DockWorkspacePaneDescriptor[], defaultResultSize: number): DockWorkspaceState => {
91
+ const sections = new Map<string, DockWorkspacePaneDescriptor[]>();
92
+ for (const pane of panes) {
93
+ const key = pane.section?.trim() || "main";
94
+ sections.set(key, [...(sections.get(key) ?? []), pane]);
95
+ }
96
+ const entries = [...sections.entries()];
97
+ return {
98
+ resultSize: clamp(defaultResultSize, MIN_RESULT_SIZE, MAX_RESULT_SIZE),
99
+ sections: entries.map(([id, items]) => ({
100
+ id,
101
+ size: entries.length > 0 ? 100 / entries.length : 100,
102
+ paneIds: items.map((pane) => pane.id),
103
+ activePaneId: items[0]?.id ?? "",
104
+ })),
105
+ };
106
+ };
107
+
108
+ export const normalizeDockWorkspaceState = (
109
+ state: DockWorkspaceState | null | undefined,
110
+ panes: DockWorkspacePaneDescriptor[],
111
+ defaultResultSize = DEFAULT_RESULT_SIZE,
112
+ ): DockWorkspaceState => {
113
+ const fallback = defaultState(panes, defaultResultSize);
114
+ if (!state || !Array.isArray(state.sections)) return fallback;
115
+
116
+ const paneById = new Map(panes.map((pane) => [pane.id, pane]));
117
+ const used = new Set<string>();
118
+ const sections: DockWorkspaceSectionState[] = [];
119
+
120
+ for (const section of state.sections) {
121
+ if (!section || typeof section.id !== "string" || !Array.isArray(section.paneIds)) continue;
122
+ const paneIds = section.paneIds.filter((id) => paneById.has(id) && !used.has(id));
123
+ for (const id of paneIds) used.add(id);
124
+ if (paneIds.length === 0) continue;
125
+ sections.push({
126
+ id: section.id,
127
+ size: Number.isFinite(section.size) ? section.size : 0,
128
+ paneIds,
129
+ activePaneId: paneIds.includes(section.activePaneId) ? section.activePaneId : paneIds[0]!,
130
+ });
131
+ }
132
+
133
+ for (const pane of panes) {
134
+ if (used.has(pane.id)) continue;
135
+ const sectionId = pane.section?.trim() || "main";
136
+ const target = sections.find((section) => section.id === sectionId);
137
+ if (target) {
138
+ target.paneIds.push(pane.id);
139
+ if (!target.activePaneId) target.activePaneId = pane.id;
140
+ } else {
141
+ sections.push({ id: sectionId, size: MIN_SECTION_SIZE, paneIds: [pane.id], activePaneId: pane.id });
142
+ }
143
+ }
144
+
145
+ if (sections.length === 0) return fallback;
146
+ return {
147
+ resultSize: clamp(state.resultSize, MIN_RESULT_SIZE, MAX_RESULT_SIZE),
148
+ sections: normalizeSizes(sections),
149
+ };
150
+ };
151
+
152
+ const dockCookieName = (storageKey: string) => `dock_${storageKey.replace(/[^A-Za-z0-9_-]/g, "_")}`;
153
+
154
+ export const readDockWorkspaceStateCookie = (cookieHeader: string | null | undefined, storageKey: string): DockWorkspaceState | null => {
155
+ if (!cookieHeader) return null;
156
+ const name = dockCookieName(storageKey);
157
+ const encoded = cookieHeader
158
+ .split(";")
159
+ .map((part) => part.trim())
160
+ .find((part) => part.startsWith(`${name}=`))
161
+ ?.slice(name.length + 1);
162
+ if (!encoded) return null;
163
+ try {
164
+ return JSON.parse(decodeURIComponent(encoded)) as DockWorkspaceState;
165
+ } catch {
166
+ return null;
167
+ }
168
+ };
169
+
170
+ const readClientStateCookie = (storageKey: string): DockWorkspaceState | null =>
171
+ typeof document === "undefined" ? null : readDockWorkspaceStateCookie(document.cookie, storageKey);
172
+
173
+ const writeClientStateCookie = (storageKey: string, state: DockWorkspaceState) => {
174
+ if (typeof document === "undefined") return;
175
+ const encoded = encodeURIComponent(JSON.stringify(state));
176
+ document.cookie = `${dockCookieName(storageKey)}=${encoded}; Path=/; Max-Age=31536000; SameSite=Lax`;
177
+ };
178
+
179
+ const iconClass = (icon: string | undefined, fallback: string) => {
180
+ const value = icon?.trim() || fallback;
181
+ return value.startsWith("ti ") ? value : `ti ${value}`;
182
+ };
183
+
184
+ function DockWorkspaceResult(props: DockWorkspaceResultProps): JSX.Element {
185
+ return {
186
+ kind: RESULT_SLOT,
187
+ title: props.title,
188
+ icon: props.icon,
189
+ children: props.children,
190
+ } satisfies ResultSlot as unknown as JSX.Element;
191
+ }
192
+
193
+ function DockWorkspacePane(props: DockWorkspacePaneProps): JSX.Element {
194
+ return {
195
+ kind: PANE_SLOT,
196
+ id: props.id,
197
+ title: props.title,
198
+ icon: props.icon,
199
+ section: props.section ?? "main",
200
+ children: props.children,
201
+ } satisfies PaneSlot as unknown as JSX.Element;
202
+ }
203
+
204
+ const DockWorkspaceRoot = (props: DockWorkspaceProps) => {
205
+ let rootEl: HTMLDivElement | undefined;
206
+ let bottomEl: HTMLDivElement | undefined;
207
+ const [isMounted, setIsMounted] = createSignal(false);
208
+ const resolved = children(() => props.children);
209
+ const slots = createMemo(() => collectDockSlots(resolved.toArray()));
210
+ const result = createMemo(() => slots().find((slot): slot is ResultSlot => slot.kind === RESULT_SLOT) ?? null);
211
+ const panes = createMemo(() => slots().filter((slot): slot is PaneSlot => slot.kind === PANE_SLOT));
212
+ const paneById = createMemo(() => new Map(panes().map((pane) => [pane.id, pane])));
213
+ const [state, setState] = createSignal<DockWorkspaceState>(
214
+ normalizeDockWorkspaceState(props.initialState, panes(), props.defaultResultSize ?? DEFAULT_RESULT_SIZE),
215
+ );
216
+
217
+ createEffect(() => {
218
+ setState((current) => normalizeDockWorkspaceState(current, panes(), props.defaultResultSize ?? DEFAULT_RESULT_SIZE));
219
+ });
220
+
221
+ onMount(() => {
222
+ const clientState = props.storageKey ? readClientStateCookie(props.storageKey) : null;
223
+ if (clientState) {
224
+ setState(normalizeDockWorkspaceState(clientState, panes(), props.defaultResultSize ?? DEFAULT_RESULT_SIZE));
225
+ }
226
+ setIsMounted(true);
227
+ });
228
+
229
+ createEffect(() => {
230
+ if (!isMounted() || !props.storageKey) return;
231
+ writeClientStateCookie(props.storageKey, state());
232
+ });
233
+
234
+ const setActivePane = (sectionId: string, paneId: string) => {
235
+ setState((current) => ({
236
+ ...current,
237
+ sections: current.sections.map((section) =>
238
+ section.id === sectionId && section.paneIds.includes(paneId) ? { ...section, activePaneId: paneId } : section,
239
+ ),
240
+ }));
241
+ };
242
+
243
+ const movePane = (paneId: string, targetSectionId: string, beforePaneId?: string) => {
244
+ if (!paneById().has(paneId)) return;
245
+ setState((current) => {
246
+ const next = current.sections
247
+ .map((section) => {
248
+ const paneIds = section.paneIds.filter((id) => id !== paneId);
249
+ return {
250
+ ...section,
251
+ paneIds,
252
+ activePaneId: paneIds.includes(section.activePaneId) ? section.activePaneId : (paneIds[0] ?? ""),
253
+ };
254
+ })
255
+ .filter((section) => section.paneIds.length > 0 || section.id === targetSectionId);
256
+
257
+ let target = next.find((section) => section.id === targetSectionId);
258
+ if (!target) {
259
+ target = { id: targetSectionId, size: 0, paneIds: [], activePaneId: paneId };
260
+ next.push(target);
261
+ }
262
+
263
+ const insertAt = beforePaneId ? target.paneIds.indexOf(beforePaneId) : -1;
264
+ const targetIds = [...target.paneIds];
265
+ if (insertAt >= 0) targetIds.splice(insertAt, 0, paneId);
266
+ else targetIds.push(paneId);
267
+
268
+ const sections = next.map((section) =>
269
+ section.id === targetSectionId ? { ...section, paneIds: targetIds, activePaneId: paneId } : section,
270
+ );
271
+ return { ...current, sections: normalizeSizes(sections) };
272
+ });
273
+ };
274
+
275
+ const dragPaneId = (event: DragEvent): string | null => event.dataTransfer?.getData("application/x-dock-pane-id") || null;
276
+
277
+ const handleVerticalResize = (event: PointerEvent) => {
278
+ event.preventDefault();
279
+ const startY = event.clientY;
280
+ const startSize = state().resultSize;
281
+ const height = rootEl?.getBoundingClientRect().height ?? 1;
282
+ const onMove = (move: PointerEvent) => {
283
+ const delta = ((move.clientY - startY) / height) * 100;
284
+ setState((current) => ({ ...current, resultSize: clamp(startSize + delta, MIN_RESULT_SIZE, MAX_RESULT_SIZE) }));
285
+ };
286
+ const onUp = () => {
287
+ window.removeEventListener("pointermove", onMove);
288
+ window.removeEventListener("pointerup", onUp);
289
+ };
290
+ window.addEventListener("pointermove", onMove);
291
+ window.addEventListener("pointerup", onUp);
292
+ };
293
+
294
+ const handleSectionResize = (event: PointerEvent, index: number) => {
295
+ event.preventDefault();
296
+ const startX = event.clientX;
297
+ const startSections = state().sections;
298
+ const width = bottomEl?.getBoundingClientRect().width ?? 1;
299
+ const onMove = (move: PointerEvent) => {
300
+ const delta = ((move.clientX - startX) / width) * 100;
301
+ setState((current) => {
302
+ const sections = current.sections.map((section, sectionIndex) => {
303
+ const start = startSections[sectionIndex];
304
+ if (!start) return section;
305
+ if (sectionIndex === index) return { ...section, size: clamp(start.size + delta, MIN_SECTION_SIZE, 100) };
306
+ if (sectionIndex === index + 1) return { ...section, size: clamp(start.size - delta, MIN_SECTION_SIZE, 100) };
307
+ return section;
308
+ });
309
+ return { ...current, sections: normalizeSizes(sections) };
310
+ });
311
+ };
312
+ const onUp = () => {
313
+ window.removeEventListener("pointermove", onMove);
314
+ window.removeEventListener("pointerup", onUp);
315
+ };
316
+ window.addEventListener("pointermove", onMove);
317
+ window.addEventListener("pointerup", onUp);
318
+ };
319
+
320
+ return (
321
+ <div ref={rootEl} class={`flex h-full min-h-0 min-w-0 flex-col overflow-hidden bg-transparent ${props.class ?? ""}`}>
322
+ <Show
323
+ when={result()}
324
+ fallback={<div class="flex min-h-0 flex-1 items-center justify-center text-sm text-dimmed">No result pane configured.</div>}
325
+ >
326
+ {(resultSlot) => (
327
+ <section class="flex min-h-0 flex-col overflow-hidden bg-surface" style={{ height: `${state().resultSize}%` }}>
328
+ <header class="flex h-9 shrink-0 items-center gap-2 px-2 text-xs font-medium text-secondary">
329
+ <i class={`${iconClass(resultSlot().icon, "ti-layout-dashboard")} text-sm`} />
330
+ <span>{resultSlot().title ?? "Result"}</span>
331
+ </header>
332
+ <div class="min-h-0 flex-1 overflow-auto">{resultSlot().children}</div>
333
+ </section>
334
+ )}
335
+ </Show>
336
+
337
+ <button
338
+ type="button"
339
+ class="h-2 shrink-0 cursor-row-resize rounded-full bg-transparent transition hover:bg-blue-500/60"
340
+ aria-label="Resize result area"
341
+ onPointerDown={handleVerticalResize}
342
+ />
343
+
344
+ <div ref={bottomEl} class="flex min-h-0 flex-1 overflow-hidden">
345
+ <For each={state().sections}>
346
+ {(section, index) => {
347
+ const activePane = createMemo(() => paneById().get(section.activePaneId) ?? paneById().get(section.paneIds[0] ?? ""));
348
+ return (
349
+ <>
350
+ <section
351
+ class="flex min-h-0 min-w-0 flex-col gap-1 overflow-hidden bg-surface"
352
+ style={{ width: `${section.size}%` }}
353
+ role="region"
354
+ onDragOver={(event) => event.preventDefault()}
355
+ onDrop={(event) => {
356
+ event.preventDefault();
357
+ const paneId = dragPaneId(event);
358
+ if (paneId) movePane(paneId, section.id);
359
+ }}
360
+ >
361
+ <div class="flex h-7 shrink-0 items-start gap-1">
362
+ <For each={section.paneIds}>
363
+ {(paneId) => {
364
+ const pane = createMemo(() => paneById().get(paneId));
365
+ return (
366
+ <Show when={pane()}>
367
+ {(item) => (
368
+ <button
369
+ type="button"
370
+ draggable
371
+ class={`flex h-7 min-w-0 max-w-48 items-center gap-1.5 rounded bg-zinc-100/75 px-2 text-xs transition dark:bg-zinc-900/60 ${
372
+ section.activePaneId === paneId
373
+ ? "font-semibold text-blue-700 dark:text-blue-300"
374
+ : "text-secondary/75 hover:bg-zinc-100 hover:text-secondary dark:hover:bg-zinc-900"
375
+ }`}
376
+ onClick={() => setActivePane(section.id, paneId)}
377
+ onDragStart={(event) => {
378
+ event.dataTransfer?.setData("application/x-dock-pane-id", paneId);
379
+ event.dataTransfer?.setData("text/plain", paneId);
380
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
381
+ }}
382
+ onDragOver={(event) => event.preventDefault()}
383
+ onDrop={(event) => {
384
+ event.preventDefault();
385
+ const dragged = dragPaneId(event);
386
+ if (dragged) movePane(dragged, section.id, paneId);
387
+ }}
388
+ >
389
+ <i class={`${iconClass(item().icon, "ti-layout-sidebar-right")} shrink-0 text-sm`} />
390
+ <span class="truncate">{item().title}</span>
391
+ </button>
392
+ )}
393
+ </Show>
394
+ );
395
+ }}
396
+ </For>
397
+ </div>
398
+ <div class="min-h-0 flex-1 overflow-auto">
399
+ <Show when={activePane()} fallback={<div class="p-3 text-sm text-dimmed">Drop a pane here.</div>}>
400
+ {(pane) => pane().children}
401
+ </Show>
402
+ </div>
403
+ </section>
404
+ <Show when={index() < state().sections.length - 1}>
405
+ <button
406
+ type="button"
407
+ class="w-2 shrink-0 cursor-col-resize rounded-full bg-transparent transition hover:bg-blue-500/60"
408
+ aria-label="Resize section"
409
+ onPointerDown={(event) => handleSectionResize(event, index())}
410
+ />
411
+ </Show>
412
+ </>
413
+ );
414
+ }}
415
+ </For>
416
+ </div>
417
+ </div>
418
+ );
419
+ };
420
+
421
+ const DockWorkspace = DockWorkspaceRoot as DockWorkspaceComponent;
422
+ DockWorkspace.Result = DockWorkspaceResult;
423
+ DockWorkspace.Pane = DockWorkspacePane;
424
+
425
+ export default DockWorkspace;
@@ -0,0 +1,153 @@
1
+ import { For, Show, type JSX } from "solid-js";
2
+ import CopyButton from "./CopyButton";
3
+ import { highlightCodeDisplayLines } from "./code-highlight";
4
+ import type { CodeDisplayLanguage } from "./CodeDisplay";
5
+
6
+ export type DocCodeHighlighter = (code: string) => string;
7
+
8
+ export type DocCodeProps = {
9
+ code: string;
10
+ title?: string;
11
+ language?: CodeDisplayLanguage;
12
+ highlight?: DocCodeHighlighter;
13
+ format?: (code: string) => string;
14
+ copy?: boolean;
15
+ copyText?: string;
16
+ lineNumbers?: boolean;
17
+ class?: string;
18
+ };
19
+
20
+ export type DocNoteVariant = "info" | "tip" | "warning";
21
+
22
+ export type DocRow = {
23
+ title: string;
24
+ icon?: string;
25
+ text: JSX.Element;
26
+ };
27
+
28
+ export type DocConcept = {
29
+ title: string;
30
+ icon: string;
31
+ text: JSX.Element;
32
+ };
33
+
34
+ export const DocPage = (props: { children: JSX.Element; class?: string }) => (
35
+ <div class={`doc-page mx-auto max-w-3xl space-y-6 text-sm leading-relaxed text-dimmed ${props.class ?? ""}`}>{props.children}</div>
36
+ );
37
+
38
+ export const DocLead = (props: { children: JSX.Element }) => (
39
+ <p class="border-l-4 border-blue-500/70 pl-4 text-[15px] leading-7 text-secondary">{props.children}</p>
40
+ );
41
+
42
+ export const DocSection = (props: { title: string; eyebrow?: string; children: JSX.Element }) => (
43
+ <section class="space-y-3">
44
+ <div>
45
+ <Show when={props.eyebrow}>
46
+ {(eyebrow) => <p class="text-[11px] font-semibold uppercase tracking-wide text-blue-600 dark:text-blue-400">{eyebrow()}</p>}
47
+ </Show>
48
+ <h3 class="text-base font-semibold text-primary">{props.title}</h3>
49
+ </div>
50
+ {props.children}
51
+ </section>
52
+ );
53
+
54
+ export const DocInlineCode = (props: { children: JSX.Element }) => (
55
+ <code class="rounded bg-zinc-100 px-1 py-px font-mono text-[11px] text-zinc-800 dark:bg-zinc-900 dark:text-zinc-200">
56
+ {props.children}
57
+ </code>
58
+ );
59
+
60
+ export const DocCode = (props: DocCodeProps) => {
61
+ const code = () => props.format?.(props.code) ?? props.code;
62
+ const lineNumbers = () => props.lineNumbers ?? false;
63
+ const hasHeader = () => Boolean(props.title || props.copy);
64
+ const lines = () => {
65
+ const formatted = code();
66
+ if (props.highlight) return formatted.split("\n").map((line) => props.highlight?.(line || " ") ?? "");
67
+ return highlightCodeDisplayLines(formatted, props.language ?? "text");
68
+ };
69
+
70
+ return (
71
+ <div class={`doc-code overflow-hidden rounded-md ${props.class ?? ""}`}>
72
+ <Show when={hasHeader()}>
73
+ <div class="doc-code-header flex items-center justify-between gap-3 px-3 pb-0 pt-2">
74
+ <Show when={props.title}>{(title) => <p class="truncate text-[10px] font-medium leading-6 text-dimmed">{title()}</p>}</Show>
75
+ <Show when={props.copy}>
76
+ <CopyButton
77
+ text={props.copyText ?? code()}
78
+ class="focus-ui inline-flex h-6 w-6 items-center justify-center rounded text-[10px] text-dimmed hover:bg-white/80 hover:text-primary dark:hover:bg-zinc-800"
79
+ />
80
+ </Show>
81
+ </div>
82
+ </Show>
83
+ <div class={`doc-code-body overflow-x-auto px-3 ${hasHeader() ? "pb-2" : "py-2"} font-mono text-[11px] leading-relaxed`}>
84
+ <div class="min-w-max">
85
+ <For each={lines()}>
86
+ {(line, index) => (
87
+ <div class={lineNumbers() ? "grid grid-cols-[2rem_1fr]" : "grid grid-cols-[1fr]"}>
88
+ <Show when={lineNumbers()}>
89
+ <span class="select-none pr-3 text-right tabular-nums text-zinc-400 dark:text-zinc-600">{index() + 1}</span>
90
+ </Show>
91
+ <code class="whitespace-pre pr-4 font-mono" innerHTML={line || " "} />
92
+ </div>
93
+ )}
94
+ </For>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ );
99
+ };
100
+
101
+ export const DocConceptGrid = (props: { items: DocConcept[] }) => (
102
+ <div class="grid gap-3 md:grid-cols-2">
103
+ <For each={props.items}>
104
+ {(item) => (
105
+ <div class="grid grid-cols-[1.75rem_1fr] gap-3 rounded-md bg-zinc-50 px-3 py-3 ring-1 ring-inset ring-zinc-200/70 dark:bg-zinc-900/35 dark:ring-zinc-800">
106
+ <i class={`ti ${item.icon} mt-0.5 text-lg text-blue-500`} aria-hidden="true" />
107
+ <div>
108
+ <p class="font-semibold text-primary">{item.title}</p>
109
+ <p class="mt-1 text-sm text-dimmed">{item.text}</p>
110
+ </div>
111
+ </div>
112
+ )}
113
+ </For>
114
+ </div>
115
+ );
116
+
117
+ export const DocRows = (props: { items: DocRow[] }) => (
118
+ <div class="divide-y divide-zinc-200/70 rounded-md bg-zinc-50/60 ring-1 ring-inset ring-zinc-200/70 dark:divide-zinc-800 dark:bg-zinc-900/25 dark:ring-zinc-800">
119
+ <For each={props.items}>
120
+ {(item) => (
121
+ <article class="grid gap-3 px-3 py-3 md:grid-cols-[2rem_10rem_1fr]">
122
+ <Show when={item.icon} fallback={<span aria-hidden="true" />}>
123
+ {(icon) => <i class={`ti ${icon()} mt-0.5 text-lg text-blue-500`} aria-hidden="true" />}
124
+ </Show>
125
+ <p class="font-semibold text-primary">{item.title}</p>
126
+ <div class="text-dimmed">{item.text}</div>
127
+ </article>
128
+ )}
129
+ </For>
130
+ </div>
131
+ );
132
+
133
+ const noteClasses: Record<DocNoteVariant, string> = {
134
+ info: "bg-blue-50/70 text-blue-950 ring-blue-100 dark:bg-blue-950/20 dark:text-blue-100 dark:ring-blue-900/50",
135
+ tip: "bg-emerald-50/80 text-emerald-950 ring-emerald-100 dark:bg-emerald-950/20 dark:text-emerald-100 dark:ring-emerald-900/50",
136
+ warning: "bg-amber-50/80 text-amber-950 ring-amber-100 dark:bg-amber-950/20 dark:text-amber-100 dark:ring-amber-900/50",
137
+ };
138
+
139
+ const noteBodyClasses: Record<DocNoteVariant, string> = {
140
+ info: "text-blue-900/80 dark:text-blue-100/75",
141
+ tip: "text-emerald-900/80 dark:text-emerald-100/75",
142
+ warning: "text-amber-900/80 dark:text-amber-100/75",
143
+ };
144
+
145
+ export const DocNote = (props: { title: string; variant?: DocNoteVariant; children: JSX.Element }) => {
146
+ const variant = () => props.variant ?? "info";
147
+ return (
148
+ <aside class={`rounded-md px-4 py-3 text-sm ring-1 ring-inset ${noteClasses[variant()]}`}>
149
+ <p class="font-semibold">{props.title}</p>
150
+ <div class={`mt-1 ${noteBodyClasses[variant()]}`}>{props.children}</div>
151
+ </aside>
152
+ );
153
+ };
@@ -67,7 +67,7 @@ const POSITION_STYLES: Record<string, string> = {
67
67
  "position-try: --flip-block-down-left { top: anchor(bottom); bottom: auto; margin-bottom: 0; margin-top: 4px; };",
68
68
  };
69
69
 
70
- const ITEM_BASE_CLASSES = "flex w-full items-center gap-3 px-4 py-2 text-sm transition-colors hover:bg-white/30 dark:hover:bg-white/10";
70
+ const ITEM_BASE_CLASSES = "flex w-full items-center gap-3 px-4 py-2 text-sm transition-colors hover:bg-zinc-100 dark:hover:bg-white/10";
71
71
 
72
72
  // ==========================
73
73
  // Component
@@ -173,7 +173,7 @@ export default function Dropdown(props: DropdownProps) {
173
173
  popover="auto"
174
174
  role="menu"
175
175
  aria-label="Dropdown menu"
176
- class={`${width} overflow-y-auto max-h-[min(24rem,80dvh)] paper p-0 border! border-zinc-300/60! dark:border-zinc-600/50! ${props.className ?? ""}`}
176
+ class={`${width} overflow-y-auto max-h-[min(24rem,80dvh)] paper [box-shadow:var(--theme-shadow-float)]! p-0 border! border-zinc-300/60! dark:border-zinc-600/50! ${props.className ?? ""}`}
177
177
  >
178
178
  {props.elements.map((item, i) =>
179
179
  "items" in item ? (