@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.
Files changed (232) hide show
  1. package/README.md +336 -0
  2. package/dist/adapters/nav-shell.d.ts +54 -0
  3. package/dist/adapters/nav-shell.d.ts.map +1 -0
  4. package/dist/adapters/nav-shell.js +112 -0
  5. package/dist/adapters/nav-shell.js.map +1 -0
  6. package/dist/brand.d.ts +64 -0
  7. package/dist/brand.d.ts.map +1 -0
  8. package/dist/brand.js +59 -0
  9. package/dist/brand.js.map +1 -0
  10. package/dist/core-nav.d.ts +17 -0
  11. package/dist/core-nav.d.ts.map +1 -0
  12. package/dist/core-nav.js +40 -0
  13. package/dist/core-nav.js.map +1 -0
  14. package/dist/index.d.ts +29 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +24 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/lib/api-scopes.d.ts +24 -0
  19. package/dist/lib/api-scopes.d.ts.map +1 -0
  20. package/dist/lib/api-scopes.js +78 -0
  21. package/dist/lib/api-scopes.js.map +1 -0
  22. package/dist/lib/catalog-suspense-key.d.ts +3 -0
  23. package/dist/lib/catalog-suspense-key.d.ts.map +1 -0
  24. package/dist/lib/catalog-suspense-key.js +12 -0
  25. package/dist/lib/catalog-suspense-key.js.map +1 -0
  26. package/dist/lib/data-changed.d.ts +51 -0
  27. package/dist/lib/data-changed.d.ts.map +1 -0
  28. package/dist/lib/data-changed.js +227 -0
  29. package/dist/lib/data-changed.js.map +1 -0
  30. package/dist/lib/desktop-home-path.d.ts +14 -0
  31. package/dist/lib/desktop-home-path.d.ts.map +1 -0
  32. package/dist/lib/desktop-home-path.js +29 -0
  33. package/dist/lib/desktop-home-path.js.map +1 -0
  34. package/dist/lib/geo-distance.d.ts +6 -0
  35. package/dist/lib/geo-distance.d.ts.map +1 -0
  36. package/dist/lib/geo-distance.js +27 -0
  37. package/dist/lib/geo-distance.js.map +1 -0
  38. package/dist/lib/img.d.ts +2 -0
  39. package/dist/lib/img.d.ts.map +1 -0
  40. package/dist/lib/img.js +40 -0
  41. package/dist/lib/img.js.map +1 -0
  42. package/dist/lib/keepalive-eviction.d.ts +14 -0
  43. package/dist/lib/keepalive-eviction.d.ts.map +1 -0
  44. package/dist/lib/keepalive-eviction.js +44 -0
  45. package/dist/lib/keepalive-eviction.js.map +1 -0
  46. package/dist/lib/ops-track.d.ts +29 -0
  47. package/dist/lib/ops-track.d.ts.map +1 -0
  48. package/dist/lib/ops-track.js +37 -0
  49. package/dist/lib/ops-track.js.map +1 -0
  50. package/dist/lib/optimize-cover-url.d.ts +10 -0
  51. package/dist/lib/optimize-cover-url.d.ts.map +1 -0
  52. package/dist/lib/optimize-cover-url.js +80 -0
  53. package/dist/lib/optimize-cover-url.js.map +1 -0
  54. package/dist/lib/page-trails.d.ts +9 -0
  55. package/dist/lib/page-trails.d.ts.map +1 -0
  56. package/dist/lib/page-trails.js +20 -0
  57. package/dist/lib/page-trails.js.map +1 -0
  58. package/dist/lib/public-origin.d.ts +32 -0
  59. package/dist/lib/public-origin.d.ts.map +1 -0
  60. package/dist/lib/public-origin.js +125 -0
  61. package/dist/lib/public-origin.js.map +1 -0
  62. package/dist/lib/server-incident.d.ts +12 -0
  63. package/dist/lib/server-incident.d.ts.map +1 -0
  64. package/dist/lib/server-incident.js +72 -0
  65. package/dist/lib/server-incident.js.map +1 -0
  66. package/dist/lib/tab-document-url.d.ts +13 -0
  67. package/dist/lib/tab-document-url.d.ts.map +1 -0
  68. package/dist/lib/tab-document-url.js +60 -0
  69. package/dist/lib/tab-document-url.js.map +1 -0
  70. package/dist/lib/utils.d.ts +20 -0
  71. package/dist/lib/utils.d.ts.map +1 -0
  72. package/dist/lib/utils.js +87 -0
  73. package/dist/lib/utils.js.map +1 -0
  74. package/dist/nav-catalog.d.ts +97 -0
  75. package/dist/nav-catalog.d.ts.map +1 -0
  76. package/dist/nav-catalog.js +282 -0
  77. package/dist/nav-catalog.js.map +1 -0
  78. package/dist/registry.d.ts +11 -0
  79. package/dist/registry.d.ts.map +1 -0
  80. package/dist/registry.js +60 -0
  81. package/dist/registry.js.map +1 -0
  82. package/dist/types.d.ts +22 -0
  83. package/dist/types.d.ts.map +1 -0
  84. package/dist/types.js +2 -0
  85. package/dist/types.js.map +1 -0
  86. package/dist-cjs/adapters/nav-shell.js +115 -0
  87. package/dist-cjs/adapters/nav-shell.js.map +1 -0
  88. package/dist-cjs/brand.js +66 -0
  89. package/dist-cjs/brand.js.map +1 -0
  90. package/dist-cjs/core-nav.js +43 -0
  91. package/dist-cjs/core-nav.js.map +1 -0
  92. package/dist-cjs/index.js +95 -0
  93. package/dist-cjs/index.js.map +1 -0
  94. package/dist-cjs/lib/api-scopes.js +86 -0
  95. package/dist-cjs/lib/api-scopes.js.map +1 -0
  96. package/dist-cjs/lib/catalog-suspense-key.js +15 -0
  97. package/dist-cjs/lib/catalog-suspense-key.js.map +1 -0
  98. package/dist-cjs/lib/data-changed.js +236 -0
  99. package/dist-cjs/lib/data-changed.js.map +1 -0
  100. package/dist-cjs/lib/desktop-home-path.js +34 -0
  101. package/dist-cjs/lib/desktop-home-path.js.map +1 -0
  102. package/dist-cjs/lib/geo-distance.js +30 -0
  103. package/dist-cjs/lib/geo-distance.js.map +1 -0
  104. package/dist-cjs/lib/img.js +43 -0
  105. package/dist-cjs/lib/img.js.map +1 -0
  106. package/dist-cjs/lib/keepalive-eviction.js +49 -0
  107. package/dist-cjs/lib/keepalive-eviction.js.map +1 -0
  108. package/dist-cjs/lib/ops-track.js +41 -0
  109. package/dist-cjs/lib/ops-track.js.map +1 -0
  110. package/dist-cjs/lib/optimize-cover-url.js +83 -0
  111. package/dist-cjs/lib/optimize-cover-url.js.map +1 -0
  112. package/dist-cjs/lib/page-trails.js +25 -0
  113. package/dist-cjs/lib/page-trails.js.map +1 -0
  114. package/dist-cjs/lib/public-origin.js +130 -0
  115. package/dist-cjs/lib/public-origin.js.map +1 -0
  116. package/dist-cjs/lib/server-incident.js +75 -0
  117. package/dist-cjs/lib/server-incident.js.map +1 -0
  118. package/dist-cjs/lib/tab-document-url.js +65 -0
  119. package/dist-cjs/lib/tab-document-url.js.map +1 -0
  120. package/dist-cjs/lib/utils.js +97 -0
  121. package/dist-cjs/lib/utils.js.map +1 -0
  122. package/dist-cjs/nav-catalog.js +293 -0
  123. package/dist-cjs/nav-catalog.js.map +1 -0
  124. package/dist-cjs/package.json +3 -0
  125. package/dist-cjs/registry.js +64 -0
  126. package/dist-cjs/registry.js.map +1 -0
  127. package/dist-cjs/types.js +3 -0
  128. package/dist-cjs/types.js.map +1 -0
  129. package/package.json +163 -0
  130. package/ui/app-error-boundary.tsx +59 -0
  131. package/ui/data-table.tsx +121 -0
  132. package/ui/desktop/auth-window-chrome.tsx +51 -0
  133. package/ui/desktop/desktop-bridge.tsx +87 -0
  134. package/ui/desktop/external-site-slot.tsx +313 -0
  135. package/ui/desktop/external-site-surface.ts +58 -0
  136. package/ui/desktop/site-link.tsx +105 -0
  137. package/ui/desktop/window-chrome-controls.tsx +5 -0
  138. package/ui/desktop-types.ts +30 -0
  139. package/ui/faceted-filters.tsx +119 -0
  140. package/ui/global-search-host.ts +20 -0
  141. package/ui/global-search.tsx +66 -0
  142. package/ui/index.ts +178 -0
  143. package/ui/kit.ts +30 -0
  144. package/ui/layout/app-shell.tsx +58 -0
  145. package/ui/layout/desktop-update-banner.tsx +100 -0
  146. package/ui/layout/entity-header.tsx +44 -0
  147. package/ui/layout/native-os-nav.ts +104 -0
  148. package/ui/layout/nav-catalog-loader.tsx +98 -0
  149. package/ui/layout/nav-icons.ts +67 -0
  150. package/ui/layout/page-chrome.tsx +90 -0
  151. package/ui/layout/page-toolbar-context.tsx +82 -0
  152. package/ui/layout/sandbox-banner.tsx +38 -0
  153. package/ui/layout/section-view-shell.tsx +73 -0
  154. package/ui/layout/sidebar-host.ts +144 -0
  155. package/ui/layout/sidebar.tsx +1150 -0
  156. package/ui/lib/ai-screencast-hub.ts +127 -0
  157. package/ui/lib/ai-workspace-client.ts +46 -0
  158. package/ui/lib/aid.ts +22 -0
  159. package/ui/lib/desktop-host.ts +114 -0
  160. package/ui/lib/fleet-tracker-client.ts +237 -0
  161. package/ui/lib/hermes-ui.ts +114 -0
  162. package/ui/lib/n8n-ui.ts +115 -0
  163. package/ui/lib/use-creezio-resource.tsx +121 -0
  164. package/ui/lib/use-shell-ui-brand.ts +22 -0
  165. package/ui/list-toolbar.tsx +309 -0
  166. package/ui/os-pages/desktop-settings-page.tsx +158 -0
  167. package/ui/os-pages/index.ts +8 -0
  168. package/ui/page-loading/entity-page-loading.tsx +59 -0
  169. package/ui/page-loading/list-page-loading.tsx +61 -0
  170. package/ui/pagination.tsx +56 -0
  171. package/ui/primitives/avatar.tsx +33 -0
  172. package/ui/primitives/badge.tsx +36 -0
  173. package/ui/primitives/breadcrumb.tsx +93 -0
  174. package/ui/primitives/button.tsx +44 -0
  175. package/ui/primitives/card.tsx +44 -0
  176. package/ui/primitives/chart.tsx +317 -0
  177. package/ui/primitives/command.tsx +111 -0
  178. package/ui/primitives/dialog.tsx +100 -0
  179. package/ui/primitives/dropdown-menu.tsx +187 -0
  180. package/ui/primitives/input.tsx +19 -0
  181. package/ui/primitives/label.tsx +17 -0
  182. package/ui/primitives/resizable.tsx +45 -0
  183. package/ui/primitives/scroll-area.tsx +41 -0
  184. package/ui/primitives/select.tsx +73 -0
  185. package/ui/primitives/separator.tsx +26 -0
  186. package/ui/primitives/sheet.tsx +90 -0
  187. package/ui/primitives/skeleton.tsx +7 -0
  188. package/ui/primitives/sonner.tsx +22 -0
  189. package/ui/primitives/tabs.tsx +51 -0
  190. package/ui/primitives/textarea.tsx +22 -0
  191. package/ui/primitives/tooltip.tsx +31 -0
  192. package/ui/pwa/client-error-reporter.tsx +62 -0
  193. package/ui/pwa/register-sw.tsx +50 -0
  194. package/ui/range-filters.tsx +174 -0
  195. package/ui/search/global-search-config.ts +32 -0
  196. package/ui/search/global-search-provider.tsx +528 -0
  197. package/ui/search/search-history.ts +74 -0
  198. package/ui/search-input.tsx +146 -0
  199. package/ui/settings/account-settings.tsx +140 -0
  200. package/ui/settings/agent-profile-settings.tsx +184 -0
  201. package/ui/settings/api-keys-settings.tsx +349 -0
  202. package/ui/settings/desktop-background-settings.tsx +128 -0
  203. package/ui/settings/desktop-connection-settings.tsx +251 -0
  204. package/ui/settings/desktop-embed-env-panel.tsx +257 -0
  205. package/ui/settings/desktop-fleet-telemetry-settings.tsx +269 -0
  206. package/ui/settings/desktop-hermes-settings.tsx +341 -0
  207. package/ui/settings/desktop-llm-keys.tsx +192 -0
  208. package/ui/settings/desktop-n8n-settings.tsx +306 -0
  209. package/ui/settings/desktop-tunnel.tsx +264 -0
  210. package/ui/settings/desktop-update-settings.tsx +175 -0
  211. package/ui/settings/factory-reset-settings.tsx +89 -0
  212. package/ui/settings/host-only-settings.tsx +62 -0
  213. package/ui/settings/locked-config-field.tsx +30 -0
  214. package/ui/settings/ops-diagnostic-settings.tsx +242 -0
  215. package/ui/settings/search-reindex-settings.tsx +158 -0
  216. package/ui/settings/server-mode-cards.tsx +801 -0
  217. package/ui/theme/tailwind-preset.cjs +82 -0
  218. package/ui/theme/theme.css +328 -0
  219. package/ui/workspace/ai-activity-panel-host.tsx +19 -0
  220. package/ui/workspace/ai-workspace-agent-host.tsx +54 -0
  221. package/ui/workspace/ai-workspace-banner.tsx +168 -0
  222. package/ui/workspace/impersonation-banner.tsx +39 -0
  223. package/ui/workspace/keep-alive.tsx +220 -0
  224. package/ui/workspace/tab-workspace-context.tsx +1172 -0
  225. package/ui/workspace/tab-workspace-host.ts +89 -0
  226. package/ui/workspace/types.ts +500 -0
  227. package/ui/workspace/use-location-search.tsx +50 -0
  228. package/ui/workspace/window-chrome-controls.tsx +116 -0
  229. package/ui/workspace/workspace-config.ts +47 -0
  230. package/ui/workspace/workspace-root.tsx +147 -0
  231. package/ui/workspace/workspace-shell.tsx +292 -0
  232. 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&apos;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
+ }