@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.
Files changed (194) hide show
  1. package/package.json +18 -8
  2. package/scripts/preload.ts +78 -23
  3. package/src/_internal/define-app.ts +119 -47
  4. package/src/_internal/runtime-context.ts +1 -0
  5. package/src/api/accounts-entities.ts +4 -0
  6. package/src/api/admin-core-settings.ts +98 -0
  7. package/src/api/announcements.ts +131 -0
  8. package/src/api/auth/schemas.ts +24 -0
  9. package/src/api/auth.ts +113 -10
  10. package/src/api/index.ts +15 -25
  11. package/src/api/me.ts +203 -14
  12. package/src/api/search/schemas.ts +1 -0
  13. package/src/api/search.ts +62 -8
  14. package/src/config/ssr.ts +2 -9
  15. package/src/contracts/announcements.test.ts +37 -0
  16. package/src/contracts/announcements.ts +121 -0
  17. package/src/contracts/app.ts +4 -0
  18. package/src/contracts/index.ts +3 -2
  19. package/src/contracts/registry.ts +4 -0
  20. package/src/contracts/shared.ts +108 -1
  21. package/src/desktop/index.ts +704 -0
  22. package/src/desktop/solid.tsx +938 -0
  23. package/src/server/api/index.ts +1 -1
  24. package/src/server/api/respond.ts +50 -10
  25. package/src/server/index.ts +44 -38
  26. package/src/server/middleware/auth.ts +98 -9
  27. package/src/server/middleware/index.ts +2 -1
  28. package/src/server/middleware/settings.ts +26 -0
  29. package/src/server/services/access.test.ts +197 -0
  30. package/src/server/services/access.ts +254 -6
  31. package/src/server/services/index.ts +14 -11
  32. package/src/server/services/pagination.ts +22 -0
  33. package/src/server/time.ts +45 -0
  34. package/src/services/account-lifecycle/index.ts +142 -18
  35. package/src/services/accounts/app.ts +658 -170
  36. package/src/services/accounts/authz.test.ts +77 -0
  37. package/src/services/accounts/authz.ts +22 -0
  38. package/src/services/accounts/entities.ts +84 -5
  39. package/src/services/accounts/groups.ts +30 -24
  40. package/src/services/accounts/model.test.ts +30 -0
  41. package/src/services/accounts/switching.test.ts +14 -0
  42. package/src/services/accounts/switching.ts +15 -6
  43. package/src/services/accounts/users.ts +75 -52
  44. package/src/services/announcements/index.test.ts +32 -0
  45. package/src/services/announcements/index.ts +224 -0
  46. package/src/services/audit/index.test.ts +84 -0
  47. package/src/services/audit/index.ts +431 -0
  48. package/src/services/auth-flows/index.ts +9 -2
  49. package/src/services/auth-flows/ipa.ts +0 -2
  50. package/src/services/auth-flows/magic-link.ts +3 -2
  51. package/src/services/auth-flows/password-reset.ts +284 -0
  52. package/src/services/auth-flows/proxy-return.test.ts +24 -0
  53. package/src/services/auth-flows/proxy-return.ts +49 -0
  54. package/src/services/gateway.ts +162 -0
  55. package/src/services/index.ts +44 -2
  56. package/src/services/ipa/effective-groups.test.ts +33 -0
  57. package/src/services/ipa/effective-groups.ts +70 -0
  58. package/src/services/ipa/profile.ts +45 -3
  59. package/src/services/ipa/search.ts +3 -5
  60. package/src/services/ipa/service-account.ts +15 -0
  61. package/src/services/ipa/sync-planning.test.ts +32 -0
  62. package/src/services/ipa/sync-planning.ts +22 -0
  63. package/src/services/ipa/sync.ts +110 -38
  64. package/src/services/oauth-tokens.ts +104 -0
  65. package/src/services/postgres.ts +21 -6
  66. package/src/services/providers/local/auth.test.ts +22 -0
  67. package/src/services/providers/local/auth.ts +46 -3
  68. package/src/services/secrets.ts +10 -0
  69. package/src/services/service-account-credentials.test.ts +210 -0
  70. package/src/services/service-account-credentials.ts +715 -0
  71. package/src/services/service-accounts.ts +188 -0
  72. package/src/services/session/index.ts +7 -8
  73. package/src/services/settings/app.ts +4 -20
  74. package/src/services/settings/defaults.ts +64 -22
  75. package/src/services/settings/store.ts +47 -0
  76. package/src/services/weather/forecast.ts +40 -7
  77. package/src/services/webauthn.test.ts +36 -0
  78. package/src/services/webauthn.ts +384 -0
  79. package/src/shared/icons.ts +391 -100
  80. package/src/shared/index.ts +7 -0
  81. package/src/shared/markdown/extensions/code.ts +38 -1
  82. package/src/shared/markdown/extensions/images.ts +39 -3
  83. package/src/shared/markdown/extensions/info-blocks.ts +5 -5
  84. package/src/shared/markdown/extensions/mark.ts +48 -0
  85. package/src/shared/markdown/extensions/sub-sup.ts +60 -0
  86. package/src/shared/markdown/extensions/tables.ts +79 -58
  87. package/src/shared/markdown/formula.test.ts +1089 -0
  88. package/src/shared/markdown/formula.ts +1187 -0
  89. package/src/shared/markdown/index.ts +76 -2
  90. package/src/shared/mock-cover.ts +130 -0
  91. package/src/shared/redirect.test.ts +49 -0
  92. package/src/shared/redirect.ts +52 -0
  93. package/src/shared/theme.test.ts +24 -0
  94. package/src/shared/theme.ts +68 -0
  95. package/src/shared/time.ts +13 -0
  96. package/src/ssr/AdminLayout.tsx +7 -3
  97. package/src/ssr/AdminSidebar.tsx +115 -49
  98. package/src/ssr/AppLaunchpad.island.tsx +176 -0
  99. package/src/ssr/Footer.island.tsx +3 -8
  100. package/src/ssr/GlobalAnnouncements.island.tsx +141 -0
  101. package/src/ssr/GlobalSearchDialog.tsx +545 -117
  102. package/src/ssr/HotkeysHelpRail.island.tsx +3 -70
  103. package/src/ssr/Layout.tsx +74 -66
  104. package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
  105. package/src/ssr/LayoutHelp.tsx +266 -0
  106. package/src/ssr/NavMenu.island.tsx +0 -39
  107. package/src/ssr/ThemeToggleRail.island.tsx +3 -3
  108. package/src/ssr/TimezoneCookie.island.tsx +23 -0
  109. package/src/ssr/islands/index.ts +13 -0
  110. package/src/styles/base-popover.css +5 -2
  111. package/src/styles/effects.css +87 -6
  112. package/src/styles/global.css +146 -9
  113. package/src/styles/input.css +3 -1
  114. package/src/styles/utilities-buttons.css +133 -27
  115. package/src/styles/utilities-code-display.css +67 -0
  116. package/src/styles/utilities-completion.css +223 -0
  117. package/src/styles/utilities-detail.css +73 -0
  118. package/src/styles/utilities-feedback.css +16 -15
  119. package/src/styles/utilities-layout.css +42 -2
  120. package/src/styles/utilities-markdown-editor.css +472 -0
  121. package/src/styles/utilities-navigation.css +63 -8
  122. package/src/styles/utilities-script.css +84 -0
  123. package/src/styles/utilities-table-tile.css +229 -0
  124. package/src/types/ambient.d.ts +9 -0
  125. package/src/ui/completion/behaviors.test.ts +95 -0
  126. package/src/ui/completion/behaviors.ts +205 -0
  127. package/src/ui/completion/engine.ts +368 -0
  128. package/src/ui/completion/index.ts +40 -0
  129. package/src/ui/completion/overlay.ts +92 -0
  130. package/src/ui/dialog-core.ts +173 -45
  131. package/src/ui/filter/FilterChip.tsx +42 -40
  132. package/src/ui/index.ts +11 -12
  133. package/src/ui/input/AutocompleteEditor.tsx +656 -0
  134. package/src/ui/input/CheckboxCard.tsx +91 -0
  135. package/src/ui/input/Combobox.tsx +375 -0
  136. package/src/ui/input/DatePicker.tsx +846 -0
  137. package/src/ui/input/DateTimeInput.tsx +29 -4
  138. package/src/ui/input/FileDropzone.tsx +116 -0
  139. package/src/ui/input/IconInput.tsx +116 -0
  140. package/src/ui/input/ImageInput.tsx +19 -2
  141. package/src/ui/input/MultiSelectInput.tsx +448 -0
  142. package/src/ui/input/NumberInput.tsx +417 -61
  143. package/src/ui/input/SegmentedControl.tsx +2 -2
  144. package/src/ui/input/Select.tsx +172 -10
  145. package/src/ui/input/Slider.tsx +3 -4
  146. package/src/ui/input/Switch.tsx +3 -2
  147. package/src/ui/input/TemplateEditor.tsx +212 -0
  148. package/src/ui/input/TextInput.tsx +144 -13
  149. package/src/ui/input/index.ts +53 -8
  150. package/src/ui/input/markdown/MarkdownEditor.tsx +774 -0
  151. package/src/ui/input/markdown/Toolbar.tsx +90 -0
  152. package/src/ui/input/markdown/actions.ts +233 -0
  153. package/src/ui/input/markdown/active-formats.ts +94 -0
  154. package/src/ui/input/markdown/behaviors.ts +193 -0
  155. package/src/ui/input/markdown/code-zone.ts +23 -0
  156. package/src/ui/input/markdown/highlight.ts +316 -0
  157. package/src/ui/layout.ts +22 -0
  158. package/src/ui/misc/AppOverview.tsx +105 -0
  159. package/src/ui/misc/AppWorkspace.tsx +607 -0
  160. package/src/ui/misc/Calendar.tsx +1291 -0
  161. package/src/ui/misc/Chart.tsx +162 -0
  162. package/src/ui/misc/CodeDisplay.tsx +54 -0
  163. package/src/ui/misc/ContextMenu.tsx +2 -2
  164. package/src/ui/misc/DataTable.tsx +269 -0
  165. package/src/ui/misc/DockWorkspace.tsx +425 -0
  166. package/src/ui/misc/Docs.tsx +153 -0
  167. package/src/ui/misc/Dropdown.tsx +2 -2
  168. package/src/ui/misc/EntitySearch.tsx +260 -129
  169. package/src/ui/misc/LinkCard.tsx +14 -2
  170. package/src/ui/misc/LogEntriesTable.tsx +34 -31
  171. package/src/ui/misc/Pagination.tsx +31 -12
  172. package/src/ui/misc/PanelDialog.tsx +109 -0
  173. package/src/ui/misc/Panes.tsx +873 -0
  174. package/src/ui/misc/PermissionEditor.tsx +358 -262
  175. package/src/ui/misc/Placeholder.tsx +40 -0
  176. package/src/ui/misc/ProgressBar.tsx +1 -1
  177. package/src/ui/misc/ResourceApiKeys.tsx +260 -0
  178. package/src/ui/misc/SettingsModal.tsx +150 -0
  179. package/src/ui/misc/StatCell.tsx +182 -40
  180. package/src/ui/misc/StatGrid.tsx +149 -0
  181. package/src/ui/misc/StructuredDataPreview.tsx +107 -0
  182. package/src/ui/misc/code-highlight.ts +213 -0
  183. package/src/ui/misc/index.ts +93 -12
  184. package/src/ui/prompts.tsx +362 -312
  185. package/src/ui/toast.ts +384 -0
  186. package/src/ui/widgets/Widget.tsx +12 -4
  187. package/src/ssr/MoreAppsDropdown.island.tsx +0 -61
  188. package/src/ui/ipa/GroupView.tsx +0 -36
  189. package/src/ui/ipa/LoginBtn.tsx +0 -16
  190. package/src/ui/ipa/UserView.tsx +0 -58
  191. package/src/ui/ipa/index.ts +0 -4
  192. package/src/ui/navigation.ts +0 -32
  193. package/src/ui/sidebar.tsx +0 -468
  194. /package/src/ui/{ipa → misc}/Avatar.tsx +0 -0
@@ -0,0 +1,607 @@
1
+ import {
2
+ captureScroll,
3
+ documentNavigate,
4
+ type LinkNavigateEvent,
5
+ type LinkProps,
6
+ type NavigationScrollMode,
7
+ navigate,
8
+ restoreScroll,
9
+ startViewTransition,
10
+ } from "@valentinkolb/ssr/nav";
11
+ import { children, createContext, createMemo, type JSX, Show, useContext } from "solid-js";
12
+
13
+ const SIDEBAR_HEADER = Symbol("AppWorkspace.SidebarHeader");
14
+ const SIDEBAR_MOBILE = Symbol("AppWorkspace.SidebarMobile");
15
+ const SIDEBAR_DESKTOP = Symbol("AppWorkspace.SidebarDesktop");
16
+
17
+ type SidebarSlotKind = typeof SIDEBAR_HEADER | typeof SIDEBAR_MOBILE | typeof SIDEBAR_DESKTOP;
18
+
19
+ type SidebarSlot = {
20
+ readonly kind: SidebarSlotKind;
21
+ children?: JSX.Element;
22
+ };
23
+
24
+ type SidebarHeaderSlot = SidebarSlot & {
25
+ readonly kind: typeof SIDEBAR_HEADER;
26
+ title: string;
27
+ subtitle?: string;
28
+ icon?: string | false;
29
+ iconStyle?: string;
30
+ iconViewTransitionName?: string;
31
+ titleViewTransitionName?: string;
32
+ action?: JSX.Element;
33
+ };
34
+
35
+ type SidebarMobileSlot = SidebarSlot & {
36
+ readonly kind: typeof SIDEBAR_MOBILE;
37
+ };
38
+
39
+ type SidebarDesktopSlot = SidebarSlot & {
40
+ readonly kind: typeof SIDEBAR_DESKTOP;
41
+ };
42
+
43
+ type SidebarMode = "desktop" | "mobile";
44
+
45
+ const SidebarModeContext = createContext<SidebarMode>("desktop");
46
+
47
+ export type AppWorkspaceProps = {
48
+ class?: string;
49
+ children: JSX.Element;
50
+ };
51
+
52
+ export type AppWorkspaceMainProps = {
53
+ class?: string;
54
+ children: JSX.Element;
55
+ };
56
+
57
+ export type AppWorkspaceDetailWidth = "sm" | "md" | "lg" | "xl";
58
+
59
+ export type AppWorkspaceDetailProps = {
60
+ id?: string;
61
+ open: boolean;
62
+ width?: AppWorkspaceDetailWidth;
63
+ widthClass?: string;
64
+ viewTransitionName?: string;
65
+ class?: string;
66
+ children: JSX.Element;
67
+ };
68
+
69
+ export type AppWorkspaceSidebarProps = {
70
+ class?: string;
71
+ children: JSX.Element;
72
+ };
73
+
74
+ export type AppWorkspaceSidebarHeaderProps = {
75
+ title: string;
76
+ subtitle?: string;
77
+ icon?: string | false;
78
+ iconStyle?: string;
79
+ iconViewTransitionName?: string;
80
+ titleViewTransitionName?: string;
81
+ action?: JSX.Element;
82
+ };
83
+
84
+ export type AppWorkspaceSidebarMobileProps = {
85
+ children: JSX.Element;
86
+ };
87
+
88
+ export type AppWorkspaceSidebarMobileItemsProps = {
89
+ scrollPreserveKey?: string | false;
90
+ children: JSX.Element;
91
+ };
92
+
93
+ export type AppWorkspaceSidebarBodyProps = {
94
+ class?: string;
95
+ scrollPreserveKey?: string | false;
96
+ children: JSX.Element;
97
+ };
98
+
99
+ export type AppWorkspaceSidebarSectionProps = {
100
+ title?: string;
101
+ class?: string;
102
+ children: JSX.Element;
103
+ };
104
+
105
+ export type AppWorkspaceSidebarItemTone = "default" | "success" | "danger";
106
+ export type AppWorkspaceSidebarIconActionTone = "default" | "success" | "danger";
107
+
108
+ export type AppWorkspaceSidebarItemProps = {
109
+ href?: string;
110
+ navigation?: "enhanced" | "document";
111
+ replace?: boolean;
112
+ scroll?: NavigationScrollMode;
113
+ onNavigate?: (event: LinkNavigateEvent) => void | Promise<void>;
114
+ active?: boolean;
115
+ activeClass?: string;
116
+ disabled?: boolean;
117
+ icon?: string;
118
+ meta?: JSX.Element;
119
+ tone?: AppWorkspaceSidebarItemTone;
120
+ title?: string;
121
+ viewTransitionName?: string;
122
+ class?: string;
123
+ actionIcon?: string;
124
+ actionLabel?: string;
125
+ onActionClick?: (event: MouseEvent) => void;
126
+ onClick?: (event: MouseEvent) => void;
127
+ data?: Record<string, string | number | boolean | null | undefined>;
128
+ children: JSX.Element;
129
+ };
130
+
131
+ export type AppWorkspaceSidebarIconGridProps = {
132
+ title?: string;
133
+ columns?: 2 | 3;
134
+ class?: string;
135
+ children: JSX.Element;
136
+ };
137
+
138
+ export type AppWorkspaceSidebarIconActionProps = {
139
+ href?: string | null;
140
+ navigation?: "enhanced" | "document";
141
+ replace?: boolean;
142
+ scroll?: NavigationScrollMode;
143
+ onNavigate?: (event: LinkNavigateEvent) => void | Promise<void>;
144
+ icon: string;
145
+ label: string;
146
+ active?: boolean;
147
+ disabled?: boolean;
148
+ tone?: AppWorkspaceSidebarIconActionTone;
149
+ viewTransitionName?: string;
150
+ onClick?: (event: MouseEvent) => void;
151
+ };
152
+
153
+ type AppWorkspaceComponent = ((props: AppWorkspaceProps) => JSX.Element) & {
154
+ Main: (props: AppWorkspaceMainProps) => JSX.Element;
155
+ Detail: (props: AppWorkspaceDetailProps) => JSX.Element;
156
+ Sidebar: (props: AppWorkspaceSidebarProps) => JSX.Element;
157
+ SidebarHeader: (props: AppWorkspaceSidebarHeaderProps) => JSX.Element;
158
+ SidebarMobile: (props: AppWorkspaceSidebarMobileProps) => JSX.Element;
159
+ SidebarMobileItems: (props: AppWorkspaceSidebarMobileItemsProps) => JSX.Element;
160
+ SidebarMobileBody: (props: AppWorkspaceSidebarBodyProps) => JSX.Element;
161
+ SidebarDesktop: (props: { children: JSX.Element }) => JSX.Element;
162
+ SidebarSection: (props: AppWorkspaceSidebarSectionProps) => JSX.Element;
163
+ SidebarBody: (props: AppWorkspaceSidebarBodyProps) => JSX.Element;
164
+ SidebarFooter: (props: { class?: string; children: JSX.Element }) => JSX.Element;
165
+ SidebarItem: (props: AppWorkspaceSidebarItemProps) => JSX.Element;
166
+ SidebarIconGrid: (props: AppWorkspaceSidebarIconGridProps) => JSX.Element;
167
+ SidebarIconAction: (props: AppWorkspaceSidebarIconActionProps) => JSX.Element;
168
+ };
169
+
170
+ const isSidebarSlot = (value: unknown): value is SidebarSlot => !!value && typeof value === "object" && "kind" in value;
171
+
172
+ const collectSidebarSlots = (value: unknown): SidebarSlot[] => {
173
+ if (Array.isArray(value)) return value.flatMap(collectSidebarSlots);
174
+ return isSidebarSlot(value) ? [value] : [];
175
+ };
176
+
177
+ const tablerIconClass = (icon: string | null | undefined, fallback: string): string => {
178
+ const value = icon?.trim() || fallback;
179
+ return value.startsWith("ti ") ? value : `ti ${value}`;
180
+ };
181
+
182
+ const shouldEnhanceNavigation = (href: string | null | undefined, mode: "enhanced" | "document" | undefined): href is string => {
183
+ if (!href || mode === "document") return false;
184
+ if (/^(https?:)?\/\//.test(href)) return false;
185
+ if (/^(mailto|tel|sms):/.test(href)) return false;
186
+ return true;
187
+ };
188
+
189
+ const linkEnhancementProps = (props: {
190
+ href: string;
191
+ navigation?: "enhanced" | "document";
192
+ replace?: boolean;
193
+ scroll?: NavigationScrollMode;
194
+ onNavigate?: (event: LinkNavigateEvent) => void | Promise<void>;
195
+ }): Pick<LinkProps, "replace" | "scroll" | "onNavigate"> | undefined => {
196
+ if (!shouldEnhanceNavigation(props.href, props.navigation)) return undefined;
197
+ return {
198
+ replace: props.replace,
199
+ scroll: props.scroll,
200
+ onNavigate:
201
+ props.onNavigate ??
202
+ ((nav) => {
203
+ nav.push();
204
+ }),
205
+ };
206
+ };
207
+
208
+ const shouldHandleEnhancedClick = (event: MouseEvent, anchor: HTMLAnchorElement): boolean => {
209
+ if (event.defaultPrevented || event.button !== 0) return false;
210
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return false;
211
+ if (anchor.target && anchor.target !== "_self") return false;
212
+ if (anchor.hasAttribute("download")) return false;
213
+ return new URL(anchor.href, window.location.href).origin === window.location.origin;
214
+ };
215
+
216
+ const handleEnhancedClick = (
217
+ event: MouseEvent & { currentTarget: HTMLAnchorElement },
218
+ href: string,
219
+ props: Pick<LinkProps, "replace" | "scroll" | "onNavigate">,
220
+ ) => {
221
+ if (!shouldHandleEnhancedClick(event, event.currentTarget)) return;
222
+
223
+ const url = new URL(href, window.location.href);
224
+ const scroll = props.scroll ?? "top";
225
+ const replace = Boolean(props.replace);
226
+ const scrollSnapshot = captureScroll();
227
+
228
+ event.preventDefault();
229
+
230
+ if (!props.onNavigate) {
231
+ navigate(href, { replace, scroll, scrollSnapshot });
232
+ return;
233
+ }
234
+
235
+ startViewTransition(() =>
236
+ props.onNavigate!({
237
+ event,
238
+ href,
239
+ url,
240
+ replace,
241
+ scroll,
242
+ push: (nextHref = href, options = {}) =>
243
+ navigate(nextHref, { replace: false, scroll, scrollSnapshot, viewTransition: false, ...options }),
244
+ replaceWith: (nextHref = href, options = {}) =>
245
+ navigate(nextHref, { replace: true, scroll, scrollSnapshot, viewTransition: false, ...options }),
246
+ fallback: (nextHref = href) => documentNavigate(nextHref, { replace }),
247
+ scrollSnapshot,
248
+ captureScroll,
249
+ restoreScroll,
250
+ }),
251
+ );
252
+ };
253
+
254
+ const detailWidthClass = (props: AppWorkspaceDetailProps): string => {
255
+ if (props.widthClass) return props.widthClass;
256
+ switch (props.width ?? "md") {
257
+ case "sm":
258
+ return "lg:w-80 xl:w-72";
259
+ case "lg":
260
+ return "lg:w-[30rem] xl:w-[34rem]";
261
+ case "xl":
262
+ return "lg:w-[34rem] xl:w-[40rem]";
263
+ case "md":
264
+ default:
265
+ return "lg:w-[20rem] xl:w-[24rem]";
266
+ }
267
+ };
268
+
269
+ const AppWorkspaceMain = (props: AppWorkspaceMainProps) => (
270
+ <main class={`order-3 flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden lg:order-2 ${props.class ?? ""}`}>{props.children}</main>
271
+ );
272
+
273
+ const AppWorkspaceDetail = (props: AppWorkspaceDetailProps) => (
274
+ <aside
275
+ id={props.id}
276
+ class={`${props.open ? "flex" : "hidden"} order-2 min-h-0 w-full shrink-0 flex-col overflow-hidden lg:order-3 lg:h-full ${detailWidthClass(props)} ${props.class ?? ""}`}
277
+ style={props.viewTransitionName ? `view-transition-name:${props.viewTransitionName}` : undefined}
278
+ >
279
+ {props.children}
280
+ </aside>
281
+ );
282
+
283
+ function AppWorkspaceSidebarHeader(props: AppWorkspaceSidebarHeaderProps): JSX.Element {
284
+ return {
285
+ kind: SIDEBAR_HEADER,
286
+ ...props,
287
+ } satisfies SidebarHeaderSlot as unknown as JSX.Element;
288
+ }
289
+
290
+ function AppWorkspaceSidebarMobile(props: AppWorkspaceSidebarMobileProps): JSX.Element {
291
+ return {
292
+ kind: SIDEBAR_MOBILE,
293
+ children: props.children,
294
+ } satisfies SidebarMobileSlot as unknown as JSX.Element;
295
+ }
296
+
297
+ function AppWorkspaceSidebarDesktop(props: { children: JSX.Element }): JSX.Element {
298
+ return {
299
+ kind: SIDEBAR_DESKTOP,
300
+ children: props.children,
301
+ } satisfies SidebarDesktopSlot as unknown as JSX.Element;
302
+ }
303
+
304
+ const SidebarHeaderContent = (props: { header: SidebarHeaderSlot; mobile?: boolean }) => (
305
+ <>
306
+ <Show when={props.header.icon !== false}>
307
+ <div
308
+ class={`${props.mobile ? "h-8 w-8 rounded-lg" : "sidebar-header-icon"} flex shrink-0 items-center justify-center bg-blue-500 text-white`}
309
+ style={`${props.header.iconStyle ?? ""}${props.header.iconViewTransitionName ? `;view-transition-name:${props.header.iconViewTransitionName}` : ""}`}
310
+ >
311
+ <i class={`${tablerIconClass(props.header.icon || undefined, "ti-layout-sidebar")} ${props.mobile ? "text-sm" : "text-xs"}`} />
312
+ </div>
313
+ </Show>
314
+ <div class="min-w-0 flex-1">
315
+ <p
316
+ class={props.mobile ? "truncate font-semibold" : "sidebar-header-title"}
317
+ style={props.header.titleViewTransitionName ? `view-transition-name:${props.header.titleViewTransitionName}` : undefined}
318
+ >
319
+ {props.header.title}
320
+ </p>
321
+ <Show when={!props.mobile && props.header.subtitle}>
322
+ <p class="sidebar-header-subtitle">{props.header.subtitle}</p>
323
+ </Show>
324
+ </div>
325
+ <Show when={!props.mobile && props.header.action}>{props.header.action}</Show>
326
+ </>
327
+ );
328
+
329
+ const AppWorkspaceSidebar = (props: AppWorkspaceSidebarProps) => {
330
+ const resolved = children(() => props.children);
331
+ const slots = createMemo(() => collectSidebarSlots(resolved()));
332
+ const header = createMemo(() => slots().find((slot): slot is SidebarHeaderSlot => slot.kind === SIDEBAR_HEADER));
333
+ const mobile = createMemo(() => slots().find((slot): slot is SidebarMobileSlot => slot.kind === SIDEBAR_MOBILE));
334
+ const desktop = createMemo(() => slots().find((slot): slot is SidebarDesktopSlot => slot.kind === SIDEBAR_DESKTOP));
335
+
336
+ return (
337
+ <>
338
+ <Show when={header() && mobile()}>
339
+ <nav class="sidebar-container-mobile">
340
+ <details class="group">
341
+ <summary class="sidebar-mobile-toggle">
342
+ <SidebarHeaderContent header={header()!} mobile />
343
+ <span class="ml-auto inline-flex h-7 w-7 items-center justify-center rounded-md text-dimmed transition-transform group-open:rotate-180">
344
+ <i class="ti ti-chevron-down text-sm" />
345
+ </span>
346
+ </summary>
347
+ <SidebarModeContext.Provider value="mobile">{mobile()!.children}</SidebarModeContext.Provider>
348
+ </details>
349
+ </nav>
350
+ </Show>
351
+
352
+ <aside class={`sidebar-container ${props.class ?? ""}`}>
353
+ <div class="paper flex h-full min-h-0 flex-col gap-4 p-3">
354
+ <Show when={header()}>
355
+ <div class="relative flex items-center gap-3 pr-7">
356
+ <SidebarHeaderContent header={header()!} />
357
+ </div>
358
+ </Show>
359
+ <SidebarModeContext.Provider value="desktop">{desktop()?.children}</SidebarModeContext.Provider>
360
+ </div>
361
+ </aside>
362
+ </>
363
+ );
364
+ };
365
+
366
+ const scrollPreserveAttr = (key: string | false | undefined) => (key ? { "data-scroll-preserve": key } : {});
367
+
368
+ const AppWorkspaceSidebarMobileItems = (props: AppWorkspaceSidebarMobileItemsProps) => (
369
+ <div class="sidebar-mobile-actions" {...scrollPreserveAttr(props.scrollPreserveKey)}>
370
+ {props.children}
371
+ </div>
372
+ );
373
+
374
+ const AppWorkspaceSidebarMobileBody = (props: AppWorkspaceSidebarBodyProps) => (
375
+ <div class={`mt-2 max-h-64 overflow-y-auto p-2 ${props.class ?? ""}`} {...scrollPreserveAttr(props.scrollPreserveKey)}>
376
+ {props.children}
377
+ </div>
378
+ );
379
+
380
+ const AppWorkspaceSidebarSection = (props: AppWorkspaceSidebarSectionProps) => (
381
+ <section class={`sidebar-group ${props.class ?? ""}`}>
382
+ <Show when={props.title}>
383
+ <p class="sidebar-section-title">{props.title}</p>
384
+ </Show>
385
+ {props.children}
386
+ </section>
387
+ );
388
+
389
+ const AppWorkspaceSidebarBody = (props: AppWorkspaceSidebarBodyProps) => (
390
+ <div class={`sidebar-body ${props.class ?? ""}`} {...scrollPreserveAttr(props.scrollPreserveKey)}>
391
+ {props.children}
392
+ </div>
393
+ );
394
+
395
+ const AppWorkspaceSidebarFooter = (props: { class?: string; children: JSX.Element }) => (
396
+ <section class={`sidebar-footer ${props.class ?? ""}`}>{props.children}</section>
397
+ );
398
+
399
+ const AppWorkspaceSidebarIconGrid = (props: AppWorkspaceSidebarIconGridProps) => (
400
+ <section class={`sidebar-icon-grid-wrap ${props.class ?? ""}`}>
401
+ <Show when={props.title}>
402
+ <p class="sidebar-section-title">{props.title}</p>
403
+ </Show>
404
+ <div class={`sidebar-icon-grid ${props.columns === 3 ? "grid-cols-3" : "grid-cols-2"}`}>{props.children}</div>
405
+ </section>
406
+ );
407
+
408
+ const itemToneClass = (tone: AppWorkspaceSidebarItemTone | undefined, mobile: boolean): string => {
409
+ if (tone === "success") {
410
+ return mobile
411
+ ? "border-green-500/25 bg-green-50/70 text-green-700 dark:border-green-400/30 dark:bg-green-950/30 dark:text-green-300"
412
+ : "text-green-600 bg-green-500/10 hover:bg-green-500/20 dark:text-green-400";
413
+ }
414
+ if (tone === "danger") {
415
+ return mobile
416
+ ? "border-red-500/25 bg-red-50/70 text-red-700 dark:border-red-400/30 dark:bg-red-950/30 dark:text-red-300"
417
+ : "text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-950/30";
418
+ }
419
+ return "";
420
+ };
421
+
422
+ const activeMobileClass = "border-blue-500/35 bg-blue-50/70 text-blue-700 dark:border-blue-400/40 dark:bg-blue-950/40 dark:text-blue-200";
423
+
424
+ const iconActionToneClass = (tone: AppWorkspaceSidebarIconActionTone | undefined): string => {
425
+ if (tone === "success") return "sidebar-icon-action-success";
426
+ if (tone === "danger") return "sidebar-icon-action-danger";
427
+ return "";
428
+ };
429
+
430
+ const AppWorkspaceSidebarItem = (props: AppWorkspaceSidebarItemProps) => {
431
+ const mode = useContext(SidebarModeContext);
432
+ const mobile = () => mode === "mobile";
433
+ const className = () =>
434
+ mobile()
435
+ ? `sidebar-item-mobile ${props.active ? (props.activeClass ?? activeMobileClass) : ""} ${itemToneClass(props.tone, true)} ${props.disabled ? "pointer-events-none opacity-50" : ""} ${props.class ?? ""}`
436
+ : `sidebar-item text-xs ${props.active ? (props.activeClass ?? "sidebar-item-active") : ""} ${itemToneClass(props.tone, false)} ${props.disabled ? "pointer-events-none opacity-50" : ""} ${props.class ?? ""}`;
437
+ const style = () => (props.viewTransitionName ? `view-transition-name:${props.viewTransitionName}` : undefined);
438
+ const dataAttrs = () =>
439
+ Object.fromEntries(
440
+ Object.entries(props.data ?? {})
441
+ .filter(([, value]) => value !== null && value !== undefined)
442
+ .map(([key, value]) => [`data-${key}`, String(value)]),
443
+ );
444
+ const enhanced = (href: string) =>
445
+ linkEnhancementProps({
446
+ href,
447
+ navigation: props.navigation,
448
+ replace: props.replace,
449
+ scroll: props.scroll,
450
+ onNavigate: props.onNavigate,
451
+ });
452
+
453
+ const content = (
454
+ <>
455
+ <Show when={props.icon}>
456
+ <i class={`${tablerIconClass(props.icon, "ti-circle")} ${mobile() ? "" : "text-sm"}`} />
457
+ </Show>
458
+ <span class="min-w-0 flex-1 truncate text-left">{props.children}</span>
459
+ <Show when={props.meta}>
460
+ <span class="shrink-0 text-dimmed tabular-nums">{props.meta}</span>
461
+ </Show>
462
+ <Show when={props.actionIcon && !mobile()}>
463
+ <button
464
+ type="button"
465
+ class="sidebar-item-action"
466
+ aria-label={props.actionLabel ?? "Row action"}
467
+ onClick={(event) => {
468
+ event.preventDefault();
469
+ event.stopPropagation();
470
+ props.onActionClick?.(event);
471
+ }}
472
+ >
473
+ <i class={`${tablerIconClass(props.actionIcon, "ti-dots")} text-xs`} />
474
+ </button>
475
+ </Show>
476
+ </>
477
+ );
478
+
479
+ return (
480
+ <Show
481
+ when={props.disabled ? undefined : props.href}
482
+ fallback={
483
+ <button
484
+ type="button"
485
+ class={className()}
486
+ title={props.title}
487
+ style={style()}
488
+ disabled={props.disabled}
489
+ onClick={props.onClick}
490
+ {...dataAttrs()}
491
+ >
492
+ {content}
493
+ </button>
494
+ }
495
+ >
496
+ {(href) => (
497
+ <Show
498
+ when={enhanced(href())}
499
+ fallback={
500
+ <a href={href()} class={className()} title={props.title} style={style()} onClick={props.onClick} {...dataAttrs()}>
501
+ {content}
502
+ </a>
503
+ }
504
+ >
505
+ {(linkProps) => (
506
+ <a
507
+ href={href()}
508
+ class={className()}
509
+ title={props.title}
510
+ style={style()}
511
+ onClick={(event) => {
512
+ props.onClick?.(event);
513
+ handleEnhancedClick(event, href(), linkProps());
514
+ }}
515
+ {...dataAttrs()}
516
+ >
517
+ {content}
518
+ </a>
519
+ )}
520
+ </Show>
521
+ )}
522
+ </Show>
523
+ );
524
+ };
525
+
526
+ const AppWorkspaceSidebarIconAction = (props: AppWorkspaceSidebarIconActionProps) => {
527
+ const className = () =>
528
+ `sidebar-icon-action ${props.active ? "sidebar-icon-action-active" : ""} ${iconActionToneClass(props.tone)} ${props.disabled ? "pointer-events-none opacity-40" : ""}`;
529
+ const style = () => (props.viewTransitionName ? `view-transition-name:${props.viewTransitionName}` : undefined);
530
+ const content = <i class={`${tablerIconClass(props.icon, "ti-circle")} text-base`} />;
531
+ const href = () => (props.href && !props.disabled ? props.href : null);
532
+ const enhanced = (href: string) =>
533
+ linkEnhancementProps({
534
+ href,
535
+ navigation: props.navigation,
536
+ replace: props.replace,
537
+ scroll: props.scroll,
538
+ onNavigate: props.onNavigate,
539
+ });
540
+
541
+ return (
542
+ <Show
543
+ when={href()}
544
+ fallback={
545
+ <button
546
+ type="button"
547
+ class={className()}
548
+ title={props.label}
549
+ aria-label={props.label}
550
+ disabled={props.disabled}
551
+ style={style()}
552
+ onClick={props.onClick}
553
+ >
554
+ {content}
555
+ </button>
556
+ }
557
+ >
558
+ {(href) => (
559
+ <Show
560
+ when={enhanced(href())}
561
+ fallback={
562
+ <a href={href()} class={className()} title={props.label} aria-label={props.label} style={style()} onClick={props.onClick}>
563
+ {content}
564
+ </a>
565
+ }
566
+ >
567
+ {(linkProps) => (
568
+ <a
569
+ href={href()}
570
+ class={className()}
571
+ title={props.label}
572
+ aria-label={props.label}
573
+ style={style()}
574
+ onClick={(event) => {
575
+ props.onClick?.(event);
576
+ handleEnhancedClick(event, href(), linkProps());
577
+ }}
578
+ >
579
+ {content}
580
+ </a>
581
+ )}
582
+ </Show>
583
+ )}
584
+ </Show>
585
+ );
586
+ };
587
+
588
+ const AppWorkspace = ((props: AppWorkspaceProps) => (
589
+ <div class={`app-cols h-full ${props.class ?? ""}`}>{props.children}</div>
590
+ )) as AppWorkspaceComponent;
591
+
592
+ AppWorkspace.Main = AppWorkspaceMain;
593
+ AppWorkspace.Detail = AppWorkspaceDetail;
594
+ AppWorkspace.Sidebar = AppWorkspaceSidebar;
595
+ AppWorkspace.SidebarHeader = AppWorkspaceSidebarHeader;
596
+ AppWorkspace.SidebarMobile = AppWorkspaceSidebarMobile;
597
+ AppWorkspace.SidebarMobileItems = AppWorkspaceSidebarMobileItems;
598
+ AppWorkspace.SidebarMobileBody = AppWorkspaceSidebarMobileBody;
599
+ AppWorkspace.SidebarDesktop = AppWorkspaceSidebarDesktop;
600
+ AppWorkspace.SidebarSection = AppWorkspaceSidebarSection;
601
+ AppWorkspace.SidebarBody = AppWorkspaceSidebarBody;
602
+ AppWorkspace.SidebarFooter = AppWorkspaceSidebarFooter;
603
+ AppWorkspace.SidebarItem = AppWorkspaceSidebarItem;
604
+ AppWorkspace.SidebarIconGrid = AppWorkspaceSidebarIconGrid;
605
+ AppWorkspace.SidebarIconAction = AppWorkspaceSidebarIconAction;
606
+
607
+ export default AppWorkspace;