@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,572 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { getShellDesktopApi, getShellUiBrand } from "@creezio/shell-ui";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useLayoutEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
type MouseEvent as ReactMouseEvent,
|
|
13
|
+
type PointerEvent as ReactPointerEvent,
|
|
14
|
+
} from "react";
|
|
15
|
+
import {
|
|
16
|
+
ChevronLeft,
|
|
17
|
+
ChevronRight,
|
|
18
|
+
FileText,
|
|
19
|
+
Globe,
|
|
20
|
+
LayoutDashboard,
|
|
21
|
+
Lock,
|
|
22
|
+
LockOpen,
|
|
23
|
+
Pin,
|
|
24
|
+
Plus,
|
|
25
|
+
X,
|
|
26
|
+
type LucideIcon,
|
|
27
|
+
} from "lucide-react";
|
|
28
|
+
import { cn } from "@creezio/shell-ui";
|
|
29
|
+
import { PageChrome } from "../layout/page-chrome";
|
|
30
|
+
import { useGlobalSearch } from "../global-search-host";
|
|
31
|
+
import { useTabWorkspace } from "./tab-workspace-host";
|
|
32
|
+
import { WindowChromeControls } from "./window-chrome-controls";
|
|
33
|
+
import { resolvePageKind, isFullscreenHref, type WorkspaceTab } from "./types";
|
|
34
|
+
|
|
35
|
+
/* Géométrie type Chrome : onglets posés en bas du bandeau, largeur adaptative. */
|
|
36
|
+
const TAB_MAX = 240;
|
|
37
|
+
const TAB_MIN = 64;
|
|
38
|
+
const TAB_ABS_MIN = 46;
|
|
39
|
+
const TAB_GAP = 0;
|
|
40
|
+
const NEW_BTN = 28;
|
|
41
|
+
const NEW_BTN_GAP = 6;
|
|
42
|
+
const DRAG_THRESHOLD = 5;
|
|
43
|
+
/** Marge gauche du strip : laisse la place à la courbe inversée du 1er onglet. */
|
|
44
|
+
const PAD_L = 8;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Icônes onglets = plateforme neutre uniquement.
|
|
48
|
+
* Routes métier (panier, fournisseurs, relevés…) → injecter via
|
|
49
|
+
* `configureWorkspaceTabIcons` depuis la marque (pas dans le kit).
|
|
50
|
+
*/
|
|
51
|
+
let brandIconForSeg: ((seg: string) => LucideIcon | null | undefined) | null =
|
|
52
|
+
null;
|
|
53
|
+
|
|
54
|
+
export function configureWorkspaceTabIcons(
|
|
55
|
+
fn: ((seg: string) => LucideIcon | null | undefined) | null,
|
|
56
|
+
): void {
|
|
57
|
+
brandIconForSeg = fn;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function iconForHref(href: string) {
|
|
61
|
+
const seg = (href.split("?")[0] || "/").split("/").filter(Boolean)[0] || "";
|
|
62
|
+
const fromBrand = brandIconForSeg?.(seg);
|
|
63
|
+
if (fromBrand) return fromBrand;
|
|
64
|
+
switch (seg) {
|
|
65
|
+
case "dashboard":
|
|
66
|
+
return LayoutDashboard;
|
|
67
|
+
case "site":
|
|
68
|
+
return Globe;
|
|
69
|
+
default:
|
|
70
|
+
return FileText;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type DragState = {
|
|
75
|
+
id: string;
|
|
76
|
+
pointerId: number;
|
|
77
|
+
startX: number;
|
|
78
|
+
originLeft: number;
|
|
79
|
+
moved: boolean;
|
|
80
|
+
dx: number;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
type TabMenuState = {
|
|
84
|
+
tabId: string;
|
|
85
|
+
x: number;
|
|
86
|
+
y: number;
|
|
87
|
+
} | null;
|
|
88
|
+
|
|
89
|
+
export function WorkspaceTabBar() {
|
|
90
|
+
const {
|
|
91
|
+
tabs,
|
|
92
|
+
activeTabId,
|
|
93
|
+
activateTab,
|
|
94
|
+
closeTab,
|
|
95
|
+
setTabLocked,
|
|
96
|
+
moveTab,
|
|
97
|
+
goBack,
|
|
98
|
+
goForward,
|
|
99
|
+
canGoBack,
|
|
100
|
+
canGoForward,
|
|
101
|
+
activeTab,
|
|
102
|
+
pinnedTabId,
|
|
103
|
+
tabBadges,
|
|
104
|
+
} = useTabWorkspace();
|
|
105
|
+
const { openForNewTab } = useGlobalSearch();
|
|
106
|
+
|
|
107
|
+
const stripRef = useRef<HTMLDivElement>(null);
|
|
108
|
+
const [stripW, setStripW] = useState(0);
|
|
109
|
+
const [animate, setAnimate] = useState(false);
|
|
110
|
+
const [drag, setDrag] = useState<DragState | null>(null);
|
|
111
|
+
const [menu, setMenu] = useState<TabMenuState>(null);
|
|
112
|
+
const dragRef = useRef<DragState | null>(null);
|
|
113
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
114
|
+
const tabsRef = useRef(tabs);
|
|
115
|
+
tabsRef.current = tabs;
|
|
116
|
+
|
|
117
|
+
// Ids déjà vus → les nouveaux onglets ont une animation d'entrée
|
|
118
|
+
const seenIdsRef = useRef<Set<string>>(new Set());
|
|
119
|
+
|
|
120
|
+
useLayoutEffect(() => {
|
|
121
|
+
const el = stripRef.current;
|
|
122
|
+
if (!el) return;
|
|
123
|
+
const measure = () => setStripW(el.clientWidth);
|
|
124
|
+
measure();
|
|
125
|
+
const ro = new ResizeObserver(measure);
|
|
126
|
+
ro.observe(el);
|
|
127
|
+
return () => ro.disconnect();
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
// Active les transitions seulement après la première mesure (pas d'anim au chargement)
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (!stripW || animate) return;
|
|
133
|
+
const raf = requestAnimationFrame(() =>
|
|
134
|
+
requestAnimationFrame(() => setAnimate(true)),
|
|
135
|
+
);
|
|
136
|
+
return () => cancelAnimationFrame(raf);
|
|
137
|
+
}, [stripW, animate]);
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!menu) return;
|
|
141
|
+
function close() {
|
|
142
|
+
setMenu(null);
|
|
143
|
+
}
|
|
144
|
+
function onKey(e: KeyboardEvent) {
|
|
145
|
+
if (e.key === "Escape") close();
|
|
146
|
+
}
|
|
147
|
+
function onPointer(e: MouseEvent) {
|
|
148
|
+
if (menuRef.current?.contains(e.target as Node)) return;
|
|
149
|
+
close();
|
|
150
|
+
}
|
|
151
|
+
window.addEventListener("keydown", onKey);
|
|
152
|
+
window.addEventListener("mousedown", onPointer);
|
|
153
|
+
window.addEventListener("scroll", close, true);
|
|
154
|
+
const focusTimer = window.setTimeout(() => {
|
|
155
|
+
menuRef.current?.querySelector<HTMLButtonElement>("button:not(:disabled)")?.focus();
|
|
156
|
+
}, 0);
|
|
157
|
+
return () => {
|
|
158
|
+
window.removeEventListener("keydown", onKey);
|
|
159
|
+
window.removeEventListener("mousedown", onPointer);
|
|
160
|
+
window.removeEventListener("scroll", close, true);
|
|
161
|
+
window.clearTimeout(focusTimer);
|
|
162
|
+
};
|
|
163
|
+
}, [menu]);
|
|
164
|
+
|
|
165
|
+
const count = tabs.length || 1;
|
|
166
|
+
const avail = Math.max(0, stripW - PAD_L - NEW_BTN - NEW_BTN_GAP);
|
|
167
|
+
const tabW = stripW
|
|
168
|
+
? Math.max(
|
|
169
|
+
TAB_ABS_MIN,
|
|
170
|
+
Math.min(TAB_MAX, Math.max(TAB_MIN, Math.floor(avail / count))),
|
|
171
|
+
)
|
|
172
|
+
: 200;
|
|
173
|
+
const slotW = tabW + TAB_GAP;
|
|
174
|
+
const compact = tabW < 90;
|
|
175
|
+
|
|
176
|
+
const orderIndex = useMemo(() => {
|
|
177
|
+
const m = new Map<string, number>();
|
|
178
|
+
tabs.forEach((t: WorkspaceTab, i: number) => m.set(t.id, i));
|
|
179
|
+
return m;
|
|
180
|
+
}, [tabs]);
|
|
181
|
+
|
|
182
|
+
const clampDragLeft = useCallback(
|
|
183
|
+
(left: number) => {
|
|
184
|
+
const max = PAD_L + Math.max(0, (tabsRef.current.length - 1) * slotW);
|
|
185
|
+
return Math.min(Math.max(PAD_L, left), max);
|
|
186
|
+
},
|
|
187
|
+
[slotW],
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const onTabPointerDown = useCallback(
|
|
191
|
+
(e: ReactPointerEvent<HTMLDivElement>, tabId: string) => {
|
|
192
|
+
if (e.button !== 0) return;
|
|
193
|
+
// Chrome active l'onglet dès le mousedown
|
|
194
|
+
activateTab(tabId);
|
|
195
|
+
const idx = tabsRef.current.findIndex((t: WorkspaceTab) => t.id === tabId);
|
|
196
|
+
if (idx < 0) return;
|
|
197
|
+
const state: DragState = {
|
|
198
|
+
id: tabId,
|
|
199
|
+
pointerId: e.pointerId,
|
|
200
|
+
startX: e.clientX,
|
|
201
|
+
originLeft: PAD_L + idx * slotW,
|
|
202
|
+
moved: false,
|
|
203
|
+
dx: 0,
|
|
204
|
+
};
|
|
205
|
+
dragRef.current = state;
|
|
206
|
+
try {
|
|
207
|
+
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
208
|
+
} catch {
|
|
209
|
+
/* pointerId synthétique (tests) — le drag suit quand même les pointermove */
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
[activateTab, slotW],
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const onTabPointerMove = useCallback(
|
|
216
|
+
(e: ReactPointerEvent<HTMLDivElement>) => {
|
|
217
|
+
const st = dragRef.current;
|
|
218
|
+
if (!st || e.pointerId !== st.pointerId) return;
|
|
219
|
+
const dx = e.clientX - st.startX;
|
|
220
|
+
if (!st.moved && Math.abs(dx) < DRAG_THRESHOLD) return;
|
|
221
|
+
const next = { ...st, moved: true, dx };
|
|
222
|
+
dragRef.current = next;
|
|
223
|
+
setDrag(next);
|
|
224
|
+
|
|
225
|
+
// Slot cible = centre de l'onglet déplacé
|
|
226
|
+
const left = clampDragLeft(st.originLeft + dx);
|
|
227
|
+
const target = Math.min(
|
|
228
|
+
tabsRef.current.length - 1,
|
|
229
|
+
Math.max(0, Math.round((left - PAD_L) / slotW)),
|
|
230
|
+
);
|
|
231
|
+
const current = tabsRef.current.findIndex((t: WorkspaceTab) => t.id === st.id);
|
|
232
|
+
if (target !== current) moveTab(st.id, target);
|
|
233
|
+
},
|
|
234
|
+
[clampDragLeft, moveTab, slotW],
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const endDrag = useCallback((e: ReactPointerEvent<HTMLDivElement>) => {
|
|
238
|
+
const st = dragRef.current;
|
|
239
|
+
if (!st || e.pointerId !== st.pointerId) return;
|
|
240
|
+
dragRef.current = null;
|
|
241
|
+
setDrag(null);
|
|
242
|
+
}, []);
|
|
243
|
+
|
|
244
|
+
const onTitlebarDoubleClick = useCallback(() => {
|
|
245
|
+
if (!getShellDesktopApi()?.customWindowChrome) return;
|
|
246
|
+
void getShellDesktopApi().toggleMaximizeWindow?.();
|
|
247
|
+
}, []);
|
|
248
|
+
|
|
249
|
+
const onTabContextMenu = useCallback(
|
|
250
|
+
(e: ReactMouseEvent<HTMLDivElement>, tabId: string) => {
|
|
251
|
+
e.preventDefault();
|
|
252
|
+
e.stopPropagation();
|
|
253
|
+
dragRef.current = null;
|
|
254
|
+
setDrag(null);
|
|
255
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
256
|
+
const menuWidth = 190;
|
|
257
|
+
const menuHeight = tabId === pinnedTabId ? 48 : 92;
|
|
258
|
+
const pad = 8;
|
|
259
|
+
const anchorX = e.clientX || rect.left + Math.min(rect.width / 2, 48);
|
|
260
|
+
const anchorY = e.clientY || rect.bottom;
|
|
261
|
+
setMenu({
|
|
262
|
+
tabId,
|
|
263
|
+
x: Math.max(pad, Math.min(anchorX, window.innerWidth - menuWidth - pad)),
|
|
264
|
+
y: Math.max(pad, Math.min(anchorY, window.innerHeight - menuHeight - pad)),
|
|
265
|
+
});
|
|
266
|
+
},
|
|
267
|
+
[pinnedTabId],
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const menuTab = menu ? tabs.find((tab: WorkspaceTab) => tab.id === menu.tabId) : null;
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<div className="flex min-w-0 flex-col">
|
|
274
|
+
{/* Bandeau navigateur — drag Electron (frameless Windows) */}
|
|
275
|
+
<div
|
|
276
|
+
className={`${getShellUiBrand().titlebarDragClass} flex h-10 min-w-0 items-stretch`}
|
|
277
|
+
onDoubleClick={onTitlebarDoubleClick}
|
|
278
|
+
>
|
|
279
|
+
{/* Toolbar ← → */}
|
|
280
|
+
<div className={`${getShellUiBrand().titlebarNoDragClass} flex shrink-0 items-center gap-0.5 self-center pr-1`}>
|
|
281
|
+
<button
|
|
282
|
+
type="button"
|
|
283
|
+
disabled={!canGoBack}
|
|
284
|
+
onClick={goBack}
|
|
285
|
+
aria-label="Page précédente"
|
|
286
|
+
title="Précédent"
|
|
287
|
+
className={cn(
|
|
288
|
+
"flex h-7 w-7 items-center justify-center rounded-full text-slate-600 transition-colors",
|
|
289
|
+
canGoBack
|
|
290
|
+
? "hover:bg-slate-400/25 hover:text-slate-900 active:bg-slate-400/40"
|
|
291
|
+
: "cursor-default opacity-35",
|
|
292
|
+
)}
|
|
293
|
+
>
|
|
294
|
+
<ChevronLeft className="h-4 w-4" />
|
|
295
|
+
</button>
|
|
296
|
+
<button
|
|
297
|
+
type="button"
|
|
298
|
+
disabled={!canGoForward}
|
|
299
|
+
onClick={goForward}
|
|
300
|
+
aria-label="Page suivante"
|
|
301
|
+
title="Suivant"
|
|
302
|
+
className={cn(
|
|
303
|
+
"flex h-7 w-7 items-center justify-center rounded-full text-slate-600 transition-colors",
|
|
304
|
+
canGoForward
|
|
305
|
+
? "hover:bg-slate-400/25 hover:text-slate-900 active:bg-slate-400/40"
|
|
306
|
+
: "cursor-default opacity-35",
|
|
307
|
+
)}
|
|
308
|
+
>
|
|
309
|
+
<ChevronRight className="h-4 w-4" />
|
|
310
|
+
</button>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
{/* Rangée d'onglets — layout mesuré, positions animées par transform */}
|
|
314
|
+
<div
|
|
315
|
+
ref={stripRef}
|
|
316
|
+
className="relative min-w-0 flex-1 overflow-hidden"
|
|
317
|
+
role="tablist"
|
|
318
|
+
aria-label="Onglets workspace"
|
|
319
|
+
>
|
|
320
|
+
{tabs.map((tab: WorkspaceTab) => {
|
|
321
|
+
const idx = orderIndex.get(tab.id) ?? 0;
|
|
322
|
+
const active = tab.id === activeTabId;
|
|
323
|
+
const pinned = tab.id === pinnedTabId;
|
|
324
|
+
const locked = pinned || tab.locked === true;
|
|
325
|
+
const badge = tabBadges[tab.id] || 0;
|
|
326
|
+
const dragging = drag?.moved && drag.id === tab.id;
|
|
327
|
+
const left = dragging
|
|
328
|
+
? clampDragLeft(drag!.originLeft + drag!.dx)
|
|
329
|
+
: PAD_L + idx * slotW;
|
|
330
|
+
const isNew = !seenIdsRef.current.has(tab.id);
|
|
331
|
+
if (stripW) seenIdsRef.current.add(tab.id);
|
|
332
|
+
const Icon = iconForHref(tab.href);
|
|
333
|
+
const showClose = !locked && (!compact || active);
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<div
|
|
337
|
+
key={tab.id}
|
|
338
|
+
role="tab"
|
|
339
|
+
aria-selected={active}
|
|
340
|
+
data-state={active ? "active" : "inactive"}
|
|
341
|
+
data-tab-locked={locked ? "true" : "false"}
|
|
342
|
+
onPointerDown={(e) => onTabPointerDown(e, tab.id)}
|
|
343
|
+
onPointerMove={onTabPointerMove}
|
|
344
|
+
onPointerUp={endDrag}
|
|
345
|
+
onPointerCancel={endDrag}
|
|
346
|
+
onContextMenu={(e) => onTabContextMenu(e, tab.id)}
|
|
347
|
+
onAuxClick={(e) => {
|
|
348
|
+
if (e.button === 1) {
|
|
349
|
+
e.preventDefault();
|
|
350
|
+
if (!locked) closeTab(tab.id);
|
|
351
|
+
}
|
|
352
|
+
}}
|
|
353
|
+
title={
|
|
354
|
+
`${tab.subtitle ? `${tab.title} — ${tab.subtitle}` : tab.title}${
|
|
355
|
+
locked ? " — Verrouillé" : ""
|
|
356
|
+
}`
|
|
357
|
+
}
|
|
358
|
+
className={cn(
|
|
359
|
+
`${getShellUiBrand().titlebarNoDragClass} tf-tab group absolute bottom-0 h-[34px] cursor-default select-none [touch-action:none]`,
|
|
360
|
+
animate &&
|
|
361
|
+
!dragging &&
|
|
362
|
+
"transition-[transform,width] duration-200 ease-out",
|
|
363
|
+
dragging && "z-20",
|
|
364
|
+
active && !dragging && "z-10",
|
|
365
|
+
active && "is-active",
|
|
366
|
+
)}
|
|
367
|
+
style={{
|
|
368
|
+
width: tabW,
|
|
369
|
+
transform: `translateX(${left}px)`,
|
|
370
|
+
}}
|
|
371
|
+
>
|
|
372
|
+
{/* Séparateur gauche (masqué près de l'actif / au hover) */}
|
|
373
|
+
<span
|
|
374
|
+
aria-hidden
|
|
375
|
+
className="tf-sep pointer-events-none absolute left-0 top-[9px] h-4 w-px bg-slate-500/40 transition-opacity duration-100"
|
|
376
|
+
/>
|
|
377
|
+
|
|
378
|
+
{/* Fond de l'onglet actif : blanc + courbes inversées, fusion avec la page */}
|
|
379
|
+
{active ? (
|
|
380
|
+
<span
|
|
381
|
+
aria-hidden
|
|
382
|
+
className="tf-tab-bg pointer-events-none absolute inset-0 rounded-t-lg bg-white shadow-[0_1px_3px_rgba(0,0,0,0.08)]"
|
|
383
|
+
/>
|
|
384
|
+
) : (
|
|
385
|
+
<span
|
|
386
|
+
aria-hidden
|
|
387
|
+
className={cn(
|
|
388
|
+
"pointer-events-none absolute inset-x-0 bottom-[2px] top-0 rounded-lg transition-colors duration-100",
|
|
389
|
+
dragging
|
|
390
|
+
? "bg-white/70 shadow-[0_2px_8px_rgba(0,0,0,0.18)]"
|
|
391
|
+
: "group-hover:bg-[rgba(255,255,255,0.55)]",
|
|
392
|
+
)}
|
|
393
|
+
/>
|
|
394
|
+
)}
|
|
395
|
+
|
|
396
|
+
{/* Contenu */}
|
|
397
|
+
<div
|
|
398
|
+
className={cn(
|
|
399
|
+
"relative flex h-full min-w-0 items-center pl-2.5",
|
|
400
|
+
showClose ? "pr-1" : "pr-2",
|
|
401
|
+
isNew && animate && "tf-tab-enter",
|
|
402
|
+
)}
|
|
403
|
+
>
|
|
404
|
+
{pinned ? (
|
|
405
|
+
<Pin
|
|
406
|
+
className={cn(
|
|
407
|
+
"h-[13px] w-[13px] shrink-0 fill-current",
|
|
408
|
+
active ? "text-sky-600" : "text-slate-500",
|
|
409
|
+
)}
|
|
410
|
+
aria-label="Onglet épinglé"
|
|
411
|
+
/>
|
|
412
|
+
) : locked ? (
|
|
413
|
+
<Lock
|
|
414
|
+
className={cn(
|
|
415
|
+
"h-[13px] w-[13px] shrink-0",
|
|
416
|
+
active ? "text-sky-600" : "text-slate-500",
|
|
417
|
+
)}
|
|
418
|
+
aria-label="Onglet verrouillé"
|
|
419
|
+
/>
|
|
420
|
+
) : (
|
|
421
|
+
<Icon
|
|
422
|
+
className={cn(
|
|
423
|
+
"h-[15px] w-[15px] shrink-0",
|
|
424
|
+
active ? "text-slate-700" : "text-slate-500",
|
|
425
|
+
)}
|
|
426
|
+
/>
|
|
427
|
+
)}
|
|
428
|
+
{tabW >= 72 ? (
|
|
429
|
+
<span
|
|
430
|
+
className={cn(
|
|
431
|
+
"tf-tab-title ml-2 min-w-0 flex-1 truncate text-[12px] leading-none tracking-tight",
|
|
432
|
+
active
|
|
433
|
+
? "font-medium text-slate-900"
|
|
434
|
+
: "text-slate-700",
|
|
435
|
+
)}
|
|
436
|
+
>
|
|
437
|
+
{tab.title}
|
|
438
|
+
</span>
|
|
439
|
+
) : (
|
|
440
|
+
<span className="min-w-0 flex-1" />
|
|
441
|
+
)}
|
|
442
|
+
{badge > 0 ? (
|
|
443
|
+
<span
|
|
444
|
+
aria-label={`${badge} nouveauté${badge > 1 ? "s" : ""}`}
|
|
445
|
+
className={cn(
|
|
446
|
+
"pointer-events-none absolute -top-0.5 z-30 flex h-4 min-w-4 items-center justify-center rounded-full bg-red-500 px-1 text-[10px] font-bold leading-none text-white shadow-sm ring-2 ring-[#ebe7df]",
|
|
447
|
+
showClose ? "right-1" : "right-1.5",
|
|
448
|
+
)}
|
|
449
|
+
>
|
|
450
|
+
{badge > 9 ? "9+" : badge}
|
|
451
|
+
</span>
|
|
452
|
+
) : null}
|
|
453
|
+
{showClose ? (
|
|
454
|
+
<button
|
|
455
|
+
type="button"
|
|
456
|
+
tabIndex={-1}
|
|
457
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
458
|
+
onClick={(e) => {
|
|
459
|
+
e.stopPropagation();
|
|
460
|
+
closeTab(tab.id);
|
|
461
|
+
}}
|
|
462
|
+
className={cn(
|
|
463
|
+
"ml-0.5 mr-1 flex h-[18px] w-[18px] shrink-0 items-center justify-center rounded-full transition-colors",
|
|
464
|
+
active
|
|
465
|
+
? "text-slate-500 hover:bg-slate-200 hover:text-slate-800 active:bg-slate-300"
|
|
466
|
+
: "text-slate-500 opacity-0 hover:bg-slate-400/30 hover:text-slate-800 focus:opacity-100 group-hover:opacity-100",
|
|
467
|
+
)}
|
|
468
|
+
aria-label={`Fermer ${tab.title}`}
|
|
469
|
+
title="Fermer"
|
|
470
|
+
>
|
|
471
|
+
<X className="h-3 w-3" strokeWidth={2.25} />
|
|
472
|
+
</button>
|
|
473
|
+
) : null}
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
);
|
|
477
|
+
})}
|
|
478
|
+
|
|
479
|
+
{/* Bouton nouvel onglet — ouvre la recherche, le résultat choisi
|
|
480
|
+
s'ouvrira dans un nouvel onglet (façon Notion) */}
|
|
481
|
+
<button
|
|
482
|
+
type="button"
|
|
483
|
+
onClick={openForNewTab}
|
|
484
|
+
aria-label="Nouvel onglet"
|
|
485
|
+
title="Nouvel onglet (recherche)"
|
|
486
|
+
className={cn(
|
|
487
|
+
`${getShellUiBrand().titlebarNoDragClass} absolute bottom-[6px] flex h-7 w-7 items-center justify-center rounded-full text-slate-600 hover:bg-slate-400/25 hover:text-slate-900 active:bg-slate-400/40`,
|
|
488
|
+
animate && "transition-transform duration-200 ease-out",
|
|
489
|
+
)}
|
|
490
|
+
style={{
|
|
491
|
+
transform: `translateX(${Math.min(
|
|
492
|
+
PAD_L + count * slotW + NEW_BTN_GAP,
|
|
493
|
+
Math.max(0, stripW - NEW_BTN),
|
|
494
|
+
)}px)`,
|
|
495
|
+
}}
|
|
496
|
+
>
|
|
497
|
+
<Plus className="h-4 w-4" />
|
|
498
|
+
</button>
|
|
499
|
+
</div>
|
|
500
|
+
|
|
501
|
+
<WindowChromeControls />
|
|
502
|
+
</div>
|
|
503
|
+
|
|
504
|
+
{menu && menuTab ? (
|
|
505
|
+
<div
|
|
506
|
+
ref={menuRef}
|
|
507
|
+
role="menu"
|
|
508
|
+
aria-label={`Actions pour l’onglet ${menuTab.title}`}
|
|
509
|
+
className={`${getShellUiBrand().titlebarNoDragClass} fixed z-[100] min-w-[11.875rem] rounded-md border border-slate-200 bg-white p-1 text-slate-900 shadow-lg`}
|
|
510
|
+
style={{ left: menu.x, top: menu.y }}
|
|
511
|
+
>
|
|
512
|
+
{menuTab.id === pinnedTabId ? (
|
|
513
|
+
<button
|
|
514
|
+
type="button"
|
|
515
|
+
role="menuitem"
|
|
516
|
+
disabled
|
|
517
|
+
className="flex w-full cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm text-slate-500 opacity-70"
|
|
518
|
+
>
|
|
519
|
+
<Lock className="h-3.5 w-3.5" />
|
|
520
|
+
Verrouillé par défaut
|
|
521
|
+
</button>
|
|
522
|
+
) : (
|
|
523
|
+
<>
|
|
524
|
+
<button
|
|
525
|
+
type="button"
|
|
526
|
+
role="menuitem"
|
|
527
|
+
onClick={() => {
|
|
528
|
+
setTabLocked(menuTab.id, !menuTab.locked);
|
|
529
|
+
setMenu(null);
|
|
530
|
+
}}
|
|
531
|
+
className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm outline-none hover:bg-slate-100 focus:bg-slate-100"
|
|
532
|
+
>
|
|
533
|
+
{menuTab.locked ? (
|
|
534
|
+
<LockOpen className="h-3.5 w-3.5" />
|
|
535
|
+
) : (
|
|
536
|
+
<Lock className="h-3.5 w-3.5" />
|
|
537
|
+
)}
|
|
538
|
+
{menuTab.locked ? "Déverrouiller" : "Verrouiller"}
|
|
539
|
+
</button>
|
|
540
|
+
{!menuTab.locked ? (
|
|
541
|
+
<>
|
|
542
|
+
<div className="my-1 h-px bg-slate-100" role="separator" />
|
|
543
|
+
<button
|
|
544
|
+
type="button"
|
|
545
|
+
role="menuitem"
|
|
546
|
+
onClick={() => {
|
|
547
|
+
closeTab(menuTab.id);
|
|
548
|
+
setMenu(null);
|
|
549
|
+
}}
|
|
550
|
+
className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm outline-none hover:bg-slate-100 focus:bg-slate-100"
|
|
551
|
+
>
|
|
552
|
+
<X className="h-3.5 w-3.5" />
|
|
553
|
+
Fermer
|
|
554
|
+
</button>
|
|
555
|
+
</>
|
|
556
|
+
) : null}
|
|
557
|
+
</>
|
|
558
|
+
)}
|
|
559
|
+
</div>
|
|
560
|
+
) : null}
|
|
561
|
+
|
|
562
|
+
{/* Bandeau sticky sous les onglets — recherche + trail (entity) + actions */}
|
|
563
|
+
{activeTab && !activeTab.fullscreen && !isFullscreenHref(activeTab.href) ? (
|
|
564
|
+
<PageChrome
|
|
565
|
+
kind={resolvePageKind(activeTab)}
|
|
566
|
+
href={activeTab.href}
|
|
567
|
+
trail={activeTab.trail}
|
|
568
|
+
/>
|
|
569
|
+
) : null}
|
|
570
|
+
</div>
|
|
571
|
+
);
|
|
572
|
+
}
|