@workspace-platform/ui 0.5.0 → 0.6.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.
@@ -0,0 +1,735 @@
1
+ // src/api/client.ts
2
+ function createWorkspaceClient(config) {
3
+ function buildHeaders(extra) {
4
+ const base = {
5
+ "Content-Type": "application/json",
6
+ ...extra
7
+ };
8
+ const token = config.getAuthToken();
9
+ if (token) base["Authorization"] = `Bearer ${token}`;
10
+ if (config.extraHeaders) Object.assign(base, config.extraHeaders);
11
+ return base;
12
+ }
13
+ async function req(path, options) {
14
+ const res = await fetch(`${config.baseUrl}${path}`, {
15
+ ...options,
16
+ headers: buildHeaders(options?.headers)
17
+ });
18
+ if (res.status === 204) return void 0;
19
+ const json = await res.json();
20
+ if (!res.ok) {
21
+ const rawMessage = json?.message ?? `Error ${res.status}`;
22
+ const errorMessage = Array.isArray(rawMessage) ? rawMessage.join(", ") : typeof rawMessage === "string" ? rawMessage : JSON.stringify(rawMessage);
23
+ throw new Error(errorMessage);
24
+ }
25
+ return json;
26
+ }
27
+ async function reqBlob(path) {
28
+ const res = await fetch(`${config.baseUrl}${path}`, {
29
+ headers: buildHeaders()
30
+ });
31
+ if (!res.ok) {
32
+ const text = await res.text().catch(() => "");
33
+ throw new Error(`Error ${res.status}: ${text}`);
34
+ }
35
+ return res.blob();
36
+ }
37
+ async function upload(path, formData) {
38
+ const headers = {};
39
+ const token = config.getAuthToken();
40
+ if (token) headers["Authorization"] = `Bearer ${token}`;
41
+ if (config.extraHeaders) Object.assign(headers, config.extraHeaders);
42
+ const res = await fetch(`${config.baseUrl}${path}`, {
43
+ method: "POST",
44
+ headers,
45
+ body: formData
46
+ });
47
+ if (!res.ok) {
48
+ const json = await res.json().catch(() => ({}));
49
+ throw new Error(
50
+ json?.message ?? `Upload failed (${res.status})`
51
+ );
52
+ }
53
+ return res.json();
54
+ }
55
+ return {
56
+ get: (path) => req(path),
57
+ post: (path, body) => req(path, { method: "POST", body: JSON.stringify(body) }),
58
+ patch: (path, body) => req(path, { method: "PATCH", body: JSON.stringify(body) }),
59
+ delete: (path) => req(path, { method: "DELETE" }),
60
+ upload: (path, formData) => upload(path, formData),
61
+ getBlob: (path) => reqBlob(path)
62
+ };
63
+ }
64
+
65
+ // src/socket/client.ts
66
+ import { io } from "socket.io-client";
67
+ function createWorkspaceSocket(config) {
68
+ let socket = null;
69
+ function getSocket() {
70
+ if (!socket) {
71
+ const token = config.getAuthToken();
72
+ socket = io(`${config.baseUrl}/workspaces`, {
73
+ autoConnect: false,
74
+ auth: { token: token ?? "" }
75
+ });
76
+ }
77
+ return socket;
78
+ }
79
+ function connect(userId) {
80
+ const s = getSocket();
81
+ if (!s.connected) s.connect();
82
+ const uid = userId ?? config.currentUserId;
83
+ if (uid) {
84
+ if (s.connected) {
85
+ s.emit("authenticate", { userId: uid });
86
+ } else {
87
+ s.once("connect", () => {
88
+ s.emit("authenticate", { userId: uid });
89
+ });
90
+ }
91
+ }
92
+ return s;
93
+ }
94
+ function disconnect() {
95
+ socket?.disconnect();
96
+ socket = null;
97
+ }
98
+ return { getSocket, connect, disconnect };
99
+ }
100
+
101
+ // src/theme.ts
102
+ var mix = (color, amount, other) => `color-mix(in srgb, ${color} ${amount}%, ${other})`;
103
+ function buildThemeVars(theme, scheme = "light") {
104
+ const vars = {};
105
+ if (!theme) return vars;
106
+ const dark = scheme === "dark";
107
+ const tintBase = dark ? "#0b1220" : "#fff";
108
+ const set = (name, value) => {
109
+ if (value != null && value !== "") vars[`--ws-${name}`] = value;
110
+ };
111
+ if (theme.primary) {
112
+ set("primary", theme.primary);
113
+ set("primary-hover", dark ? mix(theme.primary, 82, "#fff") : mix(theme.primary, 86, "#000"));
114
+ set("primary-light", mix(theme.primary, dark ? 26 : 12, tintBase));
115
+ set("primary-50", mix(theme.primary, dark ? 14 : 6, tintBase));
116
+ set("primary-100", mix(theme.primary, dark ? 20 : 14, tintBase));
117
+ set("primary-200", mix(theme.primary, dark ? 34 : 28, tintBase));
118
+ }
119
+ set("primary-contrast", theme.primaryContrast);
120
+ if (theme.secondary) {
121
+ set("secondary", theme.secondary);
122
+ set("secondary-hover", dark ? mix(theme.secondary, 82, "#fff") : mix(theme.secondary, 86, "#000"));
123
+ set("secondary-light", mix(theme.secondary, dark ? 26 : 12, tintBase));
124
+ }
125
+ set("secondary-contrast", theme.secondaryContrast);
126
+ set("bg", theme.bg);
127
+ set("bg-subtle", theme.bgSubtle);
128
+ set("surface", theme.surface);
129
+ set("surface-hover", theme.surfaceHover);
130
+ set("border", theme.border);
131
+ set("border-light", theme.borderLight);
132
+ set("text", theme.text);
133
+ set("text-secondary", theme.textSecondary);
134
+ set("text-muted", theme.textMuted);
135
+ set("text-placeholder", theme.textPlaceholder);
136
+ set("danger", theme.danger);
137
+ set("success", theme.success);
138
+ set("warning", theme.warning);
139
+ set("info", theme.info);
140
+ return vars;
141
+ }
142
+
143
+ // src/theme-context.tsx
144
+ import { createContext, useContext } from "react";
145
+ var EMPTY = { className: "", style: {} };
146
+ var WorkspaceThemeContext = createContext(EMPTY);
147
+ function useWorkspaceThemeAttrs() {
148
+ return useContext(WorkspaceThemeContext);
149
+ }
150
+
151
+ // src/styling.ts
152
+ import { createContext as createContext2, useContext as useContext2 } from "react";
153
+ import { clsx } from "clsx";
154
+ import { twMerge } from "tailwind-merge";
155
+ var EMPTY2 = {};
156
+ var WorkspaceStylesContext = createContext2(EMPTY2);
157
+ function useWorkspaceClassNames() {
158
+ return useContext2(WorkspaceStylesContext);
159
+ }
160
+ function pickSlot(override, slot) {
161
+ if (!override) return void 0;
162
+ if (typeof override === "string") return slot === "root" ? override : void 0;
163
+ return override[slot];
164
+ }
165
+ function useSlots(keys, local, className) {
166
+ const registry = useContext2(WorkspaceStylesContext);
167
+ const keyList = Array.isArray(keys) ? keys : [keys];
168
+ return (slot, ...base) => twMerge(
169
+ clsx(
170
+ ...base,
171
+ ...keyList.map((key) => pickSlot(registry[key], slot)),
172
+ pickSlot(local, slot),
173
+ slot === "root" ? className : void 0
174
+ )
175
+ );
176
+ }
177
+
178
+ // src/components/ui/cn.ts
179
+ import { clsx as clsx2 } from "clsx";
180
+ import { twMerge as twMerge2 } from "tailwind-merge";
181
+ function cn(...inputs) {
182
+ return twMerge2(clsx2(inputs));
183
+ }
184
+
185
+ // src/provider.tsx
186
+ import { createContext as createContext3, useContext as useContext3, useMemo } from "react";
187
+ import { jsx } from "react/jsx-runtime";
188
+ var WorkspaceContext = createContext3(null);
189
+ var EMPTY_CLASS_NAMES = {};
190
+ function WorkspaceProvider({
191
+ config,
192
+ children
193
+ }) {
194
+ const value = useMemo(() => {
195
+ const apiClient = createWorkspaceClient(config);
196
+ const socket = createWorkspaceSocket(config);
197
+ return { config, apiClient, socket };
198
+ }, [config.baseUrl, config.currentUserId]);
199
+ const isDark = config.colorScheme === "dark";
200
+ const themeVars = useMemo(
201
+ () => buildThemeVars(config.theme, config.colorScheme),
202
+ [config.theme, config.colorScheme]
203
+ );
204
+ const themeClassName = cn("ws-root", isDark && "ws-theme-dark");
205
+ const themeAttrs = useMemo(
206
+ () => ({ className: themeClassName, style: themeVars }),
207
+ [themeClassName, themeVars]
208
+ );
209
+ const classNames = config.classNames ?? EMPTY_CLASS_NAMES;
210
+ return /* @__PURE__ */ jsx(WorkspaceContext.Provider, { value, children: /* @__PURE__ */ jsx(WorkspaceStylesContext.Provider, { value: classNames, children: /* @__PURE__ */ jsx(WorkspaceThemeContext.Provider, { value: themeAttrs, children: /* @__PURE__ */ jsx(
211
+ "div",
212
+ {
213
+ className: themeClassName,
214
+ style: { display: "contents", ...themeVars },
215
+ children
216
+ }
217
+ ) }) }) });
218
+ }
219
+ function useWorkspaceContext() {
220
+ const ctx = useContext3(WorkspaceContext);
221
+ if (!ctx)
222
+ throw new Error(
223
+ "useWorkspaceContext must be used within <WorkspaceProvider>"
224
+ );
225
+ return ctx;
226
+ }
227
+
228
+ // src/api/index.ts
229
+ function createWorkspaceApis(client) {
230
+ const workspaceTypeApi = {
231
+ list: () => client.get("/workspace-types"),
232
+ get: (id) => client.get(`/workspace-types/${id}`),
233
+ create: (payload) => client.post("/workspace-types", payload),
234
+ update: (id, payload) => client.patch(`/workspace-types/${id}`, payload),
235
+ delete: (id) => client.delete(`/workspace-types/${id}`)
236
+ };
237
+ const workspaceApi = {
238
+ list: (params) => {
239
+ const cleanParams = Object.fromEntries(
240
+ Object.entries(params || {}).filter(([, v]) => v != null && v !== "" && v !== "all").map(([k, v]) => [k, String(v)])
241
+ );
242
+ const qs = Object.keys(cleanParams).length > 0 ? "?" + new URLSearchParams(cleanParams).toString() : "";
243
+ return client.get(`/workspaces${qs}`);
244
+ },
245
+ get: (id) => client.get(`/workspaces/${id}`),
246
+ create: (payload) => client.post("/workspaces", payload),
247
+ update: (id, payload) => client.patch(`/workspaces/${id}`, payload),
248
+ updateStatus: (id, status) => client.patch(`/workspaces/${id}/status`, { status }),
249
+ delete: (id) => client.delete(`/workspaces/${id}`),
250
+ startTyping: (id) => client.post(`/workspaces/${id}/writing/start`, {}),
251
+ stopTyping: (id) => client.post(`/workspaces/${id}/writing/stop`, {}),
252
+ timeline: (workspaceId, params) => {
253
+ const qs = params ? "?" + new URLSearchParams(
254
+ Object.fromEntries(
255
+ Object.entries(params).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)])
256
+ )
257
+ ).toString() : "";
258
+ return client.get(`/workspaces/${workspaceId}/timeline${qs}`);
259
+ }
260
+ };
261
+ const memberApi = {
262
+ list: (workspaceId) => client.get(`/workspaces/${workspaceId}/members`),
263
+ add: (workspaceId, payload) => client.post(`/workspaces/${workspaceId}/members`, payload),
264
+ updateRole: (workspaceId, memberId, role) => client.patch(`/workspaces/${workspaceId}/members/${memberId}`, { role }),
265
+ remove: (workspaceId, memberId) => client.delete(`/workspaces/${workspaceId}/members/${memberId}`)
266
+ };
267
+ const messageApi = {
268
+ list: (workspaceId, params) => {
269
+ const qs = params ? "?" + new URLSearchParams(
270
+ Object.fromEntries(
271
+ Object.entries(params).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)])
272
+ )
273
+ ).toString() : "";
274
+ return client.get(`/workspaces/${workspaceId}/messages${qs}`);
275
+ },
276
+ send: (workspaceId, payload) => client.post(`/workspaces/${workspaceId}/messages`, payload),
277
+ update: (workspaceId, messageId, payload) => client.patch(`/workspaces/${workspaceId}/messages/${messageId}`, payload),
278
+ delete: (workspaceId, messageId) => client.delete(`/workspaces/${workspaceId}/messages/${messageId}`),
279
+ togglePin: (workspaceId, messageId) => client.patch(`/workspaces/${workspaceId}/messages/${messageId}/pin`, {})
280
+ };
281
+ const artefactApi = {
282
+ upload: async (workspaceId, files, messageId, artefactType) => {
283
+ const formData = new FormData();
284
+ files.forEach((f) => formData.append("files", f));
285
+ if (messageId) formData.append("messageId", messageId);
286
+ if (artefactType) formData.append("type", artefactType);
287
+ return client.upload(`/workspaces/${workspaceId}/artefacts`, formData);
288
+ },
289
+ list: (workspaceId, artefactType) => {
290
+ const query = artefactType ? `?type=${artefactType}` : "";
291
+ return client.get(
292
+ `/workspaces/${workspaceId}/artefacts${query}`
293
+ );
294
+ },
295
+ listPaginated: (workspaceId, params) => {
296
+ const entries = [];
297
+ if (params) {
298
+ if (params.type) entries.push(["type", params.type]);
299
+ if (params.limit != null) entries.push(["limit", String(params.limit)]);
300
+ if (params.offset != null) entries.push(["offset", String(params.offset)]);
301
+ }
302
+ const qs = entries.length > 0 ? "?" + new URLSearchParams(entries).toString() : "";
303
+ return client.get(
304
+ `/workspaces/${workspaceId}/artefacts${qs}`
305
+ );
306
+ },
307
+ delete: (workspaceId, artefactId) => client.delete(`/workspaces/${workspaceId}/artefacts/${artefactId}`),
308
+ getSignedUrl: (fileId) => client.get(`/files/${fileId}/signed-url`),
309
+ getDownloadBlob: (fileId) => client.getBlob(`/files/${fileId}/download`),
310
+ messagesWithArtefacts: (workspaceId, params) => {
311
+ const entries = [];
312
+ if (params) {
313
+ if (params.limit != null) entries.push(["limit", String(params.limit)]);
314
+ if (params.offset != null) entries.push(["offset", String(params.offset)]);
315
+ if (params.sort) entries.push(["sort", params.sort]);
316
+ if (params.members && params.members.length > 0)
317
+ entries.push(["members", params.members.join(",")]);
318
+ if (params.concernsMe) entries.push(["concernsMe", "true"]);
319
+ if (params.pinned) entries.push(["pinned", "true"]);
320
+ }
321
+ const qs = entries.length > 0 ? "?" + new URLSearchParams(entries).toString() : "";
322
+ return client.get(
323
+ `/workspaces/${workspaceId}/messages/with-artefacts${qs}`
324
+ );
325
+ }
326
+ };
327
+ const reactionApi = {
328
+ toggle: (workspaceId, messageId, reaction) => client.post(
329
+ `/workspaces/${workspaceId}/messages/${messageId}/reactions/toggle`,
330
+ { reaction }
331
+ )
332
+ };
333
+ const agentApi = {
334
+ list: (search) => {
335
+ const qs = search ? `?search=${encodeURIComponent(search)}` : "";
336
+ return client.get(`/workspace-agents${qs}`);
337
+ },
338
+ create: (body) => client.post("/workspace-agents", body),
339
+ update: (id, body) => client.patch(`/workspace-agents/${id}`, body),
340
+ delete: (id) => client.delete(`/workspace-agents/${id}`),
341
+ getDeleteInfo: (id) => client.get(`/workspace-agents/${id}/delete-info`)
342
+ };
343
+ const summaryApi = {
344
+ generate: (workspaceId) => client.post(`/workspaces/${workspaceId}/summaries/generate`, {}),
345
+ getLatest: (workspaceId) => client.get(`/workspaces/${workspaceId}/summaries/latest`),
346
+ getHistory: (workspaceId) => client.get(`/workspaces/${workspaceId}/summaries`),
347
+ create: (workspaceId, payload) => client.post(`/workspaces/${workspaceId}/summaries`, payload),
348
+ update: (workspaceId, summaryId, payload) => client.patch(
349
+ `/workspaces/${workspaceId}/summaries/${summaryId}`,
350
+ payload
351
+ )
352
+ };
353
+ const webhookApi = {
354
+ list: (params) => {
355
+ const cleanParams = Object.fromEntries(
356
+ Object.entries(params || {}).filter(([, v]) => v !== void 0 && v !== "")
357
+ );
358
+ const qs = Object.keys(cleanParams).length > 0 ? "?" + new URLSearchParams(cleanParams).toString() : "";
359
+ return client.get(`/webhook-subscriptions${qs}`);
360
+ },
361
+ create: (data) => client.post("/webhook-subscriptions", data),
362
+ update: (id, data) => client.patch(`/webhook-subscriptions/${id}`, data),
363
+ delete: (id) => client.delete(`/webhook-subscriptions/${id}`)
364
+ };
365
+ const syncApi = {
366
+ getAvailableProviders: () => client.get("/workspaces/sync/providers").then((res) => res.providers),
367
+ getStatus: (workspaceId) => client.get(`/workspaces/${workspaceId}/sync-status`),
368
+ toggleSync: (workspaceId, provider, enabled) => client.patch(`/workspaces/${workspaceId}/sync`, { provider, enabled })
369
+ };
370
+ return {
371
+ workspaceTypeApi,
372
+ workspaceApi,
373
+ memberApi,
374
+ messageApi,
375
+ artefactApi,
376
+ reactionApi,
377
+ agentApi,
378
+ summaryApi,
379
+ webhookApi,
380
+ syncApi
381
+ };
382
+ }
383
+
384
+ // src/hooks/useWorkspaceApi.ts
385
+ import { useMemo as useMemo2 } from "react";
386
+ function useWorkspaceApi() {
387
+ const { apiClient } = useWorkspaceContext();
388
+ return useMemo2(() => createWorkspaceApis(apiClient), [apiClient]);
389
+ }
390
+
391
+ // src/components/ui/Modal.tsx
392
+ import { useEffect, useId, useState } from "react";
393
+ import { createPortal } from "react-dom";
394
+ import { X } from "lucide-react";
395
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
396
+ var MODAL_SIZE_CLASS = {
397
+ sm: "w-full min-w-0 max-w-sm",
398
+ md: "w-full min-w-0 max-w-md",
399
+ lg: "w-full min-w-0 max-w-lg",
400
+ xl: "w-full min-w-0 max-w-xl",
401
+ "2xl": "w-full min-w-0 max-w-2xl",
402
+ "3xl": "w-full min-w-0 max-w-3xl",
403
+ "4xl": "w-full min-w-0 max-w-4xl",
404
+ "5xl": "w-full min-w-0 max-w-5xl",
405
+ "6xl": "w-full min-w-0 max-w-6xl",
406
+ "7xl": "w-full min-w-0 max-w-7xl",
407
+ full: "w-full min-w-0 max-w-[calc(100vw-2rem)] sm:max-w-[min(96vw,90rem)]"
408
+ };
409
+ function Modal({
410
+ open,
411
+ onClose,
412
+ children,
413
+ zIndexClass = "z-[100]",
414
+ backdropClassName = "bg-black/40 backdrop-blur-sm",
415
+ size,
416
+ panelClassName,
417
+ className,
418
+ closeOnBackdropClick = false,
419
+ closeOnEscape = true,
420
+ ariaLabelledBy: ariaLabelledByProp,
421
+ ariaDescribedBy: ariaDescribedByProp,
422
+ title,
423
+ description,
424
+ titleIcon,
425
+ headingId,
426
+ modalHeaderVariant = "strip",
427
+ hideCloseButton = false,
428
+ headerTrailing,
429
+ closeButtonAriaLabel = "Fermer",
430
+ closeButtonDisabled = false,
431
+ closeButtonClassName,
432
+ closeIconSize = 20,
433
+ headerClassName,
434
+ titleClassName,
435
+ descriptionClassName = "text-text-secondary mt-1 text-sm",
436
+ classNames
437
+ }) {
438
+ const slot = useSlots("modal", classNames, className);
439
+ const [mounted, setMounted] = useState(false);
440
+ const autoHeadingId = useId();
441
+ const autoDescIdByModal = useId();
442
+ const themeAttrs = useWorkspaceThemeAttrs();
443
+ const hasHeader = title != null;
444
+ const resolvedHeadingId = headingId ?? ariaLabelledByProp ?? autoHeadingId;
445
+ const assignAutoDescriptionId = hasHeader && description != null && ariaDescribedByProp === void 0 && (typeof description === "string" || typeof description === "number");
446
+ const resolvedAriaDescribedBy = assignAutoDescriptionId ? autoDescIdByModal : ariaDescribedByProp;
447
+ const resolvedAriaLabelledBy = hasHeader ? ariaLabelledByProp ?? resolvedHeadingId : ariaLabelledByProp;
448
+ useEffect(() => {
449
+ setMounted(true);
450
+ }, []);
451
+ useEffect(() => {
452
+ if (!open) return;
453
+ const prevOverflow = document.body.style.overflow;
454
+ document.body.style.overflow = "hidden";
455
+ return () => {
456
+ document.body.style.overflow = prevOverflow;
457
+ };
458
+ }, [open]);
459
+ useEffect(() => {
460
+ if (!open || !closeOnEscape) return;
461
+ const onKeyDown = (e) => {
462
+ if (e.key === "Escape") onClose();
463
+ };
464
+ window.addEventListener("keydown", onKeyDown);
465
+ return () => window.removeEventListener("keydown", onKeyDown);
466
+ }, [open, closeOnEscape, onClose]);
467
+ const defaultCloseClasses = closeButtonClassName ?? "text-text-secondary hover:text-text rounded p-1 transition-colors";
468
+ const trailing = headerTrailing ?? (!hideCloseButton ? /* @__PURE__ */ jsx2(
469
+ "button",
470
+ {
471
+ type: "button",
472
+ disabled: closeButtonDisabled,
473
+ onClick: onClose,
474
+ className: slot(
475
+ "close",
476
+ "ws-modal__close",
477
+ defaultCloseClasses,
478
+ closeButtonDisabled && "opacity-40"
479
+ ),
480
+ "aria-label": closeButtonAriaLabel,
481
+ children: /* @__PURE__ */ jsx2(X, { size: closeIconSize })
482
+ }
483
+ ) : null);
484
+ const descriptionSlot = description == null ? null : typeof description === "string" || typeof description === "number" ? /* @__PURE__ */ jsx2(
485
+ "p",
486
+ {
487
+ id: assignAutoDescriptionId ? autoDescIdByModal : void 0,
488
+ className: slot("description", "ws-modal__description", descriptionClassName),
489
+ children: description
490
+ }
491
+ ) : description;
492
+ const headerShellClass = modalHeaderVariant === "strip" ? "border-border flex shrink-0 items-start justify-between gap-3 border-b px-6 py-4" : "mb-4 flex shrink-0 items-start justify-between gap-3";
493
+ const modalHeaderMarkup = hasHeader ? /* @__PURE__ */ jsxs("div", { className: slot("header", "ws-modal__header", headerShellClass, headerClassName), children: [
494
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-start gap-3", children: [
495
+ titleIcon != null ? /* @__PURE__ */ jsx2("div", { className: "mt-0.5 shrink-0", children: titleIcon }) : null,
496
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
497
+ /* @__PURE__ */ jsx2(
498
+ "h2",
499
+ {
500
+ id: resolvedHeadingId,
501
+ className: slot(
502
+ "title",
503
+ "ws-modal__title",
504
+ "text-text text-lg font-semibold",
505
+ titleClassName
506
+ ),
507
+ children: title
508
+ }
509
+ ),
510
+ descriptionSlot
511
+ ] })
512
+ ] }),
513
+ trailing ? /* @__PURE__ */ jsx2("div", { className: "flex shrink-0 items-start", children: trailing }) : null
514
+ ] }) : null;
515
+ if (!mounted || !open) return null;
516
+ return createPortal(
517
+ /* @__PURE__ */ jsx2(
518
+ "div",
519
+ {
520
+ className: slot(
521
+ "root",
522
+ "ws-modal",
523
+ "fixed inset-0 flex items-center justify-center p-4",
524
+ themeAttrs.className,
525
+ zIndexClass,
526
+ backdropClassName
527
+ ),
528
+ style: themeAttrs.style,
529
+ onClick: closeOnBackdropClick ? onClose : void 0,
530
+ "aria-hidden": false,
531
+ children: /* @__PURE__ */ jsxs(
532
+ "div",
533
+ {
534
+ role: "dialog",
535
+ "aria-modal": "true",
536
+ "aria-labelledby": resolvedAriaLabelledBy,
537
+ "aria-describedby": resolvedAriaDescribedBy,
538
+ className: slot(
539
+ "panel",
540
+ "ws-modal__panel",
541
+ // Solid, opaque panel by default — guarantees the modal is never
542
+ // transparent even if the caller forgets to pass `panelClassName`.
543
+ "bg-surface text-text border-border relative flex max-h-[calc(100vh-2rem)] w-full min-w-0 flex-col overflow-hidden rounded-2xl border shadow-panel animate-ws-scale-in",
544
+ size ? MODAL_SIZE_CLASS[size] : void 0,
545
+ panelClassName
546
+ ),
547
+ onClick: (e) => e.stopPropagation(),
548
+ children: [
549
+ modalHeaderMarkup,
550
+ children
551
+ ]
552
+ }
553
+ )
554
+ }
555
+ ),
556
+ document.body
557
+ );
558
+ }
559
+
560
+ // src/components/ui/Button.tsx
561
+ import React4 from "react";
562
+ import { Loader2 } from "lucide-react";
563
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
564
+ var VARIANT_CLASSES = {
565
+ primary: "bg-primary text-primary-contrast hover:bg-primary-hover border border-transparent shadow-xs",
566
+ secondary: "bg-secondary text-secondary-contrast hover:bg-secondary-hover border border-transparent shadow-xs",
567
+ outline: "bg-surface text-text border border-border hover:bg-bg-subtle hover:border-text-muted/40",
568
+ ghost: "bg-transparent text-text-secondary hover:bg-bg-subtle hover:text-text border border-transparent",
569
+ danger: "bg-danger text-white hover:brightness-95 border border-transparent shadow-xs",
570
+ subtle: "bg-primary-light text-primary hover:bg-primary-light/70 border border-transparent"
571
+ };
572
+ var SIZE_CLASSES = {
573
+ xs: "h-7 gap-1 px-2.5 text-xs rounded-lg",
574
+ sm: "h-9 gap-1.5 px-3.5 text-sm rounded-lg",
575
+ md: "h-10 gap-2 px-4 text-sm rounded-xl",
576
+ lg: "h-11 gap-2 px-5 text-[15px] rounded-xl"
577
+ };
578
+ var Button = React4.forwardRef(
579
+ ({
580
+ variant = "primary",
581
+ size = "md",
582
+ loading = false,
583
+ fullWidth = false,
584
+ leftIcon,
585
+ rightIcon,
586
+ className,
587
+ children,
588
+ disabled,
589
+ type = "button",
590
+ ...props
591
+ }, ref) => {
592
+ const isDisabled = disabled || loading;
593
+ const slot = useSlots("button", void 0, className);
594
+ return /* @__PURE__ */ jsxs2(
595
+ "button",
596
+ {
597
+ ref,
598
+ type,
599
+ disabled: isDisabled,
600
+ "data-variant": variant,
601
+ "data-size": size,
602
+ className: slot(
603
+ "root",
604
+ "ws-button",
605
+ `ws-button--${variant}`,
606
+ `ws-button--${size}`,
607
+ "inline-flex items-center justify-center font-medium whitespace-nowrap transition-all duration-150",
608
+ "focus-visible:ring-primary/40 focus-visible:ring-2 focus-visible:outline-none",
609
+ "active:scale-[0.98]",
610
+ "disabled:pointer-events-none disabled:opacity-55",
611
+ VARIANT_CLASSES[variant],
612
+ SIZE_CLASSES[size],
613
+ fullWidth && "w-full"
614
+ ),
615
+ ...props,
616
+ children: [
617
+ loading ? /* @__PURE__ */ jsx3(Loader2, { className: "animate-spin", size: size === "lg" ? 18 : 16 }) : leftIcon,
618
+ children,
619
+ !loading && rightIcon
620
+ ]
621
+ }
622
+ );
623
+ }
624
+ );
625
+ Button.displayName = "Button";
626
+
627
+ // src/components/ui/ConfirmModal.tsx
628
+ import React5 from "react";
629
+ import { AlertTriangle } from "lucide-react";
630
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
631
+ var ConfirmModal = ({
632
+ open,
633
+ onClose,
634
+ onConfirm,
635
+ title,
636
+ description,
637
+ confirmText = "Confirmer",
638
+ cancelText = "Annuler",
639
+ danger = false,
640
+ className,
641
+ classNames
642
+ }) => {
643
+ const [loading, setLoading] = React5.useState(false);
644
+ const handleConfirm = async () => {
645
+ setLoading(true);
646
+ try {
647
+ await onConfirm();
648
+ } finally {
649
+ setLoading(false);
650
+ onClose();
651
+ }
652
+ };
653
+ return /* @__PURE__ */ jsx4(
654
+ Modal,
655
+ {
656
+ open,
657
+ onClose: () => {
658
+ if (!loading) onClose();
659
+ },
660
+ size: "sm",
661
+ closeOnBackdropClick: !loading,
662
+ closeOnEscape: !loading,
663
+ hideCloseButton: true,
664
+ zIndexClass: "z-[120]",
665
+ className: cn("ws-confirm-modal", className),
666
+ classNames,
667
+ children: /* @__PURE__ */ jsxs3("div", { className: "ws-confirm-modal__body flex flex-col gap-5 p-6", children: [
668
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-start gap-3.5", children: [
669
+ /* @__PURE__ */ jsx4(
670
+ "div",
671
+ {
672
+ className: cn(
673
+ "flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-full",
674
+ danger ? "bg-danger-light" : "bg-primary-light"
675
+ ),
676
+ children: /* @__PURE__ */ jsx4(
677
+ AlertTriangle,
678
+ {
679
+ size: 20,
680
+ className: danger ? "text-danger" : "text-primary"
681
+ }
682
+ )
683
+ }
684
+ ),
685
+ /* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1 pt-0.5", children: [
686
+ /* @__PURE__ */ jsx4("h3", { className: "text-text text-base font-semibold", children: title }),
687
+ description && /* @__PURE__ */ jsx4("div", { className: "text-text-secondary mt-1.5 text-sm leading-relaxed", children: description })
688
+ ] })
689
+ ] }),
690
+ /* @__PURE__ */ jsxs3("div", { className: "flex justify-end gap-2.5", children: [
691
+ /* @__PURE__ */ jsx4(
692
+ Button,
693
+ {
694
+ variant: "outline",
695
+ size: "sm",
696
+ onClick: onClose,
697
+ disabled: loading,
698
+ children: cancelText
699
+ }
700
+ ),
701
+ /* @__PURE__ */ jsx4(
702
+ Button,
703
+ {
704
+ variant: danger ? "danger" : "primary",
705
+ size: "sm",
706
+ loading,
707
+ onClick: handleConfirm,
708
+ children: confirmText
709
+ }
710
+ )
711
+ ] })
712
+ ] })
713
+ }
714
+ );
715
+ };
716
+
717
+ export {
718
+ createWorkspaceClient,
719
+ createWorkspaceSocket,
720
+ buildThemeVars,
721
+ WorkspaceThemeContext,
722
+ useWorkspaceThemeAttrs,
723
+ WorkspaceStylesContext,
724
+ useWorkspaceClassNames,
725
+ useSlots,
726
+ cn,
727
+ WorkspaceProvider,
728
+ useWorkspaceContext,
729
+ createWorkspaceApis,
730
+ useWorkspaceApi,
731
+ Modal,
732
+ Button,
733
+ ConfirmModal
734
+ };
735
+ //# sourceMappingURL=chunk-4UDSYAAD.mjs.map