@valentinkolb/cloud 0.4.0 → 0.5.1
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 -6
- package/scripts/preload.ts +78 -23
- package/src/_internal/define-app.ts +53 -46
- 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 +116 -13
- package/src/api/index.ts +7 -2
- 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 +2 -0
- package/src/contracts/index.ts +3 -2
- package/src/contracts/registry.ts +2 -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 +47 -7
- package/src/services/auth-flows/magic-link.ts +92 -20
- 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/notifications/index.ts +82 -11
- 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 +79 -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 +58 -0
- package/src/shared/redirect.ts +56 -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
|
@@ -1,80 +1,13 @@
|
|
|
1
|
-
import { For, Show, createMemo } from "solid-js";
|
|
2
1
|
import { hotkeys } from "@valentinkolb/stdlib/solid";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
const ShortcutsDialog = (props: { openSearchHelp: () => void }) => {
|
|
7
|
-
const entries = createMemo(() =>
|
|
8
|
-
[...hotkeys.entries()].sort((a, b) => {
|
|
9
|
-
const labelSort = a.label.localeCompare(b.label);
|
|
10
|
-
return labelSort !== 0 ? labelSort : a.keys.localeCompare(b.keys);
|
|
11
|
-
}),
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<div class="flex flex-col gap-3">
|
|
16
|
-
<p class="text-xs text-dimmed leading-relaxed">
|
|
17
|
-
Use these keyboard shortcuts to work faster. The list updates automatically depending on which app or view is currently open.
|
|
18
|
-
</p>
|
|
19
|
-
<p class="text-xs text-dimmed leading-relaxed">
|
|
20
|
-
Looking for the Spotlight/search{" "}
|
|
21
|
-
<button type="button" class="text-blue-500 hover:underline dark:text-blue-400" onClick={props.openSearchHelp}>
|
|
22
|
-
help
|
|
23
|
-
</button>
|
|
24
|
-
</p>
|
|
25
|
-
|
|
26
|
-
<div class="max-h-[60vh] overflow-y-auto pr-1">
|
|
27
|
-
<div class="flex flex-col gap-2">
|
|
28
|
-
<For each={entries()}>
|
|
29
|
-
{(entry) => (
|
|
30
|
-
<div class="rounded-lg ring-1 ring-inset ring-zinc-200 dark:ring-zinc-800 p-2.5 bg-zinc-50/50 dark:bg-zinc-900/35">
|
|
31
|
-
<div class="flex items-start justify-between gap-3">
|
|
32
|
-
<div class="min-w-0">
|
|
33
|
-
<p class="text-sm font-medium text-primary truncate">{entry.label}</p>
|
|
34
|
-
<p class="text-xs text-dimmed mt-0.5">{entry.desc || "No description provided."}</p>
|
|
35
|
-
</div>
|
|
36
|
-
<div
|
|
37
|
-
class="flex items-center gap-1.5 shrink-0"
|
|
38
|
-
role="group"
|
|
39
|
-
aria-label={entry.keysPretty.map((part) => part.ariaLabel).join(" + ")}
|
|
40
|
-
>
|
|
41
|
-
<For each={entry.keysPretty}>
|
|
42
|
-
{(part) => (
|
|
43
|
-
<kbd class="inline-flex min-w-6 justify-center px-1.5 py-1 rounded-md text-[11px] leading-none font-medium ring-1 ring-inset ring-zinc-300 dark:ring-zinc-700 bg-white dark:bg-zinc-900 text-primary">
|
|
44
|
-
{part.key}
|
|
45
|
-
</kbd>
|
|
46
|
-
)}
|
|
47
|
-
</For>
|
|
48
|
-
</div>
|
|
49
|
-
</div>
|
|
50
|
-
</div>
|
|
51
|
-
)}
|
|
52
|
-
</For>
|
|
53
|
-
<Show when={entries().length === 0}>
|
|
54
|
-
<div class="rounded-lg ring-1 ring-inset ring-zinc-200 dark:ring-zinc-800 p-3 text-xs text-dimmed bg-zinc-50/50 dark:bg-zinc-900/35">
|
|
55
|
-
No shortcuts registered yet.
|
|
56
|
-
</div>
|
|
57
|
-
</Show>
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
|
|
61
|
-
</div>
|
|
62
|
-
);
|
|
63
|
-
};
|
|
2
|
+
import type { GlobalSearchHelpApp } from "./GlobalSearchHelpDialog";
|
|
3
|
+
import { openLayoutHelpDialog } from "./LayoutHelp";
|
|
64
4
|
|
|
65
5
|
/** Help action in rail nav: opens a modal with all currently registered hotkeys. */
|
|
66
6
|
export default function HotkeysHelpRail(props: { searchHelpApps?: GlobalSearchHelpApp[] }) {
|
|
67
7
|
const searchHelpApps = props.searchHelpApps ?? [];
|
|
68
8
|
|
|
69
9
|
const openHelp = () => {
|
|
70
|
-
|
|
71
|
-
close();
|
|
72
|
-
queueMicrotask(() => openGlobalSearchHelpDialog(searchHelpApps));
|
|
73
|
-
}} />, {
|
|
74
|
-
title: "Keyboard Shortcuts",
|
|
75
|
-
icon: "ti ti-keyboard",
|
|
76
|
-
size: "large",
|
|
77
|
-
});
|
|
10
|
+
openLayoutHelpDialog(searchHelpApps);
|
|
78
11
|
};
|
|
79
12
|
|
|
80
13
|
hotkeys.create(() => ({
|
package/src/ssr/Layout.tsx
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
|
-
import { hasRole, type User } from "../contracts/shared";
|
|
2
1
|
import type { JSX } from "solid-js/jsx-runtime";
|
|
3
|
-
import NavMenu from "./NavMenu.island";
|
|
4
|
-
import MoreAppsDropdown from "./MoreAppsDropdown.island";
|
|
5
|
-
import ThemeToggleRail from "./ThemeToggleRail.island";
|
|
6
|
-
import HotkeysHelpRail from "./HotkeysHelpRail.island";
|
|
7
|
-
import GlobalSearchTrigger from "./GlobalSearchTrigger.island";
|
|
8
|
-
import Footer from "./Footer.island";
|
|
9
|
-
import { dates } from "../shared";
|
|
10
|
-
import { getRuntimeContext, type RuntimeContext } from "./runtime";
|
|
11
2
|
import { resolveNavMatch } from "../contracts/app"; // ==========================
|
|
3
|
+
import { hasRole, type User } from "../contracts/shared";
|
|
4
|
+
import type { LayoutAnnouncementsState } from "../server/middleware/settings";
|
|
5
|
+
import { dates } from "../shared";
|
|
6
|
+
import { readThemeFromCookieHeader } from "../shared/theme";
|
|
7
|
+
import type { LayoutBreadcrumb } from "../ui/layout";
|
|
8
|
+
import AppLaunchpad, { type AppLaunchpadApp } from "./AppLaunchpad.island";
|
|
9
|
+
import Footer from "./Footer.island";
|
|
10
|
+
import GlobalAnnouncements from "./GlobalAnnouncements.island";
|
|
12
11
|
import type { GlobalSearchHelpApp } from "./GlobalSearchHelpDialog";
|
|
12
|
+
import GlobalSearchTrigger from "./GlobalSearchTrigger.island";
|
|
13
|
+
import HotkeysHelpRail from "./HotkeysHelpRail.island";
|
|
14
|
+
import LayoutBreadcrumbs from "./LayoutBreadcrumbs.island";
|
|
15
|
+
import NavMenu from "./NavMenu.island";
|
|
16
|
+
import { getRuntimeContext, type RuntimeContext } from "./runtime";
|
|
17
|
+
import ThemeToggleRail from "./ThemeToggleRail.island";
|
|
18
|
+
import TimezoneCookie from "./TimezoneCookie.island";
|
|
19
|
+
|
|
13
20
|
// Types
|
|
14
|
-
type Breadcrumb =
|
|
15
|
-
type AppLink = { iconClass: string; label: string; href: string; match: string };
|
|
21
|
+
type Breadcrumb = LayoutBreadcrumb;
|
|
22
|
+
type AppLink = { id: string; iconClass: string; label: string; href: string; match: string; description?: string };
|
|
16
23
|
type LayoutContext = {
|
|
17
24
|
get(key: "user"): User | undefined;
|
|
18
25
|
get(key: "page"): { theme?: "light" | "dark" };
|
|
19
26
|
get(key: "runtime"): RuntimeContext;
|
|
27
|
+
get(key: "announcements"): LayoutAnnouncementsState | undefined;
|
|
20
28
|
/**
|
|
21
29
|
* Per-request settings snapshot (populated by snapshot middleware in
|
|
22
30
|
* `_internal/define-app.ts`). Loose-typed at this layer so Layout can be
|
|
@@ -38,6 +46,8 @@ type LayoutProps = {
|
|
|
38
46
|
function active(pathname: string, match: string): string {
|
|
39
47
|
return pathname.startsWith(match) ? "active" : "";
|
|
40
48
|
}
|
|
49
|
+
const jsonScript = (value: unknown): string => JSON.stringify(value).replace(/</g, "\\u003c");
|
|
50
|
+
|
|
41
51
|
function buildNavLinks(apps: RuntimeContext["apps"], user: User | undefined): { primary: AppLink[]; more: AppLink[] } {
|
|
42
52
|
const links = apps
|
|
43
53
|
.filter((app) => !!app.nav && app.nav.section !== "hidden")
|
|
@@ -59,15 +69,24 @@ function buildNavLinks(apps: RuntimeContext["apps"], user: User | undefined): {
|
|
|
59
69
|
section: app.nav!.section,
|
|
60
70
|
link: {
|
|
61
71
|
iconClass: app.icon,
|
|
72
|
+
id: app.id,
|
|
62
73
|
label: app.name,
|
|
63
74
|
href: app.nav!.href,
|
|
64
75
|
match: resolveNavMatch(app) ?? app.nav!.href.split("?")[0] ?? app.nav!.href,
|
|
76
|
+
description: app.description,
|
|
65
77
|
} satisfies AppLink,
|
|
66
78
|
}));
|
|
67
79
|
const primary = links.filter((entry) => entry.section === "primary").map((entry) => entry.link);
|
|
68
80
|
const more = links.filter((entry) => entry.section === "more").map((entry) => entry.link);
|
|
69
81
|
if (user && hasRole(user, "admin")) {
|
|
70
|
-
more.push({
|
|
82
|
+
more.push({
|
|
83
|
+
id: "admin",
|
|
84
|
+
iconClass: "ti ti-settings",
|
|
85
|
+
label: "Admin",
|
|
86
|
+
href: "/admin",
|
|
87
|
+
match: "/admin",
|
|
88
|
+
description: "Platform administration.",
|
|
89
|
+
});
|
|
71
90
|
}
|
|
72
91
|
return { primary, more };
|
|
73
92
|
} // ==========================
|
|
@@ -81,9 +100,9 @@ function ProfileWarnings({ user }: { user: User }) {
|
|
|
81
100
|
if (!user.sn) missing.push("last name");
|
|
82
101
|
if (missing.length === 0) return null;
|
|
83
102
|
return (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
103
|
+
<a href="/me" class="flex items-center gap-2 text-xs info-block-warning no-underline mb-2 md:mb-1.5 mx-2 md:ml-0 md:mr-1.5">
|
|
104
|
+
<i class="ti ti-user-exclamation" /> <span>Your profile is incomplete: {missing.join(",")} not set.</span>
|
|
105
|
+
</a>
|
|
87
106
|
);
|
|
88
107
|
}
|
|
89
108
|
function ExpiryWarnings({ user }: { user: User }) {
|
|
@@ -122,41 +141,23 @@ function ExpiryWarnings({ user }: { user: User }) {
|
|
|
122
141
|
);
|
|
123
142
|
} // ==========================
|
|
124
143
|
// Sub-Components
|
|
125
|
-
|
|
126
|
-
return (
|
|
127
|
-
<nav class="flex items-center gap-1 sm:gap-2 min-w-0 text-sm md:text-xs">
|
|
128
|
-
{" "}
|
|
129
|
-
{breadcrumbs.map((crumb, i) => {
|
|
130
|
-
const isLast = i === breadcrumbs.length - 1;
|
|
131
|
-
return (
|
|
132
|
-
<>
|
|
133
|
-
{" "}
|
|
134
|
-
{i > 0 && <span class="text-zinc-400 dark:text-zinc-600 text-xs">/</span>}{" "}
|
|
135
|
-
{crumb.href && !isLast ? (
|
|
136
|
-
<a href={crumb.href} class="text-dimmed hover:text-primary truncate">
|
|
137
|
-
{" "}
|
|
138
|
-
{crumb.title}{" "}
|
|
139
|
-
</a>
|
|
140
|
-
) : (
|
|
141
|
-
<span class="font-semibold text-primary truncate">{crumb.title}</span>
|
|
142
|
-
)}{" "}
|
|
143
|
-
</>
|
|
144
|
-
);
|
|
145
|
-
})}{" "}
|
|
146
|
-
</nav>
|
|
147
|
-
);
|
|
148
|
-
} // ==========================
|
|
144
|
+
// ==========================
|
|
149
145
|
// Main Layout
|
|
150
146
|
export default function Layout({ children, c, title, fullPage, fullWidth }: LayoutProps) {
|
|
151
147
|
const runtime = getRuntimeContext(c);
|
|
152
148
|
const cookie = c.req.raw.headers.get("Cookie") ?? "";
|
|
153
|
-
|
|
154
|
-
c.get("page").theme = themeMatch?.[1] === "dark" ? "dark" : "light";
|
|
149
|
+
c.get("page").theme = readThemeFromCookieHeader(cookie);
|
|
155
150
|
const user = c.get("user");
|
|
156
151
|
const pathname = new URL(c.req.raw.url).pathname;
|
|
157
152
|
const { primary: primaryApps, more: moreApps } = buildNavLinks(runtime.apps, user);
|
|
158
153
|
const allApps = [...primaryApps, ...moreApps];
|
|
159
|
-
const
|
|
154
|
+
const launchpadApps: AppLaunchpadApp[] = allApps.map((app) => ({
|
|
155
|
+
id: app.id,
|
|
156
|
+
iconClass: app.iconClass,
|
|
157
|
+
label: app.label,
|
|
158
|
+
href: app.href,
|
|
159
|
+
description: app.description,
|
|
160
|
+
}));
|
|
160
161
|
const searchHelpApps: GlobalSearchHelpApp[] = runtime.apps
|
|
161
162
|
.filter((app) => (app.searchTags?.length ?? 0) > 0)
|
|
162
163
|
.map((app) => ({
|
|
@@ -169,6 +170,7 @@ export default function Layout({ children, c, title, fullPage, fullWidth }: Layo
|
|
|
169
170
|
}))
|
|
170
171
|
.sort((a, b) => a.appName.localeCompare(b.appName));
|
|
171
172
|
const settings = c.get("settings");
|
|
173
|
+
const announcements = c.get("announcements");
|
|
172
174
|
const appName = settings?.app?.name || "Cloud";
|
|
173
175
|
// Project the user record down to what NavMenu actually renders. Without
|
|
174
176
|
// this, the full `User` (mail, ssh keys, phone, address, all group
|
|
@@ -185,13 +187,15 @@ export default function Layout({ children, c, title, fullPage, fullWidth }: Layo
|
|
|
185
187
|
// Aggregate legalLinks from every running app (last-wins on duplicate href).
|
|
186
188
|
const legalLinks = (() => {
|
|
187
189
|
const seen = new Map<string, { label: string; href: string; icon?: string }>();
|
|
190
|
+
if (user) seen.set("/me", { label: "Profile", href: "/me", icon: "ti ti-user-circle" });
|
|
188
191
|
for (const app of runtime.apps) {
|
|
189
192
|
for (const link of app.legalLinks ?? []) seen.set(link.href, { ...link });
|
|
190
193
|
}
|
|
191
194
|
return [...seen.values()];
|
|
192
195
|
})();
|
|
193
196
|
const page = c.get("page") as Record<string, unknown>;
|
|
194
|
-
|
|
197
|
+
const pageTitle = typeof title === "string" ? title : (title?.at(-1)?.title ?? appName);
|
|
198
|
+
if (!page.title) page.title = pageTitle;
|
|
195
199
|
const breadcrumbs: Breadcrumb[] = !title ? [{ title: appName }] : typeof title === "string" ? [{ title }] : title;
|
|
196
200
|
const showRail =
|
|
197
201
|
!!user; /* * Grid layout: * Rail mode: [rail | content] * No rail: [content] * * Rows: [header] [main] [footer?] * The rail spans rows 1+2 via grid-row, so logo aligns with the header. */
|
|
@@ -200,10 +204,14 @@ export default function Layout({ children, c, title, fullPage, fullWidth }: Layo
|
|
|
200
204
|
? "grid-cols-1 md:grid-cols-[auto_1fr] grid-rows-[auto_1fr]"
|
|
201
205
|
: `grid-cols-1 ${!fullPage ? "grid-rows-[auto_1fr_auto]" : "grid-rows-[auto_1fr]"}`;
|
|
202
206
|
return (
|
|
203
|
-
<div
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
{
|
|
207
|
+
<div class={`grid min-h-screen w-screen relative md:h-screen md:overflow-hidden bg-zinc-50 dark:bg-zinc-950 ${gridClass}`}>
|
|
208
|
+
<TimezoneCookie />
|
|
209
|
+
{showRail && <AppLaunchpad apps={launchpadApps} legalLinks={legalLinks} />}
|
|
210
|
+
{showRail && (
|
|
211
|
+
<script id="cloud-app-launchpad-data" type="application/json">
|
|
212
|
+
{jsonScript({ apps: launchpadApps, legalLinks })}
|
|
213
|
+
</script>
|
|
214
|
+
)}{" "}
|
|
207
215
|
{/* ── Rail: logo cell (row 1, col 1) — grid gives it the same height as the header ── */}{" "}
|
|
208
216
|
{showRail && (
|
|
209
217
|
<div class="hidden md:flex items-center justify-center w-12 bg-white/20 dark:bg-zinc-950/20">
|
|
@@ -241,24 +249,18 @@ export default function Layout({ children, c, title, fullPage, fullWidth }: Layo
|
|
|
241
249
|
{/* Breadcrumbs — desktop, rail mode only */}{" "}
|
|
242
250
|
<div class="hidden md:flex items-center min-w-0">
|
|
243
251
|
{" "}
|
|
244
|
-
<
|
|
252
|
+
<LayoutBreadcrumbs breadcrumbs={breadcrumbs} />{" "}
|
|
245
253
|
</div>{" "}
|
|
246
254
|
{/* Mobile breadcrumb */}{" "}
|
|
247
255
|
<div class="md:hidden flex items-center min-w-0">
|
|
248
256
|
{" "}
|
|
249
|
-
<
|
|
257
|
+
<LayoutBreadcrumbs breadcrumbs={breadcrumbs} mobile />{" "}
|
|
250
258
|
</div>{" "}
|
|
251
259
|
</div>{" "}
|
|
252
260
|
<div class="flex items-center shrink-0 gap-1">
|
|
253
261
|
{user && (
|
|
254
|
-
<GlobalSearchTrigger
|
|
255
|
-
|
|
256
|
-
registerHotkey
|
|
257
|
-
class={showRail ? "md:hidden" : ""}
|
|
258
|
-
searchHelpApps={searchHelpApps}
|
|
259
|
-
/>
|
|
260
|
-
)}
|
|
261
|
-
{" "}
|
|
262
|
+
<GlobalSearchTrigger variant="header" registerHotkey class={showRail ? "md:hidden" : ""} searchHelpApps={searchHelpApps} />
|
|
263
|
+
)}{" "}
|
|
262
264
|
{/* Desktop: direct /me link with avatar (logged in) or NavMenu (not logged in) */}{" "}
|
|
263
265
|
{user ? (
|
|
264
266
|
<>
|
|
@@ -272,11 +274,13 @@ export default function Layout({ children, c, title, fullPage, fullWidth }: Layo
|
|
|
272
274
|
</a>{" "}
|
|
273
275
|
<div class="md:hidden">
|
|
274
276
|
{" "}
|
|
275
|
-
<
|
|
277
|
+
<div class="flex items-center gap-1">
|
|
278
|
+
<AppLaunchpad apps={launchpadApps} legalLinks={legalLinks} variant="header" label="Open apps" />
|
|
279
|
+
</div>{" "}
|
|
276
280
|
</div>{" "}
|
|
277
281
|
</>
|
|
278
282
|
) : (
|
|
279
|
-
<NavMenu user={navMenuUser}
|
|
283
|
+
<NavMenu user={navMenuUser} />
|
|
280
284
|
)}{" "}
|
|
281
285
|
</div>{" "}
|
|
282
286
|
</header>{" "}
|
|
@@ -290,12 +294,10 @@ export default function Layout({ children, c, title, fullPage, fullWidth }: Layo
|
|
|
290
294
|
<i class={`${app.iconClass} text-base`} />{" "}
|
|
291
295
|
</a>
|
|
292
296
|
))}{" "}
|
|
293
|
-
<
|
|
297
|
+
<AppLaunchpad apps={launchpadApps} legalLinks={legalLinks} variant="rail" label="Open apps" />
|
|
294
298
|
<div class="mt-auto pb-1 flex flex-col items-center gap-1">
|
|
295
299
|
{" "}
|
|
296
|
-
<GlobalSearchTrigger variant="rail" searchHelpApps={searchHelpApps} />{" "}
|
|
297
|
-
{" "}
|
|
298
|
-
<HotkeysHelpRail searchHelpApps={searchHelpApps} />{" "}
|
|
300
|
+
<GlobalSearchTrigger variant="rail" searchHelpApps={searchHelpApps} /> <HotkeysHelpRail searchHelpApps={searchHelpApps} />{" "}
|
|
299
301
|
<ThemeToggleRail />{" "}
|
|
300
302
|
</div>{" "}
|
|
301
303
|
</div>
|
|
@@ -303,10 +305,16 @@ export default function Layout({ children, c, title, fullPage, fullWidth }: Layo
|
|
|
303
305
|
{/* ── Main content (row 2) ── */}{" "}
|
|
304
306
|
<div class="flex flex-col min-h-0 min-w-0 bg-zinc-50 dark:bg-zinc-950">
|
|
305
307
|
{" "}
|
|
308
|
+
{user && announcements && (
|
|
309
|
+
<GlobalAnnouncements
|
|
310
|
+
banners={announcements.banners}
|
|
311
|
+
announcements={announcements.announcements}
|
|
312
|
+
latestAnnouncementVersion={announcements.latestAnnouncementVersion}
|
|
313
|
+
cookieState={announcements.cookieState}
|
|
314
|
+
/>
|
|
315
|
+
)}{" "}
|
|
306
316
|
{user && <ProfileWarnings user={user} />} {user && <ExpiryWarnings user={user} />}{" "}
|
|
307
|
-
<main
|
|
308
|
-
class={`flex-1 min-h-0 ${contentPadding} ${fullPage || fullWidth ? "md:overflow-hidden flex flex-col" : "md:overflow-auto"}`}
|
|
309
|
-
>
|
|
317
|
+
<main class={`flex-1 min-h-0 ${contentPadding} ${fullPage || fullWidth ? "md:overflow-hidden flex flex-col" : "md:overflow-auto"}`}>
|
|
310
318
|
{" "}
|
|
311
319
|
{children}{" "}
|
|
312
320
|
</main>{" "}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createSignal, For, onCleanup, onMount } from "solid-js";
|
|
2
|
+
import { LAYOUT_UPDATE_EVENT, type LayoutBreadcrumb, type LayoutUpdate } from "../ui/layout";
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
breadcrumbs: LayoutBreadcrumb[];
|
|
6
|
+
mobile?: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default function LayoutBreadcrumbs(props: Props) {
|
|
10
|
+
const [breadcrumbs, setBreadcrumbs] = createSignal(props.breadcrumbs);
|
|
11
|
+
const visibleBreadcrumbs = () => (props.mobile ? breadcrumbs().slice(-1) : breadcrumbs());
|
|
12
|
+
|
|
13
|
+
onMount(() => {
|
|
14
|
+
const onUpdate = (event: Event) => {
|
|
15
|
+
const detail = (event as CustomEvent<LayoutUpdate>).detail;
|
|
16
|
+
if (detail.breadcrumbs) setBreadcrumbs(detail.breadcrumbs);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
window.addEventListener(LAYOUT_UPDATE_EVENT, onUpdate);
|
|
20
|
+
onCleanup(() => window.removeEventListener(LAYOUT_UPDATE_EVENT, onUpdate));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<nav class="flex items-center gap-1 sm:gap-2 min-w-0 text-sm md:text-xs">
|
|
25
|
+
<For each={visibleBreadcrumbs()}>
|
|
26
|
+
{(crumb, i) => {
|
|
27
|
+
const isLast = () => i() === visibleBreadcrumbs().length - 1;
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
{i() > 0 && <span class="text-zinc-400 dark:text-zinc-600 text-xs">/</span>}
|
|
31
|
+
{crumb.href && !isLast() ? (
|
|
32
|
+
<a href={crumb.href} class="text-dimmed hover:text-primary truncate">
|
|
33
|
+
{crumb.title}
|
|
34
|
+
</a>
|
|
35
|
+
) : (
|
|
36
|
+
<span class="font-semibold text-primary truncate">{crumb.title}</span>
|
|
37
|
+
)}
|
|
38
|
+
</>
|
|
39
|
+
);
|
|
40
|
+
}}
|
|
41
|
+
</For>
|
|
42
|
+
</nav>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { children, createEffect, createMemo, createSignal, For, onCleanup, onMount, Show, type JSX } from "solid-js";
|
|
2
|
+
import { hotkeys } from "@valentinkolb/stdlib/solid";
|
|
3
|
+
import { prompts } from "../ui";
|
|
4
|
+
import { openGlobalSearchHelpDialog, type GlobalSearchHelpApp } from "./GlobalSearchHelpDialog";
|
|
5
|
+
|
|
6
|
+
export type LayoutHelpTab = {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
icon?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
order?: number;
|
|
12
|
+
children: JSX.Element;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type LayoutHelpProps = LayoutHelpTab;
|
|
16
|
+
|
|
17
|
+
const HELP_TABS_EVENT = "cloud:layout-help-tabs";
|
|
18
|
+
const LAST_TAB_KEY = "cloud.layoutHelp.activeTab";
|
|
19
|
+
|
|
20
|
+
declare global {
|
|
21
|
+
interface Window {
|
|
22
|
+
__cloudLayoutHelpTabs?: Map<string, LayoutHelpTab>;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const getRegistry = () => {
|
|
27
|
+
if (typeof window === "undefined") return null;
|
|
28
|
+
window.__cloudLayoutHelpTabs ??= new Map<string, LayoutHelpTab>();
|
|
29
|
+
return window.__cloudLayoutHelpTabs;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const emitTabsChanged = () => {
|
|
33
|
+
if (typeof window !== "undefined") window.dispatchEvent(new Event(HELP_TABS_EVENT));
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const readLastTab = () => {
|
|
37
|
+
if (typeof window === "undefined") return null;
|
|
38
|
+
try {
|
|
39
|
+
return window.localStorage.getItem(LAST_TAB_KEY);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const writeLastTab = (id: string) => {
|
|
46
|
+
if (typeof window === "undefined") return;
|
|
47
|
+
try {
|
|
48
|
+
window.localStorage.setItem(LAST_TAB_KEY, id);
|
|
49
|
+
} catch {
|
|
50
|
+
// Help still works if localStorage is blocked.
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const registeredTabs = () => {
|
|
55
|
+
const registry = getRegistry();
|
|
56
|
+
if (!registry) return [];
|
|
57
|
+
return [...registry.values()].sort((a, b) => (a.order ?? 100) - (b.order ?? 100) || a.title.localeCompare(b.title));
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const iconClass = (icon?: string) => (icon?.startsWith("ti ") ? icon : `ti ${icon ?? "ti-circle"}`);
|
|
61
|
+
|
|
62
|
+
export function registerLayoutHelpTab(tab: LayoutHelpTab) {
|
|
63
|
+
const registry = getRegistry();
|
|
64
|
+
if (!registry) return () => {};
|
|
65
|
+
registry.set(tab.id, tab);
|
|
66
|
+
emitTabsChanged();
|
|
67
|
+
return () => {
|
|
68
|
+
if (registry.get(tab.id) === tab) {
|
|
69
|
+
registry.delete(tab.id);
|
|
70
|
+
emitTabsChanged();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function LayoutHelp(props: LayoutHelpProps) {
|
|
76
|
+
const resolved = children(() => props.children);
|
|
77
|
+
|
|
78
|
+
onMount(() => {
|
|
79
|
+
const dispose = registerLayoutHelpTab({
|
|
80
|
+
id: props.id,
|
|
81
|
+
title: props.title,
|
|
82
|
+
icon: props.icon,
|
|
83
|
+
description: props.description,
|
|
84
|
+
order: props.order,
|
|
85
|
+
children: resolved(),
|
|
86
|
+
});
|
|
87
|
+
onCleanup(dispose);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const ShortcutsHelp = (props: { openSearchHelp: () => void }) => {
|
|
94
|
+
const entries = createMemo(() =>
|
|
95
|
+
[...hotkeys.entries()].sort((a, b) => {
|
|
96
|
+
const labelSort = a.label.localeCompare(b.label);
|
|
97
|
+
return labelSort !== 0 ? labelSort : a.keys.localeCompare(b.keys);
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div class="space-y-4">
|
|
103
|
+
<div class="info-block-info flex items-start gap-2 text-xs">
|
|
104
|
+
<i class="ti ti-info-circle mt-0.5 shrink-0" />
|
|
105
|
+
<span>Shortcuts change with the current app and view. This list updates automatically.</span>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
class="inline-flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-xs font-medium text-blue-600 hover:bg-blue-50 dark:text-blue-300 dark:hover:bg-blue-950/40"
|
|
111
|
+
onClick={props.openSearchHelp}
|
|
112
|
+
>
|
|
113
|
+
<i class="ti ti-search text-sm" />
|
|
114
|
+
Show search help
|
|
115
|
+
</button>
|
|
116
|
+
|
|
117
|
+
<div class="flex flex-col gap-2">
|
|
118
|
+
<For each={entries()}>
|
|
119
|
+
{(entry) => (
|
|
120
|
+
<div class="rounded-lg bg-zinc-50/80 p-2.5 ring-1 ring-inset ring-zinc-200 dark:bg-zinc-900/45 dark:ring-zinc-800">
|
|
121
|
+
<div class="flex items-start justify-between gap-3">
|
|
122
|
+
<div class="min-w-0">
|
|
123
|
+
<p class="truncate text-sm font-medium text-primary">{entry.label}</p>
|
|
124
|
+
<p class="mt-0.5 text-xs text-dimmed">{entry.desc || "No description provided."}</p>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="flex shrink-0 items-center gap-1.5" role="group" aria-label={entry.keysPretty.map((part) => part.ariaLabel).join(" + ")}>
|
|
127
|
+
<For each={entry.keysPretty}>
|
|
128
|
+
{(part) => (
|
|
129
|
+
<kbd class="inline-flex min-w-6 justify-center rounded-md bg-white px-1.5 py-1 text-[11px] font-medium leading-none text-primary ring-1 ring-inset ring-zinc-300 dark:bg-zinc-950 dark:ring-zinc-700">
|
|
130
|
+
{part.key}
|
|
131
|
+
</kbd>
|
|
132
|
+
)}
|
|
133
|
+
</For>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
</For>
|
|
139
|
+
<Show when={entries().length === 0}>
|
|
140
|
+
<div class="rounded-lg bg-zinc-50/80 p-3 text-xs text-dimmed ring-1 ring-inset ring-zinc-200 dark:bg-zinc-900/45 dark:ring-zinc-800">
|
|
141
|
+
No shortcuts registered yet.
|
|
142
|
+
</div>
|
|
143
|
+
</Show>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const LayoutHelpDialog = (props: { close: () => void; searchHelpApps: GlobalSearchHelpApp[] }) => {
|
|
150
|
+
const [externalTabs, setExternalTabs] = createSignal(registeredTabs());
|
|
151
|
+
const allTabs = createMemo<LayoutHelpTab[]>(() => [
|
|
152
|
+
{
|
|
153
|
+
id: "shortcuts",
|
|
154
|
+
title: "Shortcuts",
|
|
155
|
+
icon: "ti ti-keyboard",
|
|
156
|
+
description: "Keyboard actions for the current page.",
|
|
157
|
+
order: 0,
|
|
158
|
+
children: (
|
|
159
|
+
<ShortcutsHelp
|
|
160
|
+
openSearchHelp={() => {
|
|
161
|
+
props.close();
|
|
162
|
+
queueMicrotask(() => openGlobalSearchHelpDialog(props.searchHelpApps));
|
|
163
|
+
}}
|
|
164
|
+
/>
|
|
165
|
+
),
|
|
166
|
+
},
|
|
167
|
+
...externalTabs(),
|
|
168
|
+
]);
|
|
169
|
+
const initialTab = () => {
|
|
170
|
+
const last = readLastTab();
|
|
171
|
+
return allTabs().some((tab) => tab.id === last) ? last! : (allTabs()[0]?.id ?? "shortcuts");
|
|
172
|
+
};
|
|
173
|
+
const [activeId, setActiveId] = createSignal(initialTab());
|
|
174
|
+
|
|
175
|
+
onMount(() => {
|
|
176
|
+
const update = () => setExternalTabs(registeredTabs());
|
|
177
|
+
window.addEventListener(HELP_TABS_EVENT, update);
|
|
178
|
+
update();
|
|
179
|
+
onCleanup(() => window.removeEventListener(HELP_TABS_EVENT, update));
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
createEffect(() => {
|
|
183
|
+
const tabs = allTabs();
|
|
184
|
+
if (!tabs.some((tab) => tab.id === activeId())) {
|
|
185
|
+
const last = readLastTab();
|
|
186
|
+
setActiveId(tabs.some((tab) => tab.id === last) ? last! : (tabs[0]?.id ?? "shortcuts"));
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const selectTab = (id: string) => {
|
|
191
|
+
setActiveId(id);
|
|
192
|
+
writeLastTab(id);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div class="flex h-[min(90vh,52rem)] w-full flex-col gap-3">
|
|
197
|
+
<div class="paper flex items-center justify-between gap-4 px-5 py-4">
|
|
198
|
+
<div class="flex min-w-0 items-center gap-3">
|
|
199
|
+
<div class="flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-blue-500 text-white">
|
|
200
|
+
<i class="ti ti-help text-xl" />
|
|
201
|
+
</div>
|
|
202
|
+
<div class="min-w-0">
|
|
203
|
+
<h2 class="truncate text-lg font-semibold text-primary">Help</h2>
|
|
204
|
+
<p class="truncate text-sm text-dimmed">Shortcuts, app help, and guides.</p>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
<button type="button" class="icon-btn ml-auto shrink-0" onClick={props.close} aria-label="Close help">
|
|
208
|
+
<i class="ti ti-x" />
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div class="grid min-h-0 flex-1 gap-3 md:grid-cols-[14rem_1fr]">
|
|
213
|
+
<nav class="paper flex gap-1 overflow-x-auto p-2 md:min-h-0 md:flex-col md:overflow-visible" aria-label="Help topics">
|
|
214
|
+
<For each={allTabs()}>
|
|
215
|
+
{(tab) => {
|
|
216
|
+
const active = () => tab.id === activeId();
|
|
217
|
+
return (
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
class={`flex min-w-40 items-center gap-2 rounded-lg px-3 py-2 text-left text-sm transition md:min-w-0 ${
|
|
221
|
+
active()
|
|
222
|
+
? "bg-blue-50 text-blue-600 dark:bg-blue-950/45 dark:text-blue-300"
|
|
223
|
+
: "text-dimmed hover:bg-zinc-100 hover:text-primary dark:hover:bg-zinc-900"
|
|
224
|
+
}`}
|
|
225
|
+
onClick={() => selectTab(tab.id)}
|
|
226
|
+
>
|
|
227
|
+
<i class={`${iconClass(tab.icon)} shrink-0 text-base`} />
|
|
228
|
+
<span class="min-w-0 flex-1 truncate">{tab.title}</span>
|
|
229
|
+
</button>
|
|
230
|
+
);
|
|
231
|
+
}}
|
|
232
|
+
</For>
|
|
233
|
+
</nav>
|
|
234
|
+
|
|
235
|
+
<section class="paper min-h-0 overflow-hidden">
|
|
236
|
+
<For each={allTabs()}>
|
|
237
|
+
{(tab) => (
|
|
238
|
+
<div class={`${tab.id === activeId() ? "block" : "hidden"} h-full overflow-y-auto px-5 py-5 pr-4`}>
|
|
239
|
+
<div class="mb-5 flex items-start gap-3">
|
|
240
|
+
<div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-zinc-100 text-zinc-600 dark:bg-zinc-900 dark:text-zinc-300">
|
|
241
|
+
<i class={`${iconClass(tab.icon)} text-lg`} />
|
|
242
|
+
</div>
|
|
243
|
+
<div class="min-w-0">
|
|
244
|
+
<h3 class="text-base font-semibold text-primary">{tab.title}</h3>
|
|
245
|
+
<Show when={tab.description}>
|
|
246
|
+
<p class="mt-0.5 text-sm text-dimmed">{tab.description}</p>
|
|
247
|
+
</Show>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
{tab.children}
|
|
251
|
+
</div>
|
|
252
|
+
)}
|
|
253
|
+
</For>
|
|
254
|
+
</section>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
export function openLayoutHelpDialog(searchHelpApps: GlobalSearchHelpApp[] = []) {
|
|
261
|
+
void prompts.dialog<void>((close) => <LayoutHelpDialog close={close} searchHelpApps={searchHelpApps} />, {
|
|
262
|
+
surface: "bare",
|
|
263
|
+
header: false,
|
|
264
|
+
size: "wide",
|
|
265
|
+
});
|
|
266
|
+
}
|