@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,115 @@
1
+ import { getShellDesktopApi, getShellUiBrand } from "@creezio/shell-ui";
2
+ /**
3
+ * Ouverture n8n UI dans un onglet workspace.
4
+ * Contrat : n8n est déjà installé + démarré au splash Héberger.
5
+ * Aucun download / ensure / start au clic.
6
+ */
7
+
8
+ /** Partition Electron stable (catalogue outils ≥ 900000). Aligné EMBED_TOOL_SITE_IDS.n8nUi. */
9
+ export const N8N_UI_SITE_ID = 900097;
10
+
11
+ export const N8N_UI_TAB_TITLE = "n8n";
12
+
13
+ export type N8nUiOpenTarget = {
14
+ url: string;
15
+ siteId: number;
16
+ title: string;
17
+ };
18
+
19
+ export type N8nUiOpenFail = {
20
+ ok: false;
21
+ error: string;
22
+ needsConfig?: boolean;
23
+ installing?: boolean;
24
+ };
25
+
26
+ /** Résout l’URL n8n déjà prête (splash) — pas de bootstrap. */
27
+ export async function resolveN8nUiOpenTarget(): Promise<
28
+ N8nUiOpenTarget | N8nUiOpenFail
29
+ > {
30
+ const api = typeof window !== "undefined" ? getShellDesktopApi() : undefined;
31
+ if (!api?.getN8nStatus) {
32
+ return {
33
+ ok: false,
34
+ error: "n8n disponible uniquement dans l’app desktop.",
35
+ };
36
+ }
37
+ try {
38
+ const st = await api.getN8nStatus();
39
+ const url = (st.uiUrl || "").trim();
40
+ if (!url) {
41
+ if (st.status === "skipped-remote-client") {
42
+ return {
43
+ ok: false,
44
+ error:
45
+ "Client distant — URL n8n introuvable. Rejoignez un hôte via {slug}." + getShellUiBrand().publicHostSuffix + " (sous-domaine n8n.{slug}…).",
46
+ };
47
+ }
48
+ return {
49
+ ok: false,
50
+ error:
51
+ "n8n n’est pas prêt. Redémarrez " + getShellUiBrand().productName + " et attendez la fin du splash (install automatique).",
52
+ };
53
+ }
54
+ return {
55
+ url,
56
+ siteId: N8N_UI_SITE_ID,
57
+ title: N8N_UI_TAB_TITLE,
58
+ };
59
+ } catch (e) {
60
+ return {
61
+ ok: false,
62
+ error: e instanceof Error ? e.message : "Statut n8n indisponible",
63
+ };
64
+ }
65
+ }
66
+
67
+ export function isN8nUiOpenTarget(
68
+ v: N8nUiOpenTarget | N8nUiOpenFail,
69
+ ): v is N8nUiOpenTarget {
70
+ return Boolean(v && "url" in v && typeof v.url === "string");
71
+ }
72
+
73
+ /** Ouvre l’onglet n8n — URL déjà connue, cookie session optionnel. */
74
+ export async function openN8nUiInWorkspace(opts: {
75
+ openExternalSite?: (o: {
76
+ siteId: number;
77
+ url: string;
78
+ title: string;
79
+ }) => void;
80
+ openTab?: (siteId: number, url: string) => Promise<unknown>;
81
+ onProgress?: (msg: string) => void;
82
+ }): Promise<{ ok: true } | N8nUiOpenFail> {
83
+ const api = typeof window !== "undefined" ? getShellDesktopApi() : undefined;
84
+ const target = await resolveN8nUiOpenTarget();
85
+ if (!isN8nUiOpenTarget(target)) {
86
+ return target;
87
+ }
88
+ if (api?.prepareN8nSession) {
89
+ try {
90
+ const prep = await api.prepareN8nSession();
91
+ if (!prep.ok) {
92
+ console.warn("[n8n] prepareN8nSession:", prep.detail);
93
+ }
94
+ } catch (e) {
95
+ console.warn(
96
+ "[n8n] prepareN8nSession failed",
97
+ e instanceof Error ? e.message : e,
98
+ );
99
+ }
100
+ }
101
+ if (opts.openExternalSite) {
102
+ opts.openExternalSite({
103
+ siteId: target.siteId,
104
+ url: target.url,
105
+ title: target.title,
106
+ });
107
+ return { ok: true };
108
+ }
109
+ if (opts.openTab) {
110
+ await opts.openTab(target.siteId, target.url);
111
+ return { ok: true };
112
+ }
113
+ window.open(target.url, "_blank", "noreferrer");
114
+ return { ok: true };
115
+ }
@@ -0,0 +1,121 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import {
6
+ subscribeDataChanged,
7
+ type CreezioDataChangedDetail,
8
+ } from "@creezio/shell-ui";
9
+ import { usePaneActive } from "../workspace/keep-alive";
10
+
11
+ export type UseCreezioResourceOptions = {
12
+ /**
13
+ * Appelé à chaque mutation de la resource (pane active ou non).
14
+ * Utile pour rafraîchir un store client (badge, provider…).
15
+ */
16
+ onChange?: (detail: CreezioDataChangedDetail) => void;
17
+ /**
18
+ * Si true (défaut), `router.refresh()` quand la pane est active et qu'une
19
+ * mutation arrive — couvre les pages RSC keep-alive (Next 14 ne refetch
20
+ * jamais à la réactivation d'onglet seule).
21
+ */
22
+ refreshOnChange?: boolean;
23
+ /**
24
+ * Si true (défaut), au moment où la pane redevient active, refresh s'il y a
25
+ * eu des mutations pendant l'absence (compteur module-scope).
26
+ */
27
+ refreshOnActivate?: boolean;
28
+ };
29
+
30
+ const seqByResource = new Map<string, number>();
31
+ const seenByResource = new Map<string, number>();
32
+
33
+ function bump(resource: string): void {
34
+ seqByResource.set(resource, (seqByResource.get(resource) || 0) + 1);
35
+ }
36
+
37
+ function consume(resource: string): boolean {
38
+ const seq = seqByResource.get(resource) || 0;
39
+ const seen = seenByResource.get(resource) || 0;
40
+ if (seen === seq) return false;
41
+ seenByResource.set(resource, seq);
42
+ return true;
43
+ }
44
+
45
+ /**
46
+ * Déclare qu'une page / composant dépend d'une resource métier.
47
+ * Toute mutation émise via `emitDataChanged` (API header, MCP/chat, UI)
48
+ * rafraîchit l'UI si la pane est ouverte — pattern app moderne, pas panier-only.
49
+ */
50
+ export function useCreezioResource(
51
+ resource: string,
52
+ opts?: UseCreezioResourceOptions,
53
+ ): void {
54
+ const router = useRouter();
55
+ const active = usePaneActive();
56
+ const activeRef = useRef(active);
57
+ activeRef.current = active;
58
+ const onChangeRef = useRef(opts?.onChange);
59
+ onChangeRef.current = opts?.onChange;
60
+ const refreshOnChange = opts?.refreshOnChange !== false;
61
+ const refreshOnActivate = opts?.refreshOnActivate !== false;
62
+ const res = String(resource || "").trim();
63
+
64
+ useEffect(() => {
65
+ if (!res) return;
66
+ if (refreshOnActivate && active && consume(res)) {
67
+ router.refresh();
68
+ }
69
+ }, [active, res, refreshOnActivate, router]);
70
+
71
+ useEffect(() => {
72
+ if (!res) return;
73
+ return subscribeDataChanged(
74
+ (detail) => {
75
+ bump(detail.resource);
76
+ onChangeRef.current?.(detail);
77
+ if (refreshOnChange && activeRef.current && consume(detail.resource)) {
78
+ router.refresh();
79
+ }
80
+ },
81
+ { resource: res },
82
+ );
83
+ }, [res, refreshOnChange, router]);
84
+ }
85
+
86
+ /** Version multi-resources (ex. page agrégée). */
87
+ export function useCreezioResources(
88
+ resources: string[],
89
+ opts?: UseCreezioResourceOptions,
90
+ ): void {
91
+ const list = resources.map((r) => r.trim()).filter(Boolean);
92
+ const router = useRouter();
93
+ const active = usePaneActive();
94
+ const activeRef = useRef(active);
95
+ activeRef.current = active;
96
+ const onChangeRef = useRef(opts?.onChange);
97
+ onChangeRef.current = opts?.onChange;
98
+ const refreshOnChange = opts?.refreshOnChange !== false;
99
+ const refreshOnActivate = opts?.refreshOnActivate !== false;
100
+ const key = list.slice().sort().join(",");
101
+
102
+ useEffect(() => {
103
+ if (!list.length) return;
104
+ if (refreshOnActivate && active) {
105
+ let dirty = false;
106
+ for (const r of list) if (consume(r)) dirty = true;
107
+ if (dirty) router.refresh();
108
+ }
109
+ }, [active, key, refreshOnActivate, router]); // eslint-disable-line react-hooks/exhaustive-deps
110
+
111
+ useEffect(() => {
112
+ if (!list.length) return;
113
+ return subscribeDataChanged((detail) => {
114
+ bump(detail.resource);
115
+ onChangeRef.current?.(detail);
116
+ if (refreshOnChange && activeRef.current && consume(detail.resource)) {
117
+ router.refresh();
118
+ }
119
+ }, { resource: list });
120
+ }, [key, refreshOnChange, router]); // eslint-disable-line react-hooks/exhaustive-deps
121
+ }
@@ -0,0 +1,22 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Brand shell-ui réactive — CreezioUiBoot applique la marque en useEffect
5
+ * (après le 1er render) : lire la brand via ce hook (useSyncExternalStore)
6
+ * plutôt que getShellUiBrand() direct pour re-render à la configuration.
7
+ */
8
+
9
+ import { useSyncExternalStore } from "react";
10
+ import {
11
+ getShellUiBrand,
12
+ subscribeShellUiBrand,
13
+ type ShellUiBrand,
14
+ } from "../../dist/brand.js";
15
+
16
+ export function useShellUiBrand(): ShellUiBrand {
17
+ return useSyncExternalStore(
18
+ subscribeShellUiBrand,
19
+ getShellUiBrand,
20
+ getShellUiBrand,
21
+ );
22
+ }
@@ -0,0 +1,309 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useEffect, useState } from "react";
5
+ import { usePathname, useSearchParams } from "next/navigation";
6
+ import { Skeleton } from "./primitives/skeleton";
7
+ import { cn } from "@creezio/shell-ui";
8
+
9
+ /** Clés query effacées au changement de preset — injectables marque. */
10
+ const DEFAULT_PRESET_CLEAR_KEYS = [
11
+ "status",
12
+ "alerts",
13
+ "alertType",
14
+ "platform",
15
+ "hasClient",
16
+ "fleet",
17
+ "risk",
18
+ "declaration",
19
+ "infraction",
20
+ "toDeclare",
21
+ "topic",
22
+ "type",
23
+ "declared",
24
+ "repaired",
25
+ "category",
26
+ "openOnly",
27
+ "settledOnly",
28
+ "company",
29
+ "coverage",
30
+ "hasAttestation",
31
+ "hasSub",
32
+ "year",
33
+ "month",
34
+ "productType",
35
+ "hasPdf",
36
+ "amountRange",
37
+ "pipeline",
38
+ "source",
39
+ "priceMin",
40
+ "priceMax",
41
+ "amountMin",
42
+ "amountMax",
43
+ "dateFrom",
44
+ "dateTo",
45
+ "payment_status",
46
+ "currency",
47
+ "invoiceMatched",
48
+ "sort",
49
+ "sens",
50
+ "period",
51
+ "varMin",
52
+ "varMax",
53
+ "deltaMin",
54
+ "deltaMax",
55
+ "docsIncomplete",
56
+ ] as const;
57
+
58
+ let presetClearKeys: string[] = [...DEFAULT_PRESET_CLEAR_KEYS];
59
+
60
+ /** Ajoute / remplace les clés métier marque (ex. TF: fournisseur, promo). */
61
+ export function configureListToolbarClearKeys(keys: string[]): void {
62
+ presetClearKeys = Array.from(new Set([...DEFAULT_PRESET_CLEAR_KEYS, ...keys]));
63
+ }
64
+
65
+ export function getListToolbarClearKeys(): readonly string[] {
66
+ return presetClearKeys;
67
+ }
68
+
69
+ export type PresetDef = {
70
+ id: string;
71
+ label: string;
72
+ params: Record<string, string>;
73
+ };
74
+
75
+ export type ViewOption = {
76
+ /** Valeur logique (ex. table, company). */
77
+ id: string;
78
+ label: string;
79
+ /** Valeur du searchParam `view` ; omit/undefined = vue par défaut (param retiré). */
80
+ param?: string;
81
+ };
82
+
83
+ const DEFAULT_VIEW_OPTIONS: ViewOption[] = [
84
+ { id: "table", label: "Liste" },
85
+ { id: "kanban", label: "Kanban", param: "kanban" },
86
+ ];
87
+
88
+ export function ViewToggle({
89
+ view,
90
+ tableLabel = "Liste",
91
+ kanbanLabel = "Kanban",
92
+ options,
93
+ }: {
94
+ view: string;
95
+ tableLabel?: string;
96
+ kanbanLabel?: string;
97
+ /** Options custom (sinon Liste / Kanban classiques). */
98
+ options?: ViewOption[];
99
+ }) {
100
+ const pathname = usePathname();
101
+ const params = useSearchParams();
102
+ const [pendingView, setPendingView] = useState<string | null>(null);
103
+ const activeView = pendingView ?? view;
104
+ const opts =
105
+ options ??
106
+ DEFAULT_VIEW_OPTIONS.map((o) =>
107
+ o.id === "table"
108
+ ? { ...o, label: tableLabel }
109
+ : o.id === "kanban"
110
+ ? { ...o, label: kanbanLabel }
111
+ : o,
112
+ );
113
+
114
+ useEffect(() => {
115
+ setPendingView(null);
116
+ }, [view]);
117
+
118
+ function hrefFor(opt: ViewOption) {
119
+ const sp = new URLSearchParams(params.toString());
120
+ if (!opt.param) sp.delete("view");
121
+ else sp.set("view", opt.param);
122
+ sp.delete("page");
123
+ const qs = sp.toString();
124
+ return qs ? `${pathname}?${qs}` : pathname;
125
+ }
126
+
127
+ return (
128
+ <div
129
+ className="flex flex-wrap gap-2 text-sm"
130
+ aria-busy={pendingView != null && pendingView !== view ? true : undefined}
131
+ >
132
+ {opts.map((opt) => {
133
+ const selected = activeView === opt.id;
134
+ const pending = pendingView === opt.id && pendingView !== view;
135
+ return (
136
+ <Link
137
+ key={opt.id}
138
+ href={hrefFor(opt)}
139
+ aria-current={selected ? "true" : undefined}
140
+ onClick={() => {
141
+ if (opt.id !== activeView) setPendingView(opt.id);
142
+ }}
143
+ className={cn(
144
+ "rounded-lg px-3 py-1.5 transition-colors",
145
+ selected ? "bg-slate-900 text-white" : "border bg-white hover:bg-slate-50",
146
+ pending && "ring-2 ring-sky-300 ring-offset-1",
147
+ )}
148
+ >
149
+ {opt.label}
150
+ </Link>
151
+ );
152
+ })}
153
+ </div>
154
+ );
155
+ }
156
+
157
+ /** Placeholder toggle pendant loading.tsx ou navigation lente. */
158
+ export function ViewToggleSkeleton({
159
+ options,
160
+ }: {
161
+ options?: readonly Pick<ViewOption, "label">[];
162
+ }) {
163
+ const labels = options?.map((o) => o.label) ?? ["Liste", "Kanban"];
164
+ return (
165
+ <div className="flex flex-wrap gap-2 text-sm" aria-hidden>
166
+ {labels.map((label) => (
167
+ <Skeleton key={label} className="h-8 w-20 rounded-lg" />
168
+ ))}
169
+ </div>
170
+ );
171
+ }
172
+
173
+ export function PresetChips({
174
+ presets,
175
+ activeId,
176
+ clearHref,
177
+ }: {
178
+ presets: PresetDef[];
179
+ activeId?: string | null;
180
+ clearHref?: string;
181
+ }) {
182
+ const pathname = usePathname();
183
+ const params = useSearchParams();
184
+
185
+ function hrefFor(preset: PresetDef) {
186
+ const sp = new URLSearchParams(params.toString());
187
+ // Remplace les filtres métier courants, conserve view/q
188
+ for (const key of presetClearKeys) {
189
+ sp.delete(key);
190
+ }
191
+ Object.entries(preset.params).forEach(([k, v]) => {
192
+ if (!v) sp.delete(k);
193
+ else sp.set(k, v);
194
+ });
195
+ sp.set("preset", preset.id);
196
+ sp.delete("page");
197
+ return `${pathname}?${sp.toString()}`;
198
+ }
199
+
200
+ return (
201
+ <div className="flex flex-wrap gap-2">
202
+ {presets.map((p) => (
203
+ <Link
204
+ key={p.id}
205
+ href={hrefFor(p)}
206
+ className={cn(
207
+ "rounded-full border px-3 py-1 text-xs font-medium transition",
208
+ activeId === p.id
209
+ ? "border-sky-700 bg-sky-50 text-sky-900"
210
+ : "border-slate-200 bg-white text-slate-600 hover:bg-slate-50",
211
+ )}
212
+ >
213
+ {p.label}
214
+ </Link>
215
+ ))}
216
+ {clearHref ? (
217
+ <Link
218
+ href={clearHref}
219
+ className="rounded-full border border-dashed border-slate-300 px-3 py-1 text-xs text-slate-500 hover:bg-slate-50"
220
+ >
221
+ Réinitialiser
222
+ </Link>
223
+ ) : null}
224
+ </div>
225
+ );
226
+ }
227
+
228
+ export function KpiStrip({
229
+ items,
230
+ columns = 4,
231
+ }: {
232
+ items: {
233
+ label: string;
234
+ value: number;
235
+ /** Affichage custom (ex. montants €) — sinon `value` localisé. */
236
+ display?: string;
237
+ href: string;
238
+ tone?: "danger" | "warning" | "ok" | "neutral";
239
+ }[];
240
+ /** Nombre de colonnes desktop (défaut 4). */
241
+ columns?: 3 | 4 | 5 | 6;
242
+ }) {
243
+ const colsClass =
244
+ columns === 6
245
+ ? "sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-6"
246
+ : columns === 5
247
+ ? "sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-5"
248
+ : columns === 3
249
+ ? "sm:grid-cols-2 lg:grid-cols-3"
250
+ : "sm:grid-cols-2 lg:grid-cols-4";
251
+
252
+ return (
253
+ <div className={cn("grid gap-3", colsClass)}>
254
+ {items.map((item) => {
255
+ const tone =
256
+ item.tone === "danger"
257
+ ? "border-red-200 bg-red-50/80 hover:bg-red-50"
258
+ : item.tone === "warning"
259
+ ? "border-amber-200 bg-amber-50/80 hover:bg-amber-50"
260
+ : item.tone === "ok"
261
+ ? "border-emerald-200 bg-emerald-50/60 hover:bg-emerald-50"
262
+ : "border-slate-200 bg-white hover:bg-slate-50";
263
+ return (
264
+ <Link
265
+ key={item.label}
266
+ href={item.href}
267
+ className={cn(
268
+ "rounded-xl border px-3 py-3 shadow-sm transition",
269
+ tone,
270
+ )}
271
+ >
272
+ <div className="text-[11px] font-medium uppercase tracking-wide text-slate-500">
273
+ {item.label}
274
+ </div>
275
+ <div className="mt-1 text-2xl font-semibold tabular-nums text-slate-900">
276
+ {item.display ?? item.value.toLocaleString("fr-FR")}
277
+ </div>
278
+ </Link>
279
+ );
280
+ })}
281
+ </div>
282
+ );
283
+ }
284
+
285
+ export function AlertChips({
286
+ chips,
287
+ }: {
288
+ chips: { key: string; short: string; tone: "danger" | "warning" }[];
289
+ }) {
290
+ if (!chips.length) {
291
+ return <span className="text-xs text-slate-400">—</span>;
292
+ }
293
+ return (
294
+ <div className="flex flex-wrap gap-1">
295
+ {chips.map((c) => (
296
+ <span
297
+ key={c.key}
298
+ className={
299
+ c.tone === "danger"
300
+ ? "rounded-full bg-red-50 px-2 py-0.5 text-xs font-medium text-red-700"
301
+ : "rounded-full bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-800"
302
+ }
303
+ >
304
+ {c.short}
305
+ </span>
306
+ ))}
307
+ </div>
308
+ );
309
+ }