@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,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
|
+
}
|