@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,121 @@
1
+ "use client";
2
+
3
+ import {
4
+ flexRender,
5
+ getCoreRowModel,
6
+ getFilteredRowModel,
7
+ getPaginationRowModel,
8
+ getSortedRowModel,
9
+ useReactTable,
10
+ type ColumnDef,
11
+ type SortingState,
12
+ } from "@tanstack/react-table";
13
+ import { useState } from "react";
14
+ import { Button } from "./primitives/button";
15
+ import { Input } from "./primitives/input";
16
+
17
+ type DataTableProps<TData, TValue> = {
18
+ columns: ColumnDef<TData, TValue>[];
19
+ data: TData[];
20
+ searchPlaceholder?: string;
21
+ initialPageSize?: number;
22
+ };
23
+
24
+ export function DataTable<TData, TValue>({
25
+ columns,
26
+ data,
27
+ searchPlaceholder = "Rechercher…",
28
+ initialPageSize = 25,
29
+ }: DataTableProps<TData, TValue>) {
30
+ const [sorting, setSorting] = useState<SortingState>([]);
31
+ const [globalFilter, setGlobalFilter] = useState("");
32
+
33
+ const table = useReactTable({
34
+ data,
35
+ columns,
36
+ state: { sorting, globalFilter },
37
+ onSortingChange: setSorting,
38
+ onGlobalFilterChange: setGlobalFilter,
39
+ getCoreRowModel: getCoreRowModel(),
40
+ getSortedRowModel: getSortedRowModel(),
41
+ getFilteredRowModel: getFilteredRowModel(),
42
+ getPaginationRowModel: getPaginationRowModel(),
43
+ initialState: { pagination: { pageSize: initialPageSize } },
44
+ });
45
+
46
+ return (
47
+ <div className="space-y-3">
48
+ <Input
49
+ value={globalFilter ?? ""}
50
+ onChange={(e) => setGlobalFilter(e.target.value)}
51
+ placeholder={searchPlaceholder}
52
+ className="max-w-sm"
53
+ />
54
+ <div className="overflow-x-auto rounded-xl border border-slate-200 bg-white">
55
+ <table className="w-full text-sm">
56
+ <thead className="bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500">
57
+ {table.getHeaderGroups().map((hg) => (
58
+ <tr key={hg.id}>
59
+ {hg.headers.map((header) => (
60
+ <th
61
+ key={header.id}
62
+ className="cursor-pointer px-4 py-3 font-medium select-none"
63
+ onClick={header.column.getToggleSortingHandler()}
64
+ >
65
+ {header.isPlaceholder
66
+ ? null
67
+ : flexRender(header.column.columnDef.header, header.getContext())}
68
+ {{ asc: " ↑", desc: " ↓" }[header.column.getIsSorted() as string] ?? null}
69
+ </th>
70
+ ))}
71
+ </tr>
72
+ ))}
73
+ </thead>
74
+ <tbody>
75
+ {table.getRowModel().rows.length ? (
76
+ table.getRowModel().rows.map((row) => (
77
+ <tr key={row.id} className="border-t border-slate-100 hover:bg-slate-50/80">
78
+ {row.getVisibleCells().map((cell) => (
79
+ <td key={cell.id} className="px-4 py-3 align-middle">
80
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
81
+ </td>
82
+ ))}
83
+ </tr>
84
+ ))
85
+ ) : (
86
+ <tr>
87
+ <td colSpan={columns.length} className="px-4 py-10 text-center text-slate-500">
88
+ Aucun résultat
89
+ </td>
90
+ </tr>
91
+ )}
92
+ </tbody>
93
+ </table>
94
+ </div>
95
+ <div className="flex items-center justify-between text-sm text-slate-500">
96
+ <span>
97
+ {table.getFilteredRowModel().rows.length} ligne(s) — page{" "}
98
+ {table.getState().pagination.pageIndex + 1} / {table.getPageCount() || 1}
99
+ </span>
100
+ <div className="flex gap-2">
101
+ <Button
102
+ variant="outline"
103
+ size="sm"
104
+ onClick={() => table.previousPage()}
105
+ disabled={!table.getCanPreviousPage()}
106
+ >
107
+ Précédent
108
+ </Button>
109
+ <Button
110
+ variant="outline"
111
+ size="sm"
112
+ onClick={() => table.nextPage()}
113
+ disabled={!table.getCanNextPage()}
114
+ >
115
+ Suivant
116
+ </Button>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ );
121
+ }
@@ -0,0 +1,51 @@
1
+ "use client";
2
+
3
+ import { getShellDesktopApi, getShellUiBrand } from "@creezio/shell-ui";
4
+
5
+ /**
6
+ * Chrome fenêtre frameless sur les écrans hors workspace (login, setup,
7
+ * server-cockpit, …). Sans ça, Windows n’a plus de min/max/close (la
8
+ * titlebar vit uniquement dans WorkspaceTabBar).
9
+ */
10
+
11
+ import { useEffect, useState, type ReactNode } from "react";
12
+ import { WindowChromeControls } from "../workspace/window-chrome-controls";
13
+ import { cn } from "@creezio/shell-ui";
14
+
15
+ export function AuthWindowChrome({
16
+ children,
17
+ variant = "light",
18
+ }: {
19
+ children: ReactNode;
20
+ /** `dark` = cockpit serveur (#0b1020). */
21
+ variant?: "light" | "dark";
22
+ }) {
23
+ const [chrome, setChrome] = useState(false);
24
+
25
+ useEffect(() => {
26
+ setChrome(Boolean(getShellDesktopApi()?.customWindowChrome));
27
+ // Belt-and-suspenders : pas de FAB Electron topmost hors session.
28
+ void getShellDesktopApi()?.setAssistantChrome?.("hidden");
29
+ }, []);
30
+
31
+ return (
32
+ <>
33
+ {chrome ? (
34
+ <div
35
+ className={cn(
36
+ "fixed inset-x-0 top-0 z-[100] flex h-10 items-stretch border-b backdrop-blur-sm",
37
+ variant === "dark"
38
+ ? "border-white/10 bg-[#0b1020]/95"
39
+ : "border-slate-200/80 bg-slate-100/95",
40
+ )}
41
+ role="banner"
42
+ aria-label="Barre de titre"
43
+ >
44
+ <div className={`${getShellUiBrand().titlebarDragClass} min-w-0 flex-1 self-stretch`} />
45
+ <WindowChromeControls tone={variant === "dark" ? "dark" : "light"} />
46
+ </div>
47
+ ) : null}
48
+ <div className={chrome ? "pt-10" : undefined}>{children}</div>
49
+ </>
50
+ );
51
+ }
@@ -0,0 +1,87 @@
1
+ "use client";
2
+
3
+ import { getShellDesktopApi } from "@creezio/shell-ui";
4
+
5
+ /**
6
+ * Pont desktop global (monté dans WorkspaceRoot, no-op en web).
7
+ *
8
+ * Écoute les ouvertures d’onglet **site externe** depuis le process principal
9
+ * Electron (`onExternalTabOpened` / alias déprécié `onSupplierTabOpened`)
10
+ * → active l’onglet workspace correspondant.
11
+ */
12
+
13
+ import { useEffect, useRef } from "react";
14
+ import {
15
+ useTabWorkspaceOptional,
16
+ openExternalSiteFromWorkspace,
17
+ type OpenExternalSiteOpts,
18
+ } from "../workspace/tab-workspace-host";
19
+
20
+ const DEFAULT_EXTERNAL_SITE_TITLE = "Site externe";
21
+
22
+ function titleFromInfo(url: string, rawTitle?: string): string {
23
+ const t = (rawTitle || "").trim();
24
+ if (t) return t;
25
+ try {
26
+ return new URL(url).hostname.replace(/^www\./, "");
27
+ } catch {
28
+ return DEFAULT_EXTERNAL_SITE_TITLE;
29
+ }
30
+ }
31
+
32
+ function resolveOpenFn(workspace: any): ((opts: OpenExternalSiteOpts) => void) | null {
33
+ if (!workspace) return null;
34
+ return workspace.openExternalSite ?? workspace.openSupplierSite ?? null;
35
+ }
36
+
37
+ export function DesktopBridge() {
38
+ const workspace = useTabWorkspaceOptional();
39
+ const openRef = useRef(resolveOpenFn(workspace));
40
+ const readyRef = useRef(Boolean(workspace?.ready));
41
+ const pendingRef = useRef<OpenExternalSiteOpts[]>([]);
42
+ openRef.current = resolveOpenFn(workspace);
43
+ readyRef.current = Boolean(workspace?.ready);
44
+
45
+ useEffect(() => {
46
+ const open = resolveOpenFn(workspace);
47
+ if (!workspace?.ready || !open) return;
48
+ const pending = pendingRef.current;
49
+ if (!pending.length) return;
50
+ pendingRef.current = [];
51
+ for (const opts of pending) open(opts);
52
+ }, [workspace?.ready, workspace]);
53
+
54
+ useEffect(() => {
55
+ const api = getShellDesktopApi();
56
+ // SoT : onExternalTabOpened ; alias déprécié onSupplierTabOpened
57
+ const subscribe =
58
+ api?.onExternalTabOpened ?? api?.onSupplierTabOpened;
59
+ if (!subscribe) return;
60
+ return subscribe((info: {
61
+ siteId?: number;
62
+ /** @deprecated → siteId */
63
+ fournisseurId?: number;
64
+ url: string;
65
+ title?: string;
66
+ tabId?: string;
67
+ electronTabId?: string;
68
+ }) => {
69
+ const opts: OpenExternalSiteOpts = {
70
+ siteId: info.siteId ?? info.fournisseurId ?? 0,
71
+ url: info.url,
72
+ title: titleFromInfo(info.url, info.title),
73
+ electronTabId: info.electronTabId ?? info.tabId,
74
+ };
75
+ if (readyRef.current && openRef.current) {
76
+ openRef.current(opts);
77
+ } else {
78
+ pendingRef.current.push(opts);
79
+ }
80
+ });
81
+ }, []);
82
+
83
+ return null;
84
+ }
85
+
86
+ /** @deprecated — préférer openExternalSiteFromWorkspace */
87
+ export { openExternalSiteFromWorkspace };
@@ -0,0 +1,313 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Slot content-area pour un onglet site externe.
5
+ *
6
+ * Le DOM reste vide : le vrai contenu est une WebContentsView Electron posee
7
+ * exactement sur ce rectangle (sidebar + tab bar + assistant restent hors
8
+ * bounds). Un ResizeObserver reporte {x,y,width,height} au main process.
9
+ *
10
+ * Pendant un chargement intentionnel (premier open / nouvelle URL utilisateur),
11
+ * la vue native est masquee et ce slot affiche un spinner. Les navigations SPA
12
+ * internes mettent a jour l'URL workspace sans rappeler openTab.
13
+ */
14
+
15
+ import { useEffect, useRef, useState } from "react";
16
+ import { Loader2 } from "lucide-react";
17
+ import { getShellDesktopApi, isSameTabDocument } from "@creezio/shell-ui";
18
+ import { usePaneActive } from "../workspace/keep-alive";
19
+ import { useTabWorkspace } from "../workspace/tab-workspace-context";
20
+ import type { DesktopContentRect, DesktopTabLoadState } from "../desktop-types";
21
+
22
+ function readRect(el: HTMLElement): DesktopContentRect {
23
+ const r = el.getBoundingClientRect();
24
+ return {
25
+ x: Math.round(r.x),
26
+ y: Math.round(r.y),
27
+ width: Math.max(1, Math.round(r.width)),
28
+ height: Math.max(1, Math.round(r.height)),
29
+ };
30
+ }
31
+
32
+ export type ExternalSiteSlotPhase = "idle" | "loading" | "ready" | "error";
33
+
34
+ /** @deprecated Use ExternalSiteSlotPhase. */
35
+ export type SupplierSlotPhase = ExternalSiteSlotPhase;
36
+
37
+ /** Pure : derive l'UI du slot depuis les events IPC load-state. */
38
+ export function reduceExternalSiteLoadState(
39
+ current: ExternalSiteSlotPhase,
40
+ ev: Pick<DesktopTabLoadState, "state">,
41
+ ): ExternalSiteSlotPhase {
42
+ if (ev.state === "loading") return "loading";
43
+ if (ev.state === "ready") return "ready";
44
+ if (ev.state === "error") return "error";
45
+ return current;
46
+ }
47
+
48
+ /** @deprecated Use reduceExternalSiteLoadState. */
49
+ export const reduceSupplierLoadState = reduceExternalSiteLoadState;
50
+
51
+ export function ExternalSiteSlot({
52
+ siteId: siteIdProp,
53
+ initialUrl,
54
+ }: {
55
+ /** Id de partition (number ou string numérique depuis la route). */
56
+ siteId: number | string;
57
+ /** URL de secours si l'onglet workspace n'a pas encore de meta.externalSite.url. */
58
+ initialUrl?: string;
59
+ }) {
60
+ const siteIdParsed =
61
+ typeof siteIdProp === "number" ? siteIdProp : Number(siteIdProp);
62
+ const siteIdValid = Number.isFinite(siteIdParsed);
63
+ const siteId = siteIdValid ? siteIdParsed : -1;
64
+ const slotRef = useRef<HTMLDivElement>(null);
65
+ const paneActive = usePaneActive();
66
+ const workspace = useTabWorkspace();
67
+ const { tabs, setTabMeta } = workspace;
68
+ const patchExternalSiteTab =
69
+ workspace.patchExternalSiteTab ?? workspace.patchSupplierTab;
70
+ const href = `/site/${siteId}`;
71
+ const wsTab = tabs.find((t: any) => {
72
+ const external = t.externalSite;
73
+ const legacy = t.supplier;
74
+ return (
75
+ external?.siteId === siteId ||
76
+ legacy?.siteId === siteId ||
77
+ legacy?.fournisseurId === siteId ||
78
+ (t.href.split("?")[0] || "") === href
79
+ );
80
+ });
81
+ const externalMeta = wsTab?.externalSite ?? wsTab?.supplier;
82
+ const url = externalMeta?.url || initialUrl || "";
83
+ const electronTabId = externalMeta?.electronTabId;
84
+ /** Document deja ouvert / synchronise depuis Electron - bloque la boucle openTab. */
85
+ const documentUrlRef = useRef("");
86
+ const [phase, setPhase] = useState<ExternalSiteSlotPhase>(() =>
87
+ url ? "loading" : "idle",
88
+ );
89
+ const [errorMsg, setErrorMsg] = useState<string | null>(null);
90
+
91
+ // Ouvre / reutilise la WebContentsView (cookies partition conserves).
92
+ // Ne pas rappeler openTab quand l'URL change uniquement via sync SPA.
93
+ useEffect(() => {
94
+ const api = getShellDesktopApi();
95
+ if (!api || !url || !/^https?:\/\//i.test(url)) {
96
+ setPhase(url ? "error" : "idle");
97
+ if (url) setErrorMsg("URL de site invalide.");
98
+ return;
99
+ }
100
+ if (isSameTabDocument(documentUrlRef.current, url)) {
101
+ return;
102
+ }
103
+ let cancelled = false;
104
+ if (!electronTabId) {
105
+ setPhase("loading");
106
+ }
107
+ setErrorMsg(null);
108
+ void api
109
+ .openTab(siteId, url)
110
+ .then((res: any) => {
111
+ if (cancelled) return;
112
+ const finalUrl = res.url || url;
113
+ documentUrlRef.current = finalUrl;
114
+ patchExternalSiteTab(siteId, {
115
+ electronTabId: res.tabId,
116
+ url: finalUrl,
117
+ });
118
+ if (res.loadState) {
119
+ setPhase((prev) =>
120
+ reduceExternalSiteLoadState(prev, { state: res.loadState }),
121
+ );
122
+ if (res.loadState === "error") {
123
+ setErrorMsg("Chargement impossible.");
124
+ } else if (res.loadState === "ready") {
125
+ setErrorMsg(null);
126
+ }
127
+ }
128
+ })
129
+ .catch((e: unknown) => {
130
+ if (cancelled) return;
131
+ console.error("Ouverture du site externe impossible :", e);
132
+ setPhase("error");
133
+ setErrorMsg(
134
+ e instanceof Error ? e.message : "Impossible d'ouvrir le site.",
135
+ );
136
+ });
137
+ return () => {
138
+ cancelled = true;
139
+ };
140
+ }, [siteId, url, electronTabId, patchExternalSiteTab]);
141
+
142
+ // IPC load-state (spinner immediat jusqu'a ready / error).
143
+ useEffect(() => {
144
+ const api = getShellDesktopApi();
145
+ if (!api?.onTabLoadState) return;
146
+ return api.onTabLoadState((ev: any) => {
147
+ const matchTab = Boolean(electronTabId && ev.tabId === electronTabId);
148
+ const eventSiteId = ev.siteId ?? ev.fournisseurId;
149
+ const matchSite = eventSiteId === siteId;
150
+ // Avant patch electronTabId : matcher sur siteId stable.
151
+ if (!matchTab && !matchSite) return;
152
+ setPhase((prev) => reduceExternalSiteLoadState(prev, ev));
153
+ if (ev.state === "error") {
154
+ setErrorMsg(ev.error || "Chargement impossible.");
155
+ } else if (ev.state === "ready") {
156
+ setErrorMsg(null);
157
+ }
158
+ });
159
+ }, [electronTabId, siteId]);
160
+
161
+ // Bounds + activation tant que la pane est active.
162
+ useEffect(() => {
163
+ const api = getShellDesktopApi();
164
+ const el = slotRef.current;
165
+ if (!api || !el) return;
166
+ if (!paneActive) return;
167
+
168
+ let cancelled = false;
169
+ let activationInFlight = false;
170
+
171
+ const activateNative = async (rect: DesktopContentRect) => {
172
+ if (activationInFlight) return;
173
+ activationInFlight = true;
174
+ try {
175
+ if (electronTabId) {
176
+ const activated = await api.activateTab(electronTabId, rect);
177
+ if (!activated || activated.ok) return;
178
+ }
179
+
180
+ if (!api.activateSite || !url) return;
181
+ const restored = await api.activateSite(siteId, url, rect);
182
+ if (cancelled || !restored.ok || !restored.tabId) return;
183
+ const finalUrl = restored.url || url;
184
+ documentUrlRef.current = finalUrl;
185
+ patchExternalSiteTab(siteId, {
186
+ electronTabId: restored.tabId,
187
+ url: finalUrl,
188
+ });
189
+ if (restored.loadState) {
190
+ setPhase((prev) =>
191
+ reduceExternalSiteLoadState(prev, { state: restored.loadState }),
192
+ );
193
+ }
194
+ } catch (e) {
195
+ console.warn(
196
+ "Reactivation de l'onglet externe impossible :",
197
+ e instanceof Error ? e.message : e,
198
+ );
199
+ } finally {
200
+ activationInFlight = false;
201
+ }
202
+ };
203
+
204
+ void activateNative(readRect(el));
205
+
206
+ let raf = 0;
207
+ const report = () => {
208
+ cancelAnimationFrame(raf);
209
+ raf = requestAnimationFrame(() => {
210
+ const rect = readRect(el);
211
+ void api.setContentRect(rect);
212
+ void activateNative(rect);
213
+ });
214
+ };
215
+
216
+ report();
217
+ const ro = new ResizeObserver(report);
218
+ ro.observe(el);
219
+ const main = el.closest("main");
220
+ if (main) ro.observe(main);
221
+ window.addEventListener("resize", report);
222
+
223
+ return () => {
224
+ cancelled = true;
225
+ cancelAnimationFrame(raf);
226
+ ro.disconnect();
227
+ window.removeEventListener("resize", report);
228
+ };
229
+ }, [paneActive, electronTabId, siteId, url, patchExternalSiteTab]);
230
+
231
+ // Metadata UI (titre / URL live) - sans declencher de reload.
232
+ useEffect(() => {
233
+ const api = getShellDesktopApi();
234
+ if (!api || !paneActive) return;
235
+ return api.onTabsChanged?.((list: any[]) => {
236
+ const t = list.find((x) => {
237
+ const eventSiteId = x.siteId ?? x.fournisseurId;
238
+ return x.tabId === electronTabId || eventSiteId === siteId;
239
+ });
240
+ if (!t) return;
241
+ if (t.url) {
242
+ documentUrlRef.current = t.url;
243
+ }
244
+ if (t.title) {
245
+ setTabMeta(href, { title: t.title, fullscreen: true, kind: "section" });
246
+ }
247
+ if (t.url || t.title) {
248
+ patchExternalSiteTab(siteId, {
249
+ ...(t.url ? { url: t.url } : {}),
250
+ ...(t.title ? { title: t.title } : {}),
251
+ });
252
+ }
253
+ });
254
+ }, [paneActive, electronTabId, siteId, href, setTabMeta, patchExternalSiteTab]);
255
+
256
+ if (!siteIdValid) {
257
+ return (
258
+ <div className="flex h-full items-center justify-center text-sm text-slate-500">
259
+ Site invalide
260
+ </div>
261
+ );
262
+ }
263
+
264
+ return (
265
+ <div
266
+ ref={slotRef}
267
+ data-creezio-site-slot
268
+ data-creezio-site-id={siteId}
269
+ data-creezio-site-phase={phase}
270
+ className="relative h-full min-h-0 w-full bg-slate-50"
271
+ aria-label="Site externe"
272
+ aria-busy={phase === "loading"}
273
+ >
274
+ {!url && (
275
+ <div className="flex h-full items-center justify-center p-8 text-center text-sm text-muted-foreground">
276
+ Aucune URL pour cet onglet.
277
+ </div>
278
+ )}
279
+ {url && phase === "loading" && (
280
+ <div
281
+ className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-slate-50"
282
+ role="status"
283
+ aria-live="polite"
284
+ >
285
+ <Loader2 className="h-8 w-8 animate-spin text-slate-400" />
286
+ <p className="text-sm text-slate-500">Chargement du site...</p>
287
+ </div>
288
+ )}
289
+ {url && phase === "error" && (
290
+ <div className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-2 bg-slate-50 p-8 text-center">
291
+ <p className="text-sm font-medium text-slate-700">
292
+ Impossible d&apos;ouvrir cette page
293
+ </p>
294
+ <p className="max-w-md break-all text-xs text-slate-500">{url}</p>
295
+ {errorMsg ? (
296
+ <p className="max-w-md text-xs text-slate-500">{errorMsg}</p>
297
+ ) : null}
298
+ </div>
299
+ )}
300
+ </div>
301
+ );
302
+ }
303
+
304
+ /** @deprecated Use ExternalSiteSlot with siteId. */
305
+ export function SupplierSiteSlot({
306
+ fournisseurId,
307
+ initialUrl,
308
+ }: {
309
+ fournisseurId: number | string;
310
+ initialUrl?: string;
311
+ }) {
312
+ return <ExternalSiteSlot siteId={fournisseurId} initialUrl={initialUrl} />;
313
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Commandes de surface native (WebContentsView) dérivées de l'onglet workspace.
3
+ * Pur / testable - pas d'Electron.
4
+ *
5
+ * Quitter un onglet site externe -> showCrm (masquer Chromium).
6
+ * Entrer / revenir sur un onglet site externe avec electronTabId -> activate.
7
+ */
8
+
9
+ export type ExternalSiteSurfaceSignal =
10
+ | { type: "leave-external" }
11
+ | { type: "enter-external"; electronTabId?: string | null };
12
+
13
+ export type ExternalSiteSurfaceCommand =
14
+ | { type: "show-crm" }
15
+ | { type: "activate"; tabId: string }
16
+ | { type: "noop" };
17
+
18
+ /**
19
+ * Réduit le signal workspace -> commande IPC desktop.
20
+ * Symétrique : leave = showCrm, enter avec id = activate, enter sans id = noop
21
+ * (le slot ouvrira via openTab).
22
+ */
23
+ export function reduceExternalSiteSurfaceCommand(
24
+ signal: ExternalSiteSurfaceSignal,
25
+ ): ExternalSiteSurfaceCommand {
26
+ if (signal.type === "leave-external") {
27
+ return { type: "show-crm" };
28
+ }
29
+ const id = String(signal.electronTabId || "").trim();
30
+ if (!id) return { type: "noop" };
31
+ return { type: "activate", tabId: id };
32
+ }
33
+
34
+ /**
35
+ * @deprecated Use ExternalSiteSurfaceSignal with leave-external/enter-external.
36
+ */
37
+ export type SupplierSurfaceSignal =
38
+ | { type: "leave-supplier" }
39
+ | { type: "enter-supplier"; electronTabId?: string | null };
40
+
41
+ /** @deprecated Use ExternalSiteSurfaceCommand. */
42
+ export type SupplierSurfaceCommand = ExternalSiteSurfaceCommand;
43
+
44
+ /** @deprecated Use reduceExternalSiteSurfaceCommand. */
45
+ export function reduceSupplierSurfaceCommand(
46
+ signal: SupplierSurfaceSignal | ExternalSiteSurfaceSignal,
47
+ ): ExternalSiteSurfaceCommand {
48
+ if (signal.type === "leave-supplier") {
49
+ return reduceExternalSiteSurfaceCommand({ type: "leave-external" });
50
+ }
51
+ if (signal.type === "enter-supplier") {
52
+ return reduceExternalSiteSurfaceCommand({
53
+ type: "enter-external",
54
+ electronTabId: signal.electronTabId,
55
+ });
56
+ }
57
+ return reduceExternalSiteSurfaceCommand(signal);
58
+ }