@creezio/shell-ui 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +336 -0
- package/dist/adapters/nav-shell.d.ts +54 -0
- package/dist/adapters/nav-shell.d.ts.map +1 -0
- package/dist/adapters/nav-shell.js +112 -0
- package/dist/adapters/nav-shell.js.map +1 -0
- package/dist/brand.d.ts +64 -0
- package/dist/brand.d.ts.map +1 -0
- package/dist/brand.js +59 -0
- package/dist/brand.js.map +1 -0
- package/dist/core-nav.d.ts +17 -0
- package/dist/core-nav.d.ts.map +1 -0
- package/dist/core-nav.js +40 -0
- package/dist/core-nav.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api-scopes.d.ts +24 -0
- package/dist/lib/api-scopes.d.ts.map +1 -0
- package/dist/lib/api-scopes.js +78 -0
- package/dist/lib/api-scopes.js.map +1 -0
- package/dist/lib/catalog-suspense-key.d.ts +3 -0
- package/dist/lib/catalog-suspense-key.d.ts.map +1 -0
- package/dist/lib/catalog-suspense-key.js +12 -0
- package/dist/lib/catalog-suspense-key.js.map +1 -0
- package/dist/lib/data-changed.d.ts +51 -0
- package/dist/lib/data-changed.d.ts.map +1 -0
- package/dist/lib/data-changed.js +227 -0
- package/dist/lib/data-changed.js.map +1 -0
- package/dist/lib/desktop-home-path.d.ts +14 -0
- package/dist/lib/desktop-home-path.d.ts.map +1 -0
- package/dist/lib/desktop-home-path.js +29 -0
- package/dist/lib/desktop-home-path.js.map +1 -0
- package/dist/lib/geo-distance.d.ts +6 -0
- package/dist/lib/geo-distance.d.ts.map +1 -0
- package/dist/lib/geo-distance.js +27 -0
- package/dist/lib/geo-distance.js.map +1 -0
- package/dist/lib/img.d.ts +2 -0
- package/dist/lib/img.d.ts.map +1 -0
- package/dist/lib/img.js +40 -0
- package/dist/lib/img.js.map +1 -0
- package/dist/lib/keepalive-eviction.d.ts +14 -0
- package/dist/lib/keepalive-eviction.d.ts.map +1 -0
- package/dist/lib/keepalive-eviction.js +44 -0
- package/dist/lib/keepalive-eviction.js.map +1 -0
- package/dist/lib/ops-track.d.ts +29 -0
- package/dist/lib/ops-track.d.ts.map +1 -0
- package/dist/lib/ops-track.js +37 -0
- package/dist/lib/ops-track.js.map +1 -0
- package/dist/lib/optimize-cover-url.d.ts +10 -0
- package/dist/lib/optimize-cover-url.d.ts.map +1 -0
- package/dist/lib/optimize-cover-url.js +80 -0
- package/dist/lib/optimize-cover-url.js.map +1 -0
- package/dist/lib/page-trails.d.ts +9 -0
- package/dist/lib/page-trails.d.ts.map +1 -0
- package/dist/lib/page-trails.js +20 -0
- package/dist/lib/page-trails.js.map +1 -0
- package/dist/lib/public-origin.d.ts +32 -0
- package/dist/lib/public-origin.d.ts.map +1 -0
- package/dist/lib/public-origin.js +125 -0
- package/dist/lib/public-origin.js.map +1 -0
- package/dist/lib/server-incident.d.ts +12 -0
- package/dist/lib/server-incident.d.ts.map +1 -0
- package/dist/lib/server-incident.js +72 -0
- package/dist/lib/server-incident.js.map +1 -0
- package/dist/lib/tab-document-url.d.ts +13 -0
- package/dist/lib/tab-document-url.d.ts.map +1 -0
- package/dist/lib/tab-document-url.js +60 -0
- package/dist/lib/tab-document-url.js.map +1 -0
- package/dist/lib/utils.d.ts +20 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +87 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/nav-catalog.d.ts +97 -0
- package/dist/nav-catalog.d.ts.map +1 -0
- package/dist/nav-catalog.js +282 -0
- package/dist/nav-catalog.js.map +1 -0
- package/dist/registry.d.ts +11 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +60 -0
- package/dist/registry.js.map +1 -0
- package/dist/types.d.ts +22 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist-cjs/adapters/nav-shell.js +115 -0
- package/dist-cjs/adapters/nav-shell.js.map +1 -0
- package/dist-cjs/brand.js +66 -0
- package/dist-cjs/brand.js.map +1 -0
- package/dist-cjs/core-nav.js +43 -0
- package/dist-cjs/core-nav.js.map +1 -0
- package/dist-cjs/index.js +95 -0
- package/dist-cjs/index.js.map +1 -0
- package/dist-cjs/lib/api-scopes.js +86 -0
- package/dist-cjs/lib/api-scopes.js.map +1 -0
- package/dist-cjs/lib/catalog-suspense-key.js +15 -0
- package/dist-cjs/lib/catalog-suspense-key.js.map +1 -0
- package/dist-cjs/lib/data-changed.js +236 -0
- package/dist-cjs/lib/data-changed.js.map +1 -0
- package/dist-cjs/lib/desktop-home-path.js +34 -0
- package/dist-cjs/lib/desktop-home-path.js.map +1 -0
- package/dist-cjs/lib/geo-distance.js +30 -0
- package/dist-cjs/lib/geo-distance.js.map +1 -0
- package/dist-cjs/lib/img.js +43 -0
- package/dist-cjs/lib/img.js.map +1 -0
- package/dist-cjs/lib/keepalive-eviction.js +49 -0
- package/dist-cjs/lib/keepalive-eviction.js.map +1 -0
- package/dist-cjs/lib/ops-track.js +41 -0
- package/dist-cjs/lib/ops-track.js.map +1 -0
- package/dist-cjs/lib/optimize-cover-url.js +83 -0
- package/dist-cjs/lib/optimize-cover-url.js.map +1 -0
- package/dist-cjs/lib/page-trails.js +25 -0
- package/dist-cjs/lib/page-trails.js.map +1 -0
- package/dist-cjs/lib/public-origin.js +130 -0
- package/dist-cjs/lib/public-origin.js.map +1 -0
- package/dist-cjs/lib/server-incident.js +75 -0
- package/dist-cjs/lib/server-incident.js.map +1 -0
- package/dist-cjs/lib/tab-document-url.js +65 -0
- package/dist-cjs/lib/tab-document-url.js.map +1 -0
- package/dist-cjs/lib/utils.js +97 -0
- package/dist-cjs/lib/utils.js.map +1 -0
- package/dist-cjs/nav-catalog.js +293 -0
- package/dist-cjs/nav-catalog.js.map +1 -0
- package/dist-cjs/package.json +3 -0
- package/dist-cjs/registry.js +64 -0
- package/dist-cjs/registry.js.map +1 -0
- package/dist-cjs/types.js +3 -0
- package/dist-cjs/types.js.map +1 -0
- package/package.json +163 -0
- package/ui/app-error-boundary.tsx +59 -0
- package/ui/data-table.tsx +121 -0
- package/ui/desktop/auth-window-chrome.tsx +51 -0
- package/ui/desktop/desktop-bridge.tsx +87 -0
- package/ui/desktop/external-site-slot.tsx +313 -0
- package/ui/desktop/external-site-surface.ts +58 -0
- package/ui/desktop/site-link.tsx +105 -0
- package/ui/desktop/window-chrome-controls.tsx +5 -0
- package/ui/desktop-types.ts +30 -0
- package/ui/faceted-filters.tsx +119 -0
- package/ui/global-search-host.ts +20 -0
- package/ui/global-search.tsx +66 -0
- package/ui/index.ts +178 -0
- package/ui/kit.ts +30 -0
- package/ui/layout/app-shell.tsx +58 -0
- package/ui/layout/desktop-update-banner.tsx +100 -0
- package/ui/layout/entity-header.tsx +44 -0
- package/ui/layout/native-os-nav.ts +104 -0
- package/ui/layout/nav-catalog-loader.tsx +98 -0
- package/ui/layout/nav-icons.ts +67 -0
- package/ui/layout/page-chrome.tsx +90 -0
- package/ui/layout/page-toolbar-context.tsx +82 -0
- package/ui/layout/sandbox-banner.tsx +38 -0
- package/ui/layout/section-view-shell.tsx +73 -0
- package/ui/layout/sidebar-host.ts +144 -0
- package/ui/layout/sidebar.tsx +1150 -0
- package/ui/lib/ai-screencast-hub.ts +127 -0
- package/ui/lib/ai-workspace-client.ts +46 -0
- package/ui/lib/aid.ts +22 -0
- package/ui/lib/desktop-host.ts +114 -0
- package/ui/lib/fleet-tracker-client.ts +237 -0
- package/ui/lib/hermes-ui.ts +114 -0
- package/ui/lib/n8n-ui.ts +115 -0
- package/ui/lib/use-creezio-resource.tsx +121 -0
- package/ui/lib/use-shell-ui-brand.ts +22 -0
- package/ui/list-toolbar.tsx +309 -0
- package/ui/os-pages/desktop-settings-page.tsx +158 -0
- package/ui/os-pages/index.ts +8 -0
- package/ui/page-loading/entity-page-loading.tsx +59 -0
- package/ui/page-loading/list-page-loading.tsx +61 -0
- package/ui/pagination.tsx +56 -0
- package/ui/primitives/avatar.tsx +33 -0
- package/ui/primitives/badge.tsx +36 -0
- package/ui/primitives/breadcrumb.tsx +93 -0
- package/ui/primitives/button.tsx +44 -0
- package/ui/primitives/card.tsx +44 -0
- package/ui/primitives/chart.tsx +317 -0
- package/ui/primitives/command.tsx +111 -0
- package/ui/primitives/dialog.tsx +100 -0
- package/ui/primitives/dropdown-menu.tsx +187 -0
- package/ui/primitives/input.tsx +19 -0
- package/ui/primitives/label.tsx +17 -0
- package/ui/primitives/resizable.tsx +45 -0
- package/ui/primitives/scroll-area.tsx +41 -0
- package/ui/primitives/select.tsx +73 -0
- package/ui/primitives/separator.tsx +26 -0
- package/ui/primitives/sheet.tsx +90 -0
- package/ui/primitives/skeleton.tsx +7 -0
- package/ui/primitives/sonner.tsx +22 -0
- package/ui/primitives/tabs.tsx +51 -0
- package/ui/primitives/textarea.tsx +22 -0
- package/ui/primitives/tooltip.tsx +31 -0
- package/ui/pwa/client-error-reporter.tsx +62 -0
- package/ui/pwa/register-sw.tsx +50 -0
- package/ui/range-filters.tsx +174 -0
- package/ui/search/global-search-config.ts +32 -0
- package/ui/search/global-search-provider.tsx +528 -0
- package/ui/search/search-history.ts +74 -0
- package/ui/search-input.tsx +146 -0
- package/ui/settings/account-settings.tsx +140 -0
- package/ui/settings/agent-profile-settings.tsx +184 -0
- package/ui/settings/api-keys-settings.tsx +349 -0
- package/ui/settings/desktop-background-settings.tsx +128 -0
- package/ui/settings/desktop-connection-settings.tsx +251 -0
- package/ui/settings/desktop-embed-env-panel.tsx +257 -0
- package/ui/settings/desktop-fleet-telemetry-settings.tsx +269 -0
- package/ui/settings/desktop-hermes-settings.tsx +341 -0
- package/ui/settings/desktop-llm-keys.tsx +192 -0
- package/ui/settings/desktop-n8n-settings.tsx +306 -0
- package/ui/settings/desktop-tunnel.tsx +264 -0
- package/ui/settings/desktop-update-settings.tsx +175 -0
- package/ui/settings/factory-reset-settings.tsx +89 -0
- package/ui/settings/host-only-settings.tsx +62 -0
- package/ui/settings/locked-config-field.tsx +30 -0
- package/ui/settings/ops-diagnostic-settings.tsx +242 -0
- package/ui/settings/search-reindex-settings.tsx +158 -0
- package/ui/settings/server-mode-cards.tsx +801 -0
- package/ui/theme/tailwind-preset.cjs +82 -0
- package/ui/theme/theme.css +328 -0
- package/ui/workspace/ai-activity-panel-host.tsx +19 -0
- package/ui/workspace/ai-workspace-agent-host.tsx +54 -0
- package/ui/workspace/ai-workspace-banner.tsx +168 -0
- package/ui/workspace/impersonation-banner.tsx +39 -0
- package/ui/workspace/keep-alive.tsx +220 -0
- package/ui/workspace/tab-workspace-context.tsx +1172 -0
- package/ui/workspace/tab-workspace-host.ts +89 -0
- package/ui/workspace/types.ts +500 -0
- package/ui/workspace/use-location-search.tsx +50 -0
- package/ui/workspace/window-chrome-controls.tsx +116 -0
- package/ui/workspace/workspace-config.ts +47 -0
- package/ui/workspace/workspace-root.tsx +147 -0
- package/ui/workspace/workspace-shell.tsx +292 -0
- package/ui/workspace/workspace-tab-bar.tsx +572 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { getShellDesktopApi, getShellUiBrand } from "@creezio/shell-ui";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Bannière quand une mise à jour desktop est disponible.
|
|
7
|
+
* Pas de toast : la barre suffit (évite le doublon coin haut-droit).
|
|
8
|
+
* Invisible hors app Electron.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useEffect, useState } from "react";
|
|
12
|
+
import { toast } from "sonner";
|
|
13
|
+
import { ArrowUpCircle, Loader2, X } from "lucide-react";
|
|
14
|
+
import { Button } from "../primitives/button";
|
|
15
|
+
import type { DesktopUpdateStatus } from "../desktop-types";
|
|
16
|
+
|
|
17
|
+
export function DesktopUpdateBanner() {
|
|
18
|
+
const [status, setStatus] = useState<DesktopUpdateStatus | null>(null);
|
|
19
|
+
const [dismissed, setDismissed] = useState<string | null>(null);
|
|
20
|
+
const [busy, setBusy] = useState(false);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const api = getShellDesktopApi();
|
|
24
|
+
if (!api?.getUpdateStatus) return;
|
|
25
|
+
void api.getUpdateStatus().then(setStatus);
|
|
26
|
+
const unsub = api.onUpdateChanged?.(setStatus);
|
|
27
|
+
return () => unsub?.();
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
if (!status) return null;
|
|
31
|
+
if (status.state === "disabled" || status.state === "not-available") return null;
|
|
32
|
+
if (!status.updateAvailable && status.state !== "downloading" && status.state !== "ready") {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (
|
|
36
|
+
dismissed &&
|
|
37
|
+
status.availableVersion &&
|
|
38
|
+
dismissed === status.availableVersion &&
|
|
39
|
+
status.state === "available"
|
|
40
|
+
) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function onInstall() {
|
|
45
|
+
const api = getShellDesktopApi();
|
|
46
|
+
if (!api?.downloadAndInstallUpdate) return;
|
|
47
|
+
setBusy(true);
|
|
48
|
+
try {
|
|
49
|
+
const s = await api.downloadAndInstallUpdate();
|
|
50
|
+
setStatus(s);
|
|
51
|
+
if (s.state === "error") toast.error(s.error || "Échec de la mise à jour");
|
|
52
|
+
} catch (e) {
|
|
53
|
+
toast.error(e instanceof Error ? e.message : "Échec de la mise à jour");
|
|
54
|
+
} finally {
|
|
55
|
+
setBusy(false);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const downloading = status.state === "downloading";
|
|
60
|
+
const version = status.availableVersion ?? "?";
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
role="status"
|
|
65
|
+
className="relative z-[55] flex flex-wrap items-center justify-center gap-2 bg-sky-600 px-3 py-2 text-center text-[13px] text-white"
|
|
66
|
+
>
|
|
67
|
+
<ArrowUpCircle className="h-4 w-4 shrink-0" />
|
|
68
|
+
<span className="font-medium">
|
|
69
|
+
{downloading
|
|
70
|
+
? `Téléchargement de la mise à jour ${version}… ${Math.floor(status.percent ?? 0)} %`
|
|
71
|
+
: status.state === "ready"
|
|
72
|
+
? `Mise à jour ${version} prête — redémarrage…`
|
|
73
|
+
: `Mise à jour ${version} disponible`}
|
|
74
|
+
</span>
|
|
75
|
+
{!downloading && status.state !== "ready" ? (
|
|
76
|
+
<Button
|
|
77
|
+
type="button"
|
|
78
|
+
size="sm"
|
|
79
|
+
variant="secondary"
|
|
80
|
+
className="h-7 bg-white text-sky-800 hover:bg-sky-50"
|
|
81
|
+
disabled={busy}
|
|
82
|
+
onClick={() => void onInstall()}
|
|
83
|
+
>
|
|
84
|
+
{busy ? <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" /> : null}
|
|
85
|
+
Mettre à jour
|
|
86
|
+
</Button>
|
|
87
|
+
) : null}
|
|
88
|
+
{status.state === "available" ? (
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 text-white/80 hover:bg-white/10 hover:text-white"
|
|
92
|
+
aria-label="Masquer"
|
|
93
|
+
onClick={() => setDismissed(status.availableVersion ?? "x")}
|
|
94
|
+
>
|
|
95
|
+
<X className="h-4 w-4" />
|
|
96
|
+
</button>
|
|
97
|
+
) : null}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "@creezio/shell-ui";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* En-tête standard des fiches entité (contenu, sous le chrome sticky).
|
|
6
|
+
* Titre obligatoire — pas de H1 hors de ce composant.
|
|
7
|
+
*/
|
|
8
|
+
export function EntityHeader({
|
|
9
|
+
title,
|
|
10
|
+
description,
|
|
11
|
+
badges,
|
|
12
|
+
actions,
|
|
13
|
+
className,
|
|
14
|
+
}: {
|
|
15
|
+
title: string;
|
|
16
|
+
description?: ReactNode;
|
|
17
|
+
badges?: ReactNode;
|
|
18
|
+
actions?: ReactNode;
|
|
19
|
+
className?: string;
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<header
|
|
23
|
+
className={cn(
|
|
24
|
+
"flex flex-wrap items-start justify-between gap-3",
|
|
25
|
+
className,
|
|
26
|
+
)}
|
|
27
|
+
>
|
|
28
|
+
<div className="min-w-0 flex-1 space-y-2">
|
|
29
|
+
{badges ? (
|
|
30
|
+
<div className="flex flex-wrap items-center gap-2">{badges}</div>
|
|
31
|
+
) : null}
|
|
32
|
+
<h1 className="text-xl font-semibold leading-snug tracking-tight text-slate-900 sm:text-2xl">
|
|
33
|
+
{title}
|
|
34
|
+
</h1>
|
|
35
|
+
{description ? (
|
|
36
|
+
<div className="text-sm text-slate-500">{description}</div>
|
|
37
|
+
) : null}
|
|
38
|
+
</div>
|
|
39
|
+
{actions ? (
|
|
40
|
+
<div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div>
|
|
41
|
+
) : null}
|
|
42
|
+
</header>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Nav OS native Creezio — adaptateur sidebar du catalogue unique.
|
|
5
|
+
*
|
|
6
|
+
* SoT = `registerOsNavEntry` / `defaultOsCatalogEntries()`
|
|
7
|
+
* (`@creezio/shell-ui`, `src/nav-catalog.ts`). Cette fonction ne fait
|
|
8
|
+
* que projeter le registre vers `SidebarNavItem[]` (icônes résolues).
|
|
9
|
+
*
|
|
10
|
+
* La marque DOIT monter `<NavCatalogLoader />` (Phase C) — **interdit**
|
|
11
|
+
* de recopier un `OS_NAV` inline. Fallback hors mount :
|
|
12
|
+
* `defaultOsPrimaryNavItems()`. Cible : `docs/plans/PLAN-NAV-CATALOG.md`.
|
|
13
|
+
* Hermes / n8n restent injectés par la sidebar kit (Admin → Outils).
|
|
14
|
+
*
|
|
15
|
+
* Feature-off plugins : passer `{ includePlugins: false }` à
|
|
16
|
+
* `defaultOsAdminNavItems` — ne pas lister `/admin/plugins`.
|
|
17
|
+
*/
|
|
18
|
+
import {
|
|
19
|
+
Activity,
|
|
20
|
+
Braces,
|
|
21
|
+
Cable,
|
|
22
|
+
Database,
|
|
23
|
+
KeyRound,
|
|
24
|
+
List,
|
|
25
|
+
Package,
|
|
26
|
+
ScrollText,
|
|
27
|
+
Settings,
|
|
28
|
+
ShieldCheck,
|
|
29
|
+
} from "lucide-react";
|
|
30
|
+
import {
|
|
31
|
+
defaultOsCatalogEntries,
|
|
32
|
+
type NavCatalogEntry,
|
|
33
|
+
} from "../../dist/nav-catalog.js";
|
|
34
|
+
import { resolveNavIcon } from "./nav-icons";
|
|
35
|
+
import type { SidebarAdminItem, SidebarNavItem } from "./sidebar-host";
|
|
36
|
+
|
|
37
|
+
function toSidebarNavItem(entry: NavCatalogEntry): SidebarNavItem {
|
|
38
|
+
return {
|
|
39
|
+
href: entry.href,
|
|
40
|
+
label: entry.label,
|
|
41
|
+
icon: resolveNavIcon(entry.icon),
|
|
42
|
+
fromShell: true,
|
|
43
|
+
permission: entry.permission,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Pages OS utilisateur (hors métier marque) — dérivé du registre. */
|
|
48
|
+
export function defaultOsPrimaryNavItems(): SidebarNavItem[] {
|
|
49
|
+
return defaultOsCatalogEntries()
|
|
50
|
+
.filter(
|
|
51
|
+
(e) =>
|
|
52
|
+
e.available &&
|
|
53
|
+
e.defaultVisible &&
|
|
54
|
+
e.group !== "admin" &&
|
|
55
|
+
e.group !== "plugin",
|
|
56
|
+
)
|
|
57
|
+
.map(toSidebarNavItem);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type OsAdminNavOptions = {
|
|
61
|
+
/**
|
|
62
|
+
* Inclure `/admin/plugins`. Défaut `true`.
|
|
63
|
+
* Mettre `false` si `manifest.features.plugins === false`.
|
|
64
|
+
*/
|
|
65
|
+
includePlugins?: boolean;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Liens Admin OS (Hermes/n8n ajoutés à part par la sidebar kit). */
|
|
69
|
+
export function defaultOsAdminNavItems(
|
|
70
|
+
opts?: OsAdminNavOptions,
|
|
71
|
+
): SidebarAdminItem[] {
|
|
72
|
+
const includePlugins = opts?.includePlugins !== false;
|
|
73
|
+
const items: SidebarAdminItem[] = [
|
|
74
|
+
{ href: "/configuration", label: "Configuration", icon: Settings },
|
|
75
|
+
{ href: "/admin/analytics", label: "Analytics", icon: Activity },
|
|
76
|
+
];
|
|
77
|
+
if (includePlugins) {
|
|
78
|
+
items.push({ href: "/admin/plugins", label: "Plugins", icon: Package });
|
|
79
|
+
}
|
|
80
|
+
items.push(
|
|
81
|
+
{
|
|
82
|
+
href: "/admin/access",
|
|
83
|
+
label: "Rôles & accès",
|
|
84
|
+
icon: ShieldCheck,
|
|
85
|
+
permission: "platform.access.manage",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
href: "/admin/nav",
|
|
89
|
+
label: "Navigation",
|
|
90
|
+
icon: List,
|
|
91
|
+
permission: "platform.access.manage",
|
|
92
|
+
},
|
|
93
|
+
{ href: "/admin/database", label: "Database", icon: Database },
|
|
94
|
+
{ href: "/admin/integrations", label: "Intégrations", icon: KeyRound },
|
|
95
|
+
{ href: "/admin/api", label: "API", icon: Braces },
|
|
96
|
+
{ href: "/admin/mcp", label: "MCP", icon: Cable },
|
|
97
|
+
{
|
|
98
|
+
href: "/admin/request-logs",
|
|
99
|
+
label: "Logs API / MCP",
|
|
100
|
+
icon: ScrollText,
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
return items;
|
|
104
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Loader unique du catalogue sidebar — fetch `GET /api/v1/modules/nav`,
|
|
5
|
+
* bump `configureSidebar`, alimente `getNavItems`.
|
|
6
|
+
*
|
|
7
|
+
* Vit dans `@creezio/shell-ui/ui` pour que le chrome factory n'importe
|
|
8
|
+
* pas `@creezio/nav` au runtime UI. La factory **installe** quand même
|
|
9
|
+
* `@creezio/nav` (même vague publish) pour `/admin/nav` et le mount
|
|
10
|
+
* auto-register. `@creezio/nav/ui` re-exporte le même composant.
|
|
11
|
+
*
|
|
12
|
+
* Contrat items : `{ id, href, label, order, group?, permission?, icon }`.
|
|
13
|
+
*/
|
|
14
|
+
import { useEffect } from "react";
|
|
15
|
+
import {
|
|
16
|
+
parseNavCatalogSessionItems,
|
|
17
|
+
type NavCatalogSessionItem,
|
|
18
|
+
} from "../../dist/nav-catalog.js";
|
|
19
|
+
import { defaultOsAdminNavItems } from "./native-os-nav";
|
|
20
|
+
import { resolveNavIcon } from "./nav-icons";
|
|
21
|
+
import {
|
|
22
|
+
configureSidebar,
|
|
23
|
+
getSidebarHostOptional,
|
|
24
|
+
type SidebarHost,
|
|
25
|
+
type SidebarNavItem,
|
|
26
|
+
} from "./sidebar-host";
|
|
27
|
+
|
|
28
|
+
export const NAV_CATALOG_ENDPOINT = "/api/v1/modules/nav";
|
|
29
|
+
|
|
30
|
+
export function mapNavCatalogSessionToSidebar(
|
|
31
|
+
items: readonly NavCatalogSessionItem[],
|
|
32
|
+
): SidebarNavItem[] {
|
|
33
|
+
return items
|
|
34
|
+
.filter((item) => item.group !== "admin" && item.group !== "plugin")
|
|
35
|
+
.slice()
|
|
36
|
+
.sort((a, b) => a.order - b.order || a.id.localeCompare(b.id))
|
|
37
|
+
.map((item) => ({
|
|
38
|
+
href: item.href,
|
|
39
|
+
label: item.label,
|
|
40
|
+
icon: resolveNavIcon(item.icon),
|
|
41
|
+
fromShell: item.group === "core" || item.group === undefined,
|
|
42
|
+
permission: item.permission,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function applyNavCatalogItemsToSidebar(
|
|
47
|
+
items: readonly NavCatalogSessionItem[],
|
|
48
|
+
opts?: { includePlugins?: boolean },
|
|
49
|
+
): void {
|
|
50
|
+
const prev = getSidebarHostOptional();
|
|
51
|
+
const next: SidebarHost = {
|
|
52
|
+
...(prev ?? { getNavItems: () => [] }),
|
|
53
|
+
getNavItems: () => mapNavCatalogSessionToSidebar(items),
|
|
54
|
+
getAdminItems:
|
|
55
|
+
prev?.getAdminItems ??
|
|
56
|
+
(() => defaultOsAdminNavItems({ includePlugins: opts?.includePlugins })),
|
|
57
|
+
};
|
|
58
|
+
configureSidebar(next);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type NavCatalogLoaderProps = {
|
|
62
|
+
endpoint?: string;
|
|
63
|
+
includePlugins?: boolean;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Monte dans le chrome (sous `RequireSession`). Premier paint = fallback
|
|
68
|
+
* `defaultOsPrimaryNavItems()` déjà posé par `configureSidebar` ; après
|
|
69
|
+
* GET / le host est remplacé et la sidebar se re-rend (version bump).
|
|
70
|
+
*/
|
|
71
|
+
export function NavCatalogLoader(props: NavCatalogLoaderProps = {}): null {
|
|
72
|
+
const endpoint = props.endpoint ?? NAV_CATALOG_ENDPOINT;
|
|
73
|
+
const includePlugins = props.includePlugins;
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
let cancelled = false;
|
|
77
|
+
void (async () => {
|
|
78
|
+
try {
|
|
79
|
+
const res = await fetch(endpoint, {
|
|
80
|
+
credentials: "include",
|
|
81
|
+
headers: { Accept: "application/json" },
|
|
82
|
+
});
|
|
83
|
+
if (!res.ok || cancelled) return;
|
|
84
|
+
const body: unknown = await res.json();
|
|
85
|
+
const items = parseNavCatalogSessionItems(body);
|
|
86
|
+
if (cancelled || items.length === 0) return;
|
|
87
|
+
applyNavCatalogItemsToSidebar(items, { includePlugins });
|
|
88
|
+
} catch {
|
|
89
|
+
/* fallback : configureSidebar initial (catalogue OS local) */
|
|
90
|
+
}
|
|
91
|
+
})();
|
|
92
|
+
return () => {
|
|
93
|
+
cancelled = true;
|
|
94
|
+
};
|
|
95
|
+
}, [endpoint, includePlugins]);
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Résolution nom lucide → composant. Allowlist kit (ceux déjà utilisés
|
|
5
|
+
* par la sidebar OS + Bot / NotebookPen). Inconnu → `Circle` + warning,
|
|
6
|
+
* jamais de throw UI.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
Activity,
|
|
10
|
+
Bot,
|
|
11
|
+
Braces,
|
|
12
|
+
Cable,
|
|
13
|
+
Circle,
|
|
14
|
+
Database,
|
|
15
|
+
FileText,
|
|
16
|
+
KeyRound,
|
|
17
|
+
LayoutDashboard,
|
|
18
|
+
List,
|
|
19
|
+
ListTodo,
|
|
20
|
+
Mail,
|
|
21
|
+
NotebookPen,
|
|
22
|
+
Package,
|
|
23
|
+
ScrollText,
|
|
24
|
+
Settings,
|
|
25
|
+
Shield,
|
|
26
|
+
ShieldCheck,
|
|
27
|
+
SlidersHorizontal,
|
|
28
|
+
SquarePen,
|
|
29
|
+
Workflow,
|
|
30
|
+
type LucideIcon,
|
|
31
|
+
} from "lucide-react";
|
|
32
|
+
import { isKnownNavIcon } from "../../dist/nav-catalog.js";
|
|
33
|
+
|
|
34
|
+
const NAV_ICONS: Record<string, LucideIcon> = {
|
|
35
|
+
Activity,
|
|
36
|
+
Bot,
|
|
37
|
+
Braces,
|
|
38
|
+
Cable,
|
|
39
|
+
Circle,
|
|
40
|
+
Database,
|
|
41
|
+
FileText,
|
|
42
|
+
KeyRound,
|
|
43
|
+
LayoutDashboard,
|
|
44
|
+
List,
|
|
45
|
+
ListTodo,
|
|
46
|
+
Mail,
|
|
47
|
+
NotebookPen,
|
|
48
|
+
Package,
|
|
49
|
+
ScrollText,
|
|
50
|
+
Settings,
|
|
51
|
+
Shield,
|
|
52
|
+
ShieldCheck,
|
|
53
|
+
SlidersHorizontal,
|
|
54
|
+
SquarePen,
|
|
55
|
+
Workflow,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function resolveNavIcon(name: string): LucideIcon {
|
|
59
|
+
const icon = NAV_ICONS[name];
|
|
60
|
+
if (icon && isKnownNavIcon(name)) return icon;
|
|
61
|
+
if (typeof console !== "undefined" && typeof console.warn === "function") {
|
|
62
|
+
console.warn(
|
|
63
|
+
`[shell-ui] resolveNavIcon: icône inconnue "${name}", fallback Circle`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return Circle;
|
|
67
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Fragment } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { GlobalSearchTrigger } from "../global-search";
|
|
6
|
+
import {
|
|
7
|
+
Breadcrumb,
|
|
8
|
+
BreadcrumbItem,
|
|
9
|
+
BreadcrumbLink,
|
|
10
|
+
BreadcrumbList,
|
|
11
|
+
BreadcrumbPage,
|
|
12
|
+
BreadcrumbSeparator,
|
|
13
|
+
} from "../primitives/breadcrumb";
|
|
14
|
+
import { cn } from "@creezio/shell-ui";
|
|
15
|
+
import { normalizeHref } from "../workspace/types";
|
|
16
|
+
import type { PageKind, TrailCrumb } from "../workspace/types";
|
|
17
|
+
import { usePageToolbarActions } from "./page-toolbar-context";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Bandeau unique sous les onglets workspace.
|
|
21
|
+
* - section : recherche + actions (titre = label d'onglet uniquement)
|
|
22
|
+
* - entity : recherche + fil d'Ariane + actions
|
|
23
|
+
*/
|
|
24
|
+
export function PageChrome({
|
|
25
|
+
kind,
|
|
26
|
+
href,
|
|
27
|
+
trail,
|
|
28
|
+
}: {
|
|
29
|
+
kind: PageKind;
|
|
30
|
+
href: string;
|
|
31
|
+
trail?: TrailCrumb[];
|
|
32
|
+
}) {
|
|
33
|
+
const actions = usePageToolbarActions(normalizeHref(href));
|
|
34
|
+
const crumbs: TrailCrumb[] =
|
|
35
|
+
kind === "entity" && trail && trail.length > 0 ? trail : [];
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="-mx-2 border-b border-slate-200/80 bg-white px-3 py-2 sm:-mx-3 sm:px-4 md:-mx-4 md:px-5">
|
|
39
|
+
<div className="flex min-h-9 items-center gap-2 sm:gap-3">
|
|
40
|
+
<div
|
|
41
|
+
className={cn(
|
|
42
|
+
"min-w-0 shrink-0",
|
|
43
|
+
kind === "entity" ? "w-full max-w-[14rem] sm:max-w-xs" : "max-w-md flex-1",
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
<GlobalSearchTrigger variant="toolbar" />
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{kind === "entity" && crumbs.length > 0 ? (
|
|
50
|
+
<div className="min-w-0 flex-1 overflow-hidden">
|
|
51
|
+
<Breadcrumb>
|
|
52
|
+
<BreadcrumbList className="flex-nowrap">
|
|
53
|
+
{crumbs.map((c, i) => {
|
|
54
|
+
const last = i === crumbs.length - 1;
|
|
55
|
+
// Le séparateur (un <li>) doit être FRÈRE de BreadcrumbItem
|
|
56
|
+
// (aussi un <li>) — un <li> imbriqué dans un <li> est du HTML
|
|
57
|
+
// invalide qui casse l'hydratation React (mismatch #418).
|
|
58
|
+
return (
|
|
59
|
+
<Fragment key={`${c.label}-${i}`}>
|
|
60
|
+
{i > 0 ? <BreadcrumbSeparator /> : null}
|
|
61
|
+
<BreadcrumbItem className="min-w-0">
|
|
62
|
+
{last || !c.href ? (
|
|
63
|
+
<BreadcrumbPage
|
|
64
|
+
className={cn(!last && "font-normal text-slate-500")}
|
|
65
|
+
>
|
|
66
|
+
{c.label}
|
|
67
|
+
</BreadcrumbPage>
|
|
68
|
+
) : (
|
|
69
|
+
<BreadcrumbLink asChild>
|
|
70
|
+
<Link href={c.href}>{c.label}</Link>
|
|
71
|
+
</BreadcrumbLink>
|
|
72
|
+
)}
|
|
73
|
+
</BreadcrumbItem>
|
|
74
|
+
</Fragment>
|
|
75
|
+
);
|
|
76
|
+
})}
|
|
77
|
+
</BreadcrumbList>
|
|
78
|
+
</Breadcrumb>
|
|
79
|
+
</div>
|
|
80
|
+
) : null}
|
|
81
|
+
|
|
82
|
+
{actions ? (
|
|
83
|
+
<div className="ml-auto flex shrink-0 flex-wrap items-center justify-end gap-2">
|
|
84
|
+
{actions}
|
|
85
|
+
</div>
|
|
86
|
+
) : null}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useLayoutEffect,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
} from "react";
|
|
13
|
+
import { normalizeHref } from "../workspace/types";
|
|
14
|
+
|
|
15
|
+
/** Clé stable pour la toolbar : pathname seul (les vues `?view=` partagent les actions). */
|
|
16
|
+
export function toolbarKey(href: string): string {
|
|
17
|
+
return normalizeHref(href).split("?")[0] || "/";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type RegisterFn = (href: string, actions: ReactNode | null) => void;
|
|
21
|
+
|
|
22
|
+
const RegisterContext = createContext<RegisterFn | null>(null);
|
|
23
|
+
const ToolbarVersionContext = createContext(0);
|
|
24
|
+
|
|
25
|
+
/** Ref module — lecture synchrone dans usePageToolbarActions sans recréer le contexte. */
|
|
26
|
+
const entriesRef = { current: new Map<string, ReactNode | null>() };
|
|
27
|
+
|
|
28
|
+
export function PageToolbarProvider({ children }: { children: ReactNode }) {
|
|
29
|
+
const [version, bump] = useState(0);
|
|
30
|
+
|
|
31
|
+
const register = useCallback<RegisterFn>((href, actions) => {
|
|
32
|
+
const key = toolbarKey(href);
|
|
33
|
+
const prev = entriesRef.current.get(key);
|
|
34
|
+
if (actions == null) {
|
|
35
|
+
if (!entriesRef.current.has(key)) return;
|
|
36
|
+
entriesRef.current.delete(key);
|
|
37
|
+
} else if (prev === actions) {
|
|
38
|
+
return;
|
|
39
|
+
} else {
|
|
40
|
+
entriesRef.current.set(key, actions);
|
|
41
|
+
}
|
|
42
|
+
bump((n) => n + 1);
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<RegisterContext.Provider value={register}>
|
|
47
|
+
<ToolbarVersionContext.Provider value={version}>
|
|
48
|
+
{children}
|
|
49
|
+
</ToolbarVersionContext.Provider>
|
|
50
|
+
</RegisterContext.Provider>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function useToolbarRegister(): RegisterFn | null {
|
|
55
|
+
return useContext(RegisterContext);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Publie les actions de page vers le bandeau sticky (clé = pathname de la pane). */
|
|
59
|
+
export function useRegisterPageToolbar(
|
|
60
|
+
href: string | null,
|
|
61
|
+
actions?: ReactNode,
|
|
62
|
+
) {
|
|
63
|
+
const register = useToolbarRegister();
|
|
64
|
+
const actionsRef = useRef(actions);
|
|
65
|
+
actionsRef.current = actions;
|
|
66
|
+
|
|
67
|
+
useLayoutEffect(() => {
|
|
68
|
+
if (!register || !href) return;
|
|
69
|
+
register(href, actionsRef.current ?? null);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!register || !href) return;
|
|
74
|
+
return () => register(href, null);
|
|
75
|
+
}, [register, href]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function usePageToolbarActions(href: string): ReactNode | null {
|
|
79
|
+
const version = useContext(ToolbarVersionContext);
|
|
80
|
+
void version;
|
|
81
|
+
return entriesRef.current.get(toolbarKey(href)) ?? null;
|
|
82
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bandeau permanent en environnement sandbox (clone restaurant).
|
|
3
|
+
* Activé via APP_ENV=sandbox — jamais en prod / client.
|
|
4
|
+
*
|
|
5
|
+
* Lecture dynamique de process.env pour éviter l'inlining Next au build.
|
|
6
|
+
*/
|
|
7
|
+
function runtimeEnv(name: string): string {
|
|
8
|
+
return String((process.env as Record<string, string | undefined>)[name] ?? "").trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function SandboxBanner() {
|
|
12
|
+
const env = runtimeEnv("APP_ENV").toLowerCase();
|
|
13
|
+
if (env !== "sandbox") return null;
|
|
14
|
+
|
|
15
|
+
const tenant = runtimeEnv("SANDBOX_TENANT") || "sandbox";
|
|
16
|
+
const url = runtimeEnv("SANDBOX_PUBLIC_URL");
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
role="status"
|
|
21
|
+
className="relative z-[60] flex items-center justify-center gap-3 bg-amber-500 px-3 py-1.5 text-center text-[12px] font-semibold tracking-wide text-amber-950"
|
|
22
|
+
>
|
|
23
|
+
<span className="rounded bg-amber-950/15 px-1.5 py-0.5 font-mono text-[10px] uppercase">
|
|
24
|
+
Sandbox
|
|
25
|
+
</span>
|
|
26
|
+
<span>
|
|
27
|
+
Environnement de test « {tenant} » — les changements ici ne touchent pas
|
|
28
|
+
l'instance client
|
|
29
|
+
{url ? (
|
|
30
|
+
<>
|
|
31
|
+
{" "}
|
|
32
|
+
· <span className="font-mono font-normal">{url.replace(/^https?:\/\//, "")}</span>
|
|
33
|
+
</>
|
|
34
|
+
) : null}
|
|
35
|
+
</span>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Suspense, type ReactNode } from "react";
|
|
2
|
+
import { AppShell } from "./app-shell";
|
|
3
|
+
import {
|
|
4
|
+
ViewToggle,
|
|
5
|
+
ViewToggleSkeleton,
|
|
6
|
+
type ViewOption,
|
|
7
|
+
} from "../list-toolbar";
|
|
8
|
+
|
|
9
|
+
/** À ré-exporter depuis page.tsx des sections catalogue avec vues. */
|
|
10
|
+
export const SECTION_VIEW_DYNAMIC = "force-dynamic" as const;
|
|
11
|
+
|
|
12
|
+
export type SectionViewShellProps = {
|
|
13
|
+
title: string;
|
|
14
|
+
subtitle: string;
|
|
15
|
+
view: string;
|
|
16
|
+
viewOptions: readonly ViewOption[];
|
|
17
|
+
suspenseKey: string;
|
|
18
|
+
fallback: ReactNode;
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Shell section immédiat : AppShell + ViewToggle optimiste + Suspense catalogue.
|
|
24
|
+
* Le fetch lourd vit dans *-catalog.tsx (async server component).
|
|
25
|
+
*/
|
|
26
|
+
export function SectionViewShell({
|
|
27
|
+
title,
|
|
28
|
+
subtitle,
|
|
29
|
+
view,
|
|
30
|
+
viewOptions,
|
|
31
|
+
suspenseKey,
|
|
32
|
+
fallback,
|
|
33
|
+
children,
|
|
34
|
+
}: SectionViewShellProps) {
|
|
35
|
+
return (
|
|
36
|
+
<AppShell
|
|
37
|
+
kind="section"
|
|
38
|
+
title={title}
|
|
39
|
+
subtitle={subtitle}
|
|
40
|
+
actions={<ViewToggle view={view} options={[...viewOptions]} />}
|
|
41
|
+
>
|
|
42
|
+
<Suspense key={suspenseKey} fallback={fallback}>
|
|
43
|
+
{children}
|
|
44
|
+
</Suspense>
|
|
45
|
+
</AppShell>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type SectionViewLoadingProps = {
|
|
50
|
+
title: string;
|
|
51
|
+
subtitle?: string;
|
|
52
|
+
viewOptions: readonly ViewOption[];
|
|
53
|
+
contentFallback: ReactNode;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** loading.tsx — même chrome que SectionViewShell (toggle skeleton + contenu). */
|
|
57
|
+
export function SectionViewLoading({
|
|
58
|
+
title,
|
|
59
|
+
subtitle = "Chargement…",
|
|
60
|
+
viewOptions,
|
|
61
|
+
contentFallback,
|
|
62
|
+
}: SectionViewLoadingProps) {
|
|
63
|
+
return (
|
|
64
|
+
<AppShell
|
|
65
|
+
kind="section"
|
|
66
|
+
title={title}
|
|
67
|
+
subtitle={subtitle}
|
|
68
|
+
actions={<ViewToggleSkeleton options={viewOptions} />}
|
|
69
|
+
>
|
|
70
|
+
{contentFallback}
|
|
71
|
+
</AppShell>
|
|
72
|
+
);
|
|
73
|
+
}
|