@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,116 @@
1
+ "use client";
2
+
3
+ import { getShellDesktopApi, getShellUiBrand } from "@creezio/shell-ui";
4
+
5
+ import { useCallback, useEffect, useState } from "react";
6
+ import { Minus, Square, X } from "lucide-react";
7
+ import { cn } from "@creezio/shell-ui";
8
+
9
+ /** Icône « restaurer » (deux carrés superposés, style Windows). */
10
+ function RestoreIcon({ className }: { className?: string }) {
11
+ return (
12
+ <svg
13
+ viewBox="0 0 12 12"
14
+ className={className}
15
+ aria-hidden
16
+ fill="none"
17
+ stroke="currentColor"
18
+ strokeWidth="1.25"
19
+ >
20
+ <rect x="3.25" y="1.75" width="7" height="7" rx="0.5" />
21
+ <path d="M1.75 3.75v6.5h6.5" />
22
+ </svg>
23
+ );
24
+ }
25
+
26
+ /**
27
+ * Boutons min / max-restore / close — chrome Notion-like (frameless Windows).
28
+ * Rendu uniquement si `getShellDesktopApi().customWindowChrome` (sync preload).
29
+ */
30
+ export function WindowChromeControls({
31
+ tone = "light",
32
+ }: {
33
+ tone?: "light" | "dark";
34
+ } = {}) {
35
+ const [visible, setVisible] = useState(false);
36
+ const [maximized, setMaximized] = useState(false);
37
+
38
+ useEffect(() => {
39
+ const api = getShellDesktopApi();
40
+ if (!api?.customWindowChrome) return;
41
+ setVisible(true);
42
+ void api.isWindowMaximized?.().then((v: boolean) => setMaximized(Boolean(v)));
43
+ return api.onWindowMaximizedChanged?.((v: boolean) => setMaximized(v));
44
+ }, []);
45
+
46
+ const onMinimize = useCallback(() => {
47
+ void getShellDesktopApi()?.minimizeWindow?.();
48
+ }, []);
49
+
50
+ const onToggleMax = useCallback(() => {
51
+ void getShellDesktopApi()?.toggleMaximizeWindow?.().then((r: { isMaximized?: boolean } | void) => {
52
+ if (r && typeof r.isMaximized === "boolean") setMaximized(r.isMaximized);
53
+ });
54
+ }, []);
55
+
56
+ const onClose = useCallback(() => {
57
+ void getShellDesktopApi()?.closeWindow?.();
58
+ }, []);
59
+
60
+ if (!visible) return null;
61
+
62
+ const btnTone =
63
+ tone === "dark"
64
+ ? "text-slate-300 hover:bg-white/10 hover:text-white active:bg-white/15"
65
+ : "text-slate-600 hover:bg-black/[0.08] hover:text-slate-900 active:bg-black/[0.12]";
66
+
67
+ return (
68
+ <div
69
+ className={`${getShellUiBrand().titlebarNoDragClass} flex h-10 shrink-0 items-stretch self-stretch`}
70
+ role="group"
71
+ aria-label="Contrôles de la fenêtre"
72
+ >
73
+ <button
74
+ type="button"
75
+ onClick={onMinimize}
76
+ aria-label="Réduire"
77
+ title="Réduire"
78
+ className={cn(
79
+ "flex w-[46px] cursor-default items-center justify-center transition-colors",
80
+ btnTone,
81
+ )}
82
+ >
83
+ <Minus className="h-3.5 w-3.5" strokeWidth={2} />
84
+ </button>
85
+ <button
86
+ type="button"
87
+ onClick={onToggleMax}
88
+ aria-label={maximized ? "Restaurer" : "Agrandir"}
89
+ title={maximized ? "Restaurer" : "Agrandir"}
90
+ className={cn(
91
+ "flex w-[46px] cursor-default items-center justify-center transition-colors",
92
+ btnTone,
93
+ )}
94
+ >
95
+ {maximized ? (
96
+ <RestoreIcon className="h-3 w-3" />
97
+ ) : (
98
+ <Square className="h-3 w-3" strokeWidth={2} />
99
+ )}
100
+ </button>
101
+ <button
102
+ type="button"
103
+ onClick={onClose}
104
+ aria-label="Fermer"
105
+ title="Fermer"
106
+ className={cn(
107
+ "flex w-[46px] cursor-default items-center justify-center transition-colors",
108
+ tone === "dark" ? "text-slate-300" : "text-slate-600",
109
+ "hover:bg-[#e81123] hover:text-white active:bg-[#c50f1f] active:text-white",
110
+ )}
111
+ >
112
+ <X className="h-3.5 w-3.5" strokeWidth={2} />
113
+ </button>
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,47 @@
1
+ export type ProductDetailCtxAdapter = {
2
+ isDetailPath(path: string): boolean;
3
+ withInferredCtx(href: string, fromHref?: string | null): string;
4
+ withCatalogueCtx(href: string): string;
5
+ };
6
+
7
+ let sidebarCollapsedKey = "creezio-sidebar-collapsed";
8
+ let defaultNewTabHref = "/dashboard";
9
+ let preferCatalogueSelector = "[data-creezio-assistant-ui]";
10
+ let productDetailCtxAdapter: ProductDetailCtxAdapter | null = null;
11
+
12
+ export function configureSidebarCollapsedKey(key: string): void {
13
+ const next = key.trim();
14
+ if (next) sidebarCollapsedKey = next;
15
+ }
16
+
17
+ export function getSidebarCollapsedKey(): string {
18
+ return sidebarCollapsedKey;
19
+ }
20
+
21
+ export function configureDefaultNewTabHref(href: string): void {
22
+ const next = href.trim();
23
+ if (next) defaultNewTabHref = next;
24
+ }
25
+
26
+ export function getDefaultNewTabHref(): string {
27
+ return defaultNewTabHref;
28
+ }
29
+
30
+ export function configurePreferCatalogueSelector(sel: string): void {
31
+ const next = sel.trim();
32
+ if (next) preferCatalogueSelector = next;
33
+ }
34
+
35
+ export function getPreferCatalogueSelector(): string {
36
+ return preferCatalogueSelector;
37
+ }
38
+
39
+ export function configureProductDetailCtx(
40
+ adapter: ProductDetailCtxAdapter | null,
41
+ ): void {
42
+ productDetailCtxAdapter = adapter;
43
+ }
44
+
45
+ export function getProductDetailCtxAdapter(): ProductDetailCtxAdapter | null {
46
+ return productDetailCtxAdapter;
47
+ }
@@ -0,0 +1,147 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { useEffect } from "react";
5
+ import { usePathname } from "next/navigation";
6
+ import { getShellDesktopApi } from "@creezio/shell-ui";
7
+ import {
8
+ AssistantProvider,
9
+ AssistantWidget,
10
+ UiDriver,
11
+ } from "@creezio/assistant/ui";
12
+ import { DesktopBridge } from "../desktop/desktop-bridge";
13
+ import { AuthWindowChrome } from "../desktop/auth-window-chrome";
14
+ import { AiWorkspaceAgentHost } from "./ai-workspace-agent-host";
15
+ import { AiWorkspaceBanner } from "./ai-workspace-banner";
16
+ import { ImpersonationBanner } from "./impersonation-banner";
17
+ import { TabWorkspaceProvider } from "./tab-workspace-context";
18
+ import { WorkspaceShell } from "./workspace-shell";
19
+
20
+ function isAuthSurface(pathname: string | null): boolean {
21
+ if (!pathname) return false;
22
+ return (
23
+ pathname === "/login" ||
24
+ pathname.startsWith("/login/") ||
25
+ pathname === "/setup" ||
26
+ pathname.startsWith("/setup/") ||
27
+ pathname === "/onboarding" ||
28
+ pathname.startsWith("/onboarding/")
29
+ );
30
+ }
31
+
32
+ function isServerCockpitSurface(pathname: string | null): boolean {
33
+ if (!pathname) return false;
34
+ return (
35
+ pathname === "/server-cockpit" || pathname.startsWith("/server-cockpit/")
36
+ );
37
+ }
38
+
39
+ /**
40
+ * Surfaces publiques « bare » (ADR-module-natif-hybride) : rendu sans aucun
41
+ * chrome OS (sidebar, onglets, assistant). Cas actuel : la landing page
42
+ * @creezio/landing servie sur /lp, ou via le host public lp.{zone} (le
43
+ * middleware marque réécrit / → /lp mais usePathname garde l'URL navigateur).
44
+ */
45
+ function isPublicBareSurface(pathname: string | null): boolean {
46
+ if (pathname === "/lp" || pathname?.startsWith("/lp/")) return true;
47
+ if (
48
+ typeof window !== "undefined" &&
49
+ window.location.hostname.toLowerCase().startsWith("lp.")
50
+ ) {
51
+ return true;
52
+ }
53
+ return false;
54
+ }
55
+
56
+ function defaultHideAssistantOn(pathname: string | null): boolean {
57
+ return (
58
+ isAuthSurface(pathname) ||
59
+ isServerCockpitSurface(pathname) ||
60
+ isPublicBareSurface(pathname)
61
+ );
62
+ }
63
+
64
+ export function WorkspaceRoot({
65
+ children,
66
+ wrapWorkspace,
67
+ banners,
68
+ sidebar,
69
+ footbar,
70
+ afterShell,
71
+ hideAssistantOn = defaultHideAssistantOn,
72
+ }: {
73
+ children: ReactNode;
74
+ wrapWorkspace?: (node: ReactNode) => ReactNode;
75
+ banners?: ReactNode;
76
+ sidebar?: ReactNode;
77
+ footbar?: ReactNode;
78
+ /** Slot ADDITIF rendu après le chrome par défaut (assistant inclus) —
79
+ * ne remplace PAS le widget assistant (pour le masquer : hideAssistantOn). */
80
+ afterShell?: ReactNode;
81
+ hideAssistantOn?: (pathname: string | null) => boolean;
82
+ }) {
83
+ const pathname = usePathname();
84
+ const authSurface = isAuthSurface(pathname);
85
+ const serverCockpit = isServerCockpitSurface(pathname);
86
+ const hideAssistant = hideAssistantOn(pathname);
87
+
88
+ // Hors session / cockpit serveur : masquer le FAB Electron topmost.
89
+ useEffect(() => {
90
+ if (!hideAssistant) return;
91
+ void getShellDesktopApi()?.setAssistantChrome?.("hidden");
92
+ }, [hideAssistant]);
93
+
94
+ // Surface publique (ex. landing /lp) : contenu nu, aucun shell ni assistant.
95
+ if (isPublicBareSurface(pathname)) {
96
+ return <>{children}</>;
97
+ }
98
+
99
+ const content = authSurface ? (
100
+ <AuthWindowChrome>{children}</AuthWindowChrome>
101
+ ) : serverCockpit ? (
102
+ <AuthWindowChrome variant="dark">{children}</AuthWindowChrome>
103
+ ) : (
104
+ children
105
+ );
106
+
107
+ const shell = (
108
+ <WorkspaceShell sidebar={sidebar} footbar={footbar}>
109
+ {hideAssistant ? null : <AiWorkspaceBanner />}
110
+ <ImpersonationBanner />
111
+ {banners}
112
+ {content}
113
+ </WorkspaceShell>
114
+ );
115
+
116
+ const wrappedShell = wrapWorkspace ? wrapWorkspace(shell) : shell;
117
+ // afterShell est un slot ADDITIF : il s'insère APRÈS le chrome par défaut
118
+ // (bridge desktop, widget assistant, UiDriver, agent host). Le remplacer
119
+ // ferait disparaître l'assistant — régression prod constatée (TF3 y montait
120
+ // sa cloche d'alertes et perdait FAB + panneau chat).
121
+ const defaultAfterShell = (
122
+ <>
123
+ <DesktopBridge />
124
+ {hideAssistant ? null : (
125
+ <>
126
+ <AssistantWidget />
127
+ <UiDriver />
128
+ <AiWorkspaceAgentHost />
129
+ </>
130
+ )}
131
+ {afterShell}
132
+ </>
133
+ );
134
+
135
+ // AssistantProvider DOIT envelopper WorkspaceShell + AssistantWidget :
136
+ // les deux appellent useAssistantUi() du même module kit. Un provider
137
+ // marque local (createContext jumeau) ne suffit PAS — crash prod
138
+ // « useAssistantUi must be used within AssistantProvider ».
139
+ return (
140
+ <AssistantProvider>
141
+ <TabWorkspaceProvider>
142
+ {wrappedShell}
143
+ {defaultAfterShell}
144
+ </TabWorkspaceProvider>
145
+ </AssistantProvider>
146
+ );
147
+ }
@@ -0,0 +1,292 @@
1
+ "use client";
2
+
3
+ import {
4
+ cloneElement,
5
+ isValidElement,
6
+ useCallback,
7
+ useEffect,
8
+ useRef,
9
+ useState,
10
+ type CSSProperties,
11
+ type ReactElement,
12
+ type ReactNode,
13
+ } from "react";
14
+ import { usePathname } from "next/navigation";
15
+ import { Menu } from "lucide-react";
16
+ import {
17
+ ASSISTANT_PANEL_WIDTH_PX,
18
+ useAssistantUi,
19
+ } from "@creezio/assistant/ui";
20
+ import { cn, getShellUiBrand } from "@creezio/shell-ui";
21
+ import { Sidebar } from "../layout/sidebar";
22
+ import { Button } from "../primitives/button";
23
+ import { GlobalSearchProvider } from "../search/global-search-provider";
24
+ import { PageToolbarProvider } from "../layout/page-toolbar-context";
25
+ import { KeepAliveOutlet } from "./keep-alive";
26
+ import { useTabWorkspace } from "./tab-workspace-context";
27
+ import { isWorkspacePath, normalizeHref } from "./types";
28
+ import { useLocationSearch } from "./use-location-search";
29
+ import { WorkspaceTabBar } from "./workspace-tab-bar";
30
+ import {
31
+ getPreferCatalogueSelector,
32
+ getSidebarCollapsedKey,
33
+ } from "./workspace-config";
34
+
35
+ type SidebarChromeProps = {
36
+ collapsed: boolean;
37
+ onToggleCollapse: () => void;
38
+ mobileOpen: boolean;
39
+ onMobileClose: () => void;
40
+ };
41
+
42
+ /** Sidebar marque (élément) reçoit les props chrome ; sinon Sidebar kit. */
43
+ function renderSidebarSlot(
44
+ sidebar: ReactNode | undefined,
45
+ props: SidebarChromeProps,
46
+ ): ReactNode {
47
+ if (sidebar == null) {
48
+ return <Sidebar {...props} />;
49
+ }
50
+ if (isValidElement(sidebar)) {
51
+ return cloneElement(
52
+ sidebar as ReactElement<Partial<SidebarChromeProps>>,
53
+ props,
54
+ );
55
+ }
56
+ return sidebar;
57
+ }
58
+
59
+ function resolveInternalHref(anchor: HTMLAnchorElement): string | null {
60
+ const raw = anchor.getAttribute("href");
61
+ if (!raw || raw.startsWith("#") || raw.startsWith("mailto:") || raw.startsWith("tel:")) {
62
+ return null;
63
+ }
64
+ if (anchor.target === "_blank" || anchor.hasAttribute("download")) return null;
65
+ if (anchor.dataset.workspaceNav === "ignore") return null;
66
+
67
+ let url: URL;
68
+ try {
69
+ url = new URL(raw, window.location.origin);
70
+ } catch {
71
+ return null;
72
+ }
73
+ if (url.origin !== window.location.origin) return null;
74
+ if (!isWorkspacePath(url.pathname)) return null;
75
+ return normalizeHref(`${url.pathname}${url.search}`);
76
+ }
77
+
78
+ /** Texte lisible du lien pour titre d'onglet provisoire. */
79
+ function linkLabel(anchor: HTMLAnchorElement): string | undefined {
80
+ const titled = (anchor.getAttribute("title") || "").trim();
81
+ if (titled) return titled.slice(0, 80);
82
+ const text = (anchor.innerText || anchor.textContent || "")
83
+ .replace(/\s+/g, " ")
84
+ .trim();
85
+ if (!text || text.length < 2) return undefined;
86
+ return text.slice(0, 80);
87
+ }
88
+
89
+ export function WorkspaceShell({
90
+ children,
91
+ sidebar,
92
+ footbar,
93
+ }: {
94
+ children: ReactNode;
95
+ sidebar?: ReactNode;
96
+ footbar?: ReactNode;
97
+ }) {
98
+ const pathname = usePathname() || "/";
99
+ const search = useLocationSearch(pathname);
100
+ // normalizeHref canonise "/" → /dashboard : la page racine (pure redirection)
101
+ // ne doit jamais avoir sa propre pane keep-alive (boucle router.replace).
102
+ const cacheKey = normalizeHref(search ? `${pathname}?${search}` : pathname);
103
+ const inWorkspace = isWorkspacePath(pathname);
104
+
105
+ const { navigate, ready, activeTab } = useTabWorkspace();
106
+ const { open, hydrated } = useAssistantUi();
107
+ const brand = getShellUiBrand();
108
+ // Panel ouvert : push layout historique (pr-[400px]).
109
+ // Panel fermé : PAS de gutter — FAB en overlay (React web / Electron topmost).
110
+ const rightChromePx = hydrated && open ? ASSISTANT_PANEL_WIDTH_PX : 0;
111
+ const activeHref = activeTab ? normalizeHref(activeTab.href) : cacheKey;
112
+
113
+ const [navOpen, setNavOpen] = useState(false);
114
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
115
+ const menuButtonRef = useRef<HTMLButtonElement>(null);
116
+ const wasOpenRef = useRef(false);
117
+ /** Clics interceptés avant hydratation workspace — rejoués dès ready. */
118
+ const pendingNavRef = useRef<{
119
+ href: string;
120
+ newTab: boolean;
121
+ title?: string;
122
+ preferCatalogue?: boolean;
123
+ } | null>(null);
124
+
125
+ const closeNav = useCallback(() => setNavOpen(false), []);
126
+
127
+ // Préférence sidebar réduite persistée (localStorage).
128
+ useEffect(() => {
129
+ try {
130
+ if (window.localStorage.getItem(getSidebarCollapsedKey()) === "1") {
131
+ setSidebarCollapsed(true);
132
+ }
133
+ } catch {
134
+ // stockage indisponible : on reste déplié
135
+ }
136
+ }, []);
137
+
138
+ const toggleSidebarCollapsed = useCallback(() => {
139
+ setSidebarCollapsed((prev) => {
140
+ const next = !prev;
141
+ try {
142
+ window.localStorage.setItem(getSidebarCollapsedKey(), next ? "1" : "0");
143
+ } catch {
144
+ // ignore
145
+ }
146
+ return next;
147
+ });
148
+ }, []);
149
+
150
+ useEffect(() => {
151
+ if (wasOpenRef.current && !navOpen) {
152
+ menuButtonRef.current?.focus();
153
+ }
154
+ wasOpenRef.current = navOpen;
155
+ }, [navOpen]);
156
+
157
+ // Rejouer la navigation en file dès que le workspace est prêt.
158
+ useEffect(() => {
159
+ if (!ready || !pendingNavRef.current) return;
160
+ const pending = pendingNavRef.current;
161
+ pendingNavRef.current = null;
162
+ navigate(pending.href, {
163
+ newTab: pending.newTab,
164
+ title: pending.title,
165
+ preferCatalogue: pending.preferCatalogue,
166
+ });
167
+ }, [ready, navigate]);
168
+
169
+ // Interception dès le premier paint (même avant ready) — évite le rechargement MPA.
170
+ useEffect(() => {
171
+ if (!inWorkspace) return;
172
+
173
+ function dispatchNav(
174
+ href: string,
175
+ newTab: boolean,
176
+ title?: string,
177
+ preferCatalogue?: boolean,
178
+ ) {
179
+ if (!ready) {
180
+ pendingNavRef.current = { href, newTab, title, preferCatalogue };
181
+ return;
182
+ }
183
+ navigate(href, { newTab, title, preferCatalogue });
184
+ }
185
+
186
+ function onClick(e: MouseEvent) {
187
+ if (e.defaultPrevented || e.button !== 0) return;
188
+ const anchor = (e.target as Element | null)?.closest?.("a");
189
+ if (!anchor) return;
190
+ const href = resolveInternalHref(anchor);
191
+ if (!href) return;
192
+
193
+ const newTab = e.metaKey || e.ctrlKey || e.shiftKey;
194
+ const preferCatalogue = Boolean(
195
+ anchor.closest(getPreferCatalogueSelector()),
196
+ );
197
+ e.preventDefault();
198
+ dispatchNav(href, newTab, linkLabel(anchor), preferCatalogue);
199
+ }
200
+
201
+ function onAuxClick(e: MouseEvent) {
202
+ if (e.button !== 1 || e.defaultPrevented) return;
203
+ const anchor = (e.target as Element | null)?.closest?.("a");
204
+ if (!anchor) return;
205
+ const href = resolveInternalHref(anchor);
206
+ if (!href) return;
207
+ const preferCatalogue = Boolean(
208
+ anchor.closest(getPreferCatalogueSelector()),
209
+ );
210
+ e.preventDefault();
211
+ dispatchNav(href, true, linkLabel(anchor), preferCatalogue);
212
+ }
213
+
214
+ document.addEventListener("click", onClick, true);
215
+ document.addEventListener("auxclick", onAuxClick, true);
216
+ return () => {
217
+ document.removeEventListener("click", onClick, true);
218
+ document.removeEventListener("auxclick", onAuxClick, true);
219
+ };
220
+ }, [inWorkspace, ready, navigate]);
221
+
222
+ if (!inWorkspace) {
223
+ return <>{children}</>;
224
+ }
225
+
226
+ return (
227
+ <GlobalSearchProvider>
228
+ <PageToolbarProvider>
229
+ <div
230
+ className={cn(
231
+ // h-dvh : hauteur viewport stable (PWA standalone incluse)
232
+ "flex h-dvh max-h-dvh overflow-hidden bg-gradient-to-br from-slate-50 via-white to-sky-50/40",
233
+ "transition-[padding] duration-200 ease-out",
234
+ // md+ : push layout uniquement panel ouvert (pas de gutter FAB)
235
+ rightChromePx > 0 && "md:pr-[var(--assistant-chrome-right)]",
236
+ )}
237
+ style={
238
+ {
239
+ "--assistant-panel-width": `${ASSISTANT_PANEL_WIDTH_PX}px`,
240
+ "--assistant-chrome-right": `${rightChromePx}px`,
241
+ } as CSSProperties
242
+ }
243
+ data-creezio-assistant-chrome={hydrated && open ? "panel" : "fab-overlay"}
244
+ >
245
+ {renderSidebarSlot(sidebar, {
246
+ collapsed: sidebarCollapsed,
247
+ onToggleCollapse: toggleSidebarCollapsed,
248
+ mobileOpen: navOpen,
249
+ onMobileClose: closeNav,
250
+ })}
251
+ <div
252
+ className={cn(
253
+ "flex min-h-0 min-w-0 flex-1 flex-col transition-[margin] duration-200 ease-out",
254
+ sidebarCollapsed ? "md:ml-16" : "md:ml-64",
255
+ )}
256
+ >
257
+ <header className="sticky top-0 z-20 shrink-0 bg-[#ebe7df] px-2 sm:px-3 md:px-4 md:pr-0">
258
+ <div className="flex items-stretch gap-1.5">
259
+ <Button
260
+ ref={menuButtonRef}
261
+ type="button"
262
+ variant="outline"
263
+ size="icon"
264
+ className={cn(
265
+ brand.titlebarNoDragClass,
266
+ "mt-1.5 h-7 w-7 shrink-0 border-slate-300/80 bg-white/70 md:hidden",
267
+ )}
268
+ onClick={() => setNavOpen(true)}
269
+ aria-label="Ouvrir le menu de navigation"
270
+ aria-expanded={navOpen}
271
+ aria-controls="mobile-nav"
272
+ >
273
+ <Menu className="h-4 w-4" />
274
+ </Button>
275
+ <div className="min-w-0 flex-1">
276
+ <WorkspaceTabBar />
277
+ </div>
278
+ </div>
279
+ </header>
280
+ {/* Scroll délégué aux panes keep-alive (pas au main) → scrollY natif par onglet */}
281
+ <main className="relative min-h-0 flex-1 overflow-hidden bg-white">
282
+ <KeepAliveOutlet routeKey={cacheKey} activeHref={activeHref}>
283
+ {children}
284
+ </KeepAliveOutlet>
285
+ </main>
286
+ {footbar}
287
+ </div>
288
+ </div>
289
+ </PageToolbarProvider>
290
+ </GlobalSearchProvider>
291
+ );
292
+ }