@hachej/boring-core 0.1.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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/dist/CoreFront-CDeLdfb0.d.ts +19 -0
  4. package/dist/app/front/index.d.ts +18 -0
  5. package/dist/app/front/index.js +162 -0
  6. package/dist/app/front/styles.css +6 -0
  7. package/dist/app/server/index.d.ts +96 -0
  8. package/dist/app/server/index.js +507 -0
  9. package/dist/app/vite/index.d.ts +10 -0
  10. package/dist/app/vite/index.js +33 -0
  11. package/dist/authHook-vsRhOvnh.d.ts +38 -0
  12. package/dist/chunk-CZ4HIXII.js +2869 -0
  13. package/dist/chunk-H5KU6R6Y.js +68 -0
  14. package/dist/chunk-HSRBZLKT.js +1684 -0
  15. package/dist/chunk-HYNKZSTF.js +18 -0
  16. package/dist/chunk-MLKGABMK.js +9 -0
  17. package/dist/chunk-VTOS4C7B.js +3443 -0
  18. package/dist/connection-CE7z-wBp.d.ts +145 -0
  19. package/dist/front/index.d.ts +458 -0
  20. package/dist/front/index.js +126 -0
  21. package/dist/front/theme.css +168 -0
  22. package/dist/front/top-bar-slot.d.ts +10 -0
  23. package/dist/front/top-bar-slot.js +9 -0
  24. package/dist/index-COZa03RP.d.ts +266 -0
  25. package/dist/migrate-D49JsATX.d.ts +8 -0
  26. package/dist/server/db/index.d.ts +209 -0
  27. package/dist/server/db/index.js +18 -0
  28. package/dist/server/index.d.ts +395 -0
  29. package/dist/server/index.js +136 -0
  30. package/dist/shared/index.d.ts +1 -0
  31. package/dist/shared/index.js +13 -0
  32. package/drizzle/.gitkeep +0 -0
  33. package/drizzle/0000_easy_meggan.sql +53 -0
  34. package/drizzle/0001_groovy_smiling_tiger.sql +14 -0
  35. package/drizzle/0002_busy_iron_man.sql +16 -0
  36. package/drizzle/0003_aspiring_richard_fisk.sql +12 -0
  37. package/drizzle/0004_heavy_lenny_balinger.sql +9 -0
  38. package/drizzle/0005_flimsy_mastermind.sql +17 -0
  39. package/drizzle/0006_happy_callisto.sql +13 -0
  40. package/drizzle/0007_v7_substrate.sql +54 -0
  41. package/drizzle/0008_workspace_sandbox_handles.sql +32 -0
  42. package/drizzle/0009_workspace_runtime_resources.sql +39 -0
  43. package/drizzle/meta/0000_snapshot.json +380 -0
  44. package/drizzle/meta/0001_snapshot.json +471 -0
  45. package/drizzle/meta/0002_snapshot.json +599 -0
  46. package/drizzle/meta/0003_snapshot.json +693 -0
  47. package/drizzle/meta/0004_snapshot.json +753 -0
  48. package/drizzle/meta/0005_snapshot.json +886 -0
  49. package/drizzle/meta/0006_snapshot.json +968 -0
  50. package/drizzle/meta/_journal.json +76 -0
  51. package/drizzle/schema.ts +110 -0
  52. package/package.json +127 -0
@@ -0,0 +1,3443 @@
1
+ import {
2
+ TopBarSlotProvider
3
+ } from "./chunk-HYNKZSTF.js";
4
+ import {
5
+ ConfigFetchError,
6
+ ERROR_CODES,
7
+ HttpError
8
+ } from "./chunk-H5KU6R6Y.js";
9
+
10
+ // src/front/AppErrorBoundary.tsx
11
+ import { Component } from "react";
12
+ import { Button, ErrorState } from "@hachej/boring-ui-kit";
13
+ import { jsx } from "react/jsx-runtime";
14
+ var AppErrorBoundary = class extends Component {
15
+ state = { error: null };
16
+ static getDerivedStateFromError(error) {
17
+ return { error };
18
+ }
19
+ componentDidCatch(error, errorInfo) {
20
+ console.error("[AppErrorBoundary]", error, errorInfo);
21
+ this.props.onError?.(error, errorInfo);
22
+ }
23
+ handleReload = () => {
24
+ window.location.reload();
25
+ };
26
+ handleRetry = () => {
27
+ this.setState({ error: null });
28
+ };
29
+ render() {
30
+ const { error } = this.state;
31
+ if (!error) return this.props.children;
32
+ const isConfigError = error instanceof ConfigFetchError;
33
+ return /* @__PURE__ */ jsx("div", { className: "flex min-h-screen items-center justify-center bg-background p-8 text-foreground", children: /* @__PURE__ */ jsx(
34
+ ErrorState,
35
+ {
36
+ className: "w-full max-w-lg",
37
+ title: isConfigError ? "Cannot reach server" : "Something went wrong",
38
+ description: error.message,
39
+ details: isConfigError && error.requestId ? `Request ID: ${error.requestId}` : void 0,
40
+ actions: /* @__PURE__ */ jsx(Button, { type: "button", onClick: isConfigError ? this.handleRetry : this.handleReload, children: isConfigError ? "Retry" : "Reload page" })
41
+ }
42
+ ) });
43
+ }
44
+ };
45
+
46
+ // src/front/ConfigProvider.tsx
47
+ import { createContext, useContext, useState, useEffect, useRef } from "react";
48
+
49
+ // src/front/utils.ts
50
+ var apiBase = "";
51
+ function setApiBase(base) {
52
+ apiBase = base.replace(/\/$/, "");
53
+ }
54
+ function getApiBase() {
55
+ return apiBase;
56
+ }
57
+ function buildApiUrl(path) {
58
+ if (path.startsWith("http://") || path.startsWith("https://")) return path;
59
+ const base = getApiBase();
60
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
61
+ return `${base}${normalizedPath}`;
62
+ }
63
+ function getWsBase() {
64
+ const base = getApiBase();
65
+ if (base.startsWith("https://")) return base.replace("https://", "wss://");
66
+ if (base.startsWith("http://")) return base.replace("http://", "ws://");
67
+ const protocol = typeof window !== "undefined" && window.location.protocol === "https:" ? "wss:" : "ws:";
68
+ const host = typeof window !== "undefined" ? window.location.host : "localhost";
69
+ return `${protocol}//${host}${base}`;
70
+ }
71
+ function buildWsUrl(path) {
72
+ const base = getWsBase();
73
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
74
+ return `${base}${normalizedPath}`;
75
+ }
76
+ function openWebSocket(path, protocols) {
77
+ return new WebSocket(buildWsUrl(path), protocols);
78
+ }
79
+ async function parseErrorEnvelope(response) {
80
+ try {
81
+ const body = await response.json();
82
+ const code = body.code ?? "internal_error";
83
+ const message = body.message ?? body.error ?? response.statusText;
84
+ return { code, message, requestId: body.requestId };
85
+ } catch {
86
+ return {
87
+ code: ERROR_CODES.INTERNAL_ERROR,
88
+ message: response.statusText || `HTTP ${response.status}`
89
+ };
90
+ }
91
+ }
92
+ async function apiFetch(url, init) {
93
+ const fullUrl = buildApiUrl(url);
94
+ const response = await fetch(fullUrl, {
95
+ ...init,
96
+ credentials: "include"
97
+ }).catch((err) => {
98
+ throw new HttpError({
99
+ status: 0,
100
+ code: ERROR_CODES.INTERNAL_ERROR,
101
+ message: `Network error: ${err instanceof Error ? err.message : String(err)}`
102
+ });
103
+ });
104
+ if (!response.ok) {
105
+ const envelope = await parseErrorEnvelope(response);
106
+ throw new HttpError({
107
+ status: response.status,
108
+ code: envelope.code,
109
+ message: envelope.message,
110
+ requestId: envelope.requestId
111
+ });
112
+ }
113
+ return response;
114
+ }
115
+ async function apiFetchJson(url, init) {
116
+ const response = await apiFetch(url, init);
117
+ return response.json();
118
+ }
119
+ function getHttpErrorDetail(err) {
120
+ if (err instanceof HttpError) {
121
+ return { code: err.code, message: err.message, status: err.status };
122
+ }
123
+ if (err instanceof Error) {
124
+ return { code: "internal_error", message: err.message };
125
+ }
126
+ return { code: "internal_error", message: String(err) };
127
+ }
128
+ var routes = {
129
+ signin: "/auth/signin",
130
+ signup: "/auth/signup",
131
+ forgotPassword: "/auth/forgot-password",
132
+ resetPassword: "/auth/reset-password",
133
+ verifyEmail: "/auth/verify-email",
134
+ callbackGithub: "/auth/callback/github",
135
+ me: "/me",
136
+ workspaceMembers: "/w/:id/members",
137
+ workspaceInvites: "/w/:id/invites",
138
+ workspaceSettings: "/w/:id/settings",
139
+ inviteAccept: "/invites/:token"
140
+ };
141
+ function routeHref(name, params) {
142
+ let path = routes[name];
143
+ if (params) {
144
+ for (const [key, value] of Object.entries(params)) {
145
+ path = path.replace(`:${key}`, encodeURIComponent(value));
146
+ }
147
+ }
148
+ return path;
149
+ }
150
+
151
+ // src/front/ConfigProvider.tsx
152
+ import { jsx as jsx2 } from "react/jsx-runtime";
153
+ var ConfigContext = createContext(null);
154
+ var DEFAULT_BACKOFF_MS = [500, 1e3, 2e3];
155
+ function sleep(ms) {
156
+ return new Promise((resolve) => setTimeout(resolve, ms));
157
+ }
158
+ async function fetchConfigWithRetry(backoffMs) {
159
+ const maxRetries = backoffMs.length;
160
+ let lastError;
161
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
162
+ try {
163
+ return await apiFetchJson("/api/v1/config");
164
+ } catch (err) {
165
+ lastError = err;
166
+ if (attempt < maxRetries) {
167
+ await sleep(backoffMs[attempt]);
168
+ }
169
+ }
170
+ }
171
+ const message = lastError instanceof Error ? lastError.message : "Failed to load config";
172
+ const requestId = lastError && typeof lastError === "object" && "requestId" in lastError ? lastError.requestId : void 0;
173
+ throw new ConfigFetchError(message, requestId);
174
+ }
175
+ function ConfigProvider({
176
+ children,
177
+ retryBackoff = DEFAULT_BACKOFF_MS
178
+ }) {
179
+ const [config, setConfig] = useState(null);
180
+ const [error, setError] = useState(null);
181
+ const fetchedRef = useRef(false);
182
+ useEffect(() => {
183
+ if (fetchedRef.current) return;
184
+ fetchedRef.current = true;
185
+ fetchConfigWithRetry(retryBackoff).then(setConfig).catch((err) => {
186
+ setError(
187
+ err instanceof ConfigFetchError ? err : new ConfigFetchError(
188
+ err instanceof Error ? err.message : "Unknown error"
189
+ )
190
+ );
191
+ });
192
+ }, []);
193
+ if (error) throw error;
194
+ if (!config) return null;
195
+ return /* @__PURE__ */ jsx2(ConfigContext.Provider, { value: config, children });
196
+ }
197
+ function useConfig() {
198
+ const ctx = useContext(ConfigContext);
199
+ if (!ctx)
200
+ throw new Error("useConfig must be used within a ConfigProvider");
201
+ return ctx;
202
+ }
203
+ function useConfigLoaded() {
204
+ return useContext(ConfigContext) !== null;
205
+ }
206
+
207
+ // src/front/ThemeProvider.tsx
208
+ import {
209
+ createContext as createContext2,
210
+ useContext as useContext2,
211
+ useCallback,
212
+ useEffect as useEffect2,
213
+ useMemo,
214
+ useState as useState2
215
+ } from "react";
216
+ import { jsx as jsx3 } from "react/jsx-runtime";
217
+ var ThemeContext = createContext2(null);
218
+ var STORAGE_KEY = "boring-core:theme";
219
+ function getSystemTheme() {
220
+ if (typeof window === "undefined") return "light";
221
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
222
+ }
223
+ function resolveTheme(pref) {
224
+ return pref === "system" ? getSystemTheme() : pref;
225
+ }
226
+ function applyTheme(theme) {
227
+ document.documentElement.setAttribute("data-theme", theme);
228
+ document.documentElement.classList.toggle("dark", theme === "dark");
229
+ }
230
+ function ThemeProvider({ children, defaultTheme = "system" }) {
231
+ const [preference, setPreference] = useState2(() => {
232
+ if (typeof window === "undefined") return defaultTheme;
233
+ try {
234
+ const stored = localStorage.getItem(STORAGE_KEY);
235
+ if (stored === "light" || stored === "dark" || stored === "system") return stored;
236
+ } catch {
237
+ }
238
+ return defaultTheme;
239
+ });
240
+ const [systemTheme, setSystemTheme] = useState2(getSystemTheme);
241
+ const resolved = preference === "system" ? systemTheme : preference;
242
+ useEffect2(() => {
243
+ applyTheme(resolved);
244
+ return () => {
245
+ document.documentElement.removeAttribute("data-theme");
246
+ document.documentElement.classList.remove("dark");
247
+ };
248
+ }, [resolved]);
249
+ useEffect2(() => {
250
+ const mql = window.matchMedia("(prefers-color-scheme: dark)");
251
+ const handler = () => setSystemTheme(getSystemTheme());
252
+ mql.addEventListener("change", handler);
253
+ return () => mql.removeEventListener("change", handler);
254
+ }, []);
255
+ useEffect2(() => {
256
+ const handler = (e) => {
257
+ if (e.key !== STORAGE_KEY) return;
258
+ const val = e.newValue;
259
+ if (val === "light" || val === "dark" || val === "system") {
260
+ setPreference(val);
261
+ }
262
+ };
263
+ window.addEventListener("storage", handler);
264
+ return () => window.removeEventListener("storage", handler);
265
+ }, []);
266
+ const setTheme = useCallback((theme) => {
267
+ setPreference(theme);
268
+ try {
269
+ localStorage.setItem(STORAGE_KEY, theme);
270
+ } catch {
271
+ }
272
+ }, []);
273
+ const toggleTheme = useCallback(() => {
274
+ setPreference((prev) => {
275
+ const next = resolveTheme(prev) === "light" ? "dark" : "light";
276
+ try {
277
+ localStorage.setItem(STORAGE_KEY, next);
278
+ } catch {
279
+ }
280
+ return next;
281
+ });
282
+ }, []);
283
+ const value = useMemo(
284
+ () => ({ theme: resolved, preference, setTheme, toggleTheme }),
285
+ [resolved, preference, setTheme, toggleTheme]
286
+ );
287
+ return /* @__PURE__ */ jsx3(ThemeContext.Provider, { value, children });
288
+ }
289
+ function useTheme() {
290
+ const ctx = useContext2(ThemeContext);
291
+ if (!ctx) throw new Error("useTheme must be used within a ThemeProvider");
292
+ return ctx;
293
+ }
294
+
295
+ // src/front/hooks/useKeyboardShortcuts.ts
296
+ import { useEffect as useEffect3, useRef as useRef2 } from "react";
297
+ var MAC_PLATFORM_RE = /Mac|iPod|iPhone|iPad/;
298
+ function normalizeKey(key) {
299
+ const lowered = key.toLowerCase();
300
+ if (lowered === "space") return " ";
301
+ if (lowered === "esc") return "escape";
302
+ return lowered;
303
+ }
304
+ function parseShortcut(shortcut) {
305
+ const parts = shortcut.split("+").map((part) => part.trim().toLowerCase()).filter(Boolean);
306
+ if (parts.length === 0) {
307
+ return {
308
+ key: "",
309
+ cmd: false,
310
+ ctrl: false,
311
+ meta: false,
312
+ shift: false,
313
+ alt: false
314
+ };
315
+ }
316
+ const key = normalizeKey(parts[parts.length - 1] ?? "");
317
+ const modifiers = parts.slice(0, -1);
318
+ return {
319
+ key,
320
+ cmd: modifiers.includes("cmd"),
321
+ ctrl: modifiers.includes("ctrl"),
322
+ meta: modifiers.includes("meta"),
323
+ shift: modifiers.includes("shift"),
324
+ alt: modifiers.includes("alt") || modifiers.includes("option")
325
+ };
326
+ }
327
+ function isMacPlatform() {
328
+ if (typeof navigator === "undefined") return false;
329
+ return MAC_PLATFORM_RE.test(navigator.platform);
330
+ }
331
+ function matchesBinding(event, parsed, macPlatform) {
332
+ const expectedMeta = parsed.meta || parsed.cmd && macPlatform;
333
+ const expectedCtrl = parsed.ctrl || parsed.cmd && !macPlatform;
334
+ const expectedShift = parsed.shift;
335
+ const expectedAlt = parsed.alt;
336
+ if (event.metaKey !== expectedMeta) return false;
337
+ if (event.ctrlKey !== expectedCtrl) return false;
338
+ if (event.shiftKey !== expectedShift) return false;
339
+ if (event.altKey !== expectedAlt) return false;
340
+ return normalizeKey(event.key) === parsed.key;
341
+ }
342
+ function useKeyboardShortcuts(bindings) {
343
+ const bindingsRef = useRef2(bindings);
344
+ bindingsRef.current = bindings;
345
+ useEffect3(() => {
346
+ if (typeof window === "undefined") return;
347
+ const macPlatform = isMacPlatform();
348
+ const parsedBindings = bindingsRef.current.map((binding) => ({
349
+ binding,
350
+ parsed: parseShortcut(binding.shortcut)
351
+ }));
352
+ const onKeyDown = (event) => {
353
+ for (const { binding, parsed } of parsedBindings) {
354
+ if (!parsed.key) continue;
355
+ if (!matchesBinding(event, parsed, macPlatform)) continue;
356
+ event.preventDefault();
357
+ binding.handler(event);
358
+ return;
359
+ }
360
+ };
361
+ window.addEventListener("keydown", onKeyDown);
362
+ return () => {
363
+ window.removeEventListener("keydown", onKeyDown);
364
+ };
365
+ }, [bindings]);
366
+ }
367
+
368
+ // src/front/hooks/useViewportBreakpoint.ts
369
+ import { useEffect as useEffect4, useState as useState3 } from "react";
370
+ function getBreakpoint() {
371
+ if (typeof window === "undefined") return "sm";
372
+ if (typeof window.matchMedia !== "function") return "sm";
373
+ if (window.matchMedia("(min-width: 1536px)").matches) return "2xl";
374
+ if (window.matchMedia("(min-width: 1280px)").matches) return "xl";
375
+ if (window.matchMedia("(min-width: 1024px)").matches) return "lg";
376
+ if (window.matchMedia("(min-width: 768px)").matches) return "md";
377
+ return "sm";
378
+ }
379
+ function useViewportBreakpoint() {
380
+ const [breakpoint, setBreakpoint] = useState3(getBreakpoint);
381
+ useEffect4(() => {
382
+ if (typeof window === "undefined") return;
383
+ const update = () => {
384
+ setBreakpoint(getBreakpoint());
385
+ };
386
+ update();
387
+ window.addEventListener("resize", update);
388
+ return () => {
389
+ window.removeEventListener("resize", update);
390
+ };
391
+ }, []);
392
+ return breakpoint;
393
+ }
394
+
395
+ // src/front/hooks/useReducedMotion.ts
396
+ import { useEffect as useEffect5, useState as useState4 } from "react";
397
+ var QUERY = "(prefers-reduced-motion: reduce)";
398
+ function prefersReducedMotion() {
399
+ if (typeof window === "undefined") return false;
400
+ if (typeof window.matchMedia !== "function") return false;
401
+ return window.matchMedia(QUERY).matches;
402
+ }
403
+ function useReducedMotion() {
404
+ const [reducedMotion, setReducedMotion] = useState4(prefersReducedMotion);
405
+ useEffect5(() => {
406
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
407
+ const media = window.matchMedia(QUERY);
408
+ const update = (event) => {
409
+ setReducedMotion(event?.matches ?? media.matches);
410
+ };
411
+ update();
412
+ if (typeof media.addEventListener === "function") {
413
+ media.addEventListener("change", update);
414
+ return () => media.removeEventListener("change", update);
415
+ }
416
+ if (typeof media.addListener === "function") {
417
+ media.addListener(update);
418
+ return () => media.removeListener(update);
419
+ }
420
+ }, []);
421
+ return reducedMotion;
422
+ }
423
+
424
+ // src/front/hooks/useBlobUrl.ts
425
+ import { useEffect as useEffect6, useState as useState5 } from "react";
426
+ function useBlobUrl(blob) {
427
+ const [blobUrl, setBlobUrl] = useState5(null);
428
+ useEffect6(() => {
429
+ if (!blob) {
430
+ setBlobUrl(null);
431
+ return;
432
+ }
433
+ const nextUrl = URL.createObjectURL(blob);
434
+ setBlobUrl(nextUrl);
435
+ return () => {
436
+ URL.revokeObjectURL(nextUrl);
437
+ };
438
+ }, [blob]);
439
+ return blobUrl;
440
+ }
441
+
442
+ // src/front/hooks/useCapabilities.ts
443
+ import { useSuspenseQuery } from "@tanstack/react-query";
444
+ var CAPABILITIES_QUERY_KEY = ["capabilities"];
445
+ function useCapabilities() {
446
+ const query = useSuspenseQuery({
447
+ queryKey: CAPABILITIES_QUERY_KEY,
448
+ queryFn: async () => apiFetchJson("/api/v1/capabilities")
449
+ });
450
+ return query.data;
451
+ }
452
+
453
+ // src/front/hooks/useWorkspaceMembers.ts
454
+ import { useQuery } from "@tanstack/react-query";
455
+ function useWorkspaceMembers(workspaceId) {
456
+ return useQuery({
457
+ queryKey: ["members", workspaceId],
458
+ queryFn: async () => {
459
+ const data = await apiFetchJson(
460
+ `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/members`
461
+ );
462
+ return data.members;
463
+ },
464
+ enabled: workspaceId.length > 0
465
+ });
466
+ }
467
+
468
+ // src/front/WorkspaceAuthProvider.tsx
469
+ import { createContext as createContext3, useContext as useContext3 } from "react";
470
+ import { useQuery as useQuery2, useQueryClient } from "@tanstack/react-query";
471
+ import { matchPath, useLocation, useParams } from "react-router-dom";
472
+ import { jsx as jsx4 } from "react/jsx-runtime";
473
+ var WorkspaceContext = createContext3({
474
+ workspace: null,
475
+ role: null
476
+ });
477
+ var WORKSPACES_QUERY_KEY = ["workspaces"];
478
+ function workspaceQueryKey(workspaceId) {
479
+ return ["workspace", workspaceId ?? null];
480
+ }
481
+ async function fetchWorkspaces() {
482
+ const data = await apiFetchJson("/api/v1/workspaces");
483
+ return data.workspaces;
484
+ }
485
+ async function fetchWorkspace(workspaceId) {
486
+ return await apiFetchJson(
487
+ `/api/v1/workspaces/${encodeURIComponent(workspaceId)}`
488
+ );
489
+ }
490
+ function workspaceIdFromPath(pathname) {
491
+ const match = matchPath("/w/:id/*", pathname) ?? matchPath("/w/:id", pathname) ?? matchPath("/workspace/:id/*", pathname) ?? matchPath("/workspace/:id", pathname);
492
+ const id = match?.params.id?.trim();
493
+ return id ? id : null;
494
+ }
495
+ function WorkspaceAuthProvider({ children }) {
496
+ const { id } = useParams();
497
+ const location = useLocation();
498
+ const queryClient = useQueryClient();
499
+ const routeWorkspaceId = id?.trim() ? id : workspaceIdFromPath(location.pathname);
500
+ const workspacesQuery = useQuery2({
501
+ queryKey: WORKSPACES_QUERY_KEY,
502
+ queryFn: fetchWorkspaces
503
+ });
504
+ const defaultWorkspace = routeWorkspaceId === null ? workspacesQuery.data?.find((workspace2) => workspace2.isDefault) ?? workspacesQuery.data?.[0] ?? null : null;
505
+ const resolvedId = routeWorkspaceId ?? defaultWorkspace?.id ?? null;
506
+ const cachedDetail = resolvedId ? queryClient.getQueryData(workspaceQueryKey(resolvedId)) : void 0;
507
+ const detailQuery = useQuery2({
508
+ queryKey: workspaceQueryKey(resolvedId),
509
+ queryFn: () => {
510
+ if (!resolvedId) {
511
+ throw new Error("Workspace id is required");
512
+ }
513
+ return fetchWorkspace(resolvedId);
514
+ },
515
+ enabled: resolvedId !== null
516
+ });
517
+ const detail = detailQuery.data ?? cachedDetail ?? null;
518
+ const workspace = detailQuery.isError ? null : detail?.workspace ?? null;
519
+ const role = detailQuery.isError ? null : detail?.role ?? null;
520
+ return /* @__PURE__ */ jsx4(WorkspaceContext.Provider, { value: { workspace, role }, children });
521
+ }
522
+ function useCurrentWorkspace() {
523
+ return useContext3(WorkspaceContext).workspace;
524
+ }
525
+ function useWorkspaceRole() {
526
+ return useContext3(WorkspaceContext).role;
527
+ }
528
+
529
+ // src/front/auth/AuthProvider.tsx
530
+ import { createContext as createContext4, useContext as useContext4, useCallback as useCallback2, useMemo as useMemo2 } from "react";
531
+
532
+ // src/front/auth/authClient.ts
533
+ import { createAuthClient } from "better-auth/react";
534
+ import { magicLinkClient } from "better-auth/client/plugins";
535
+ function createBetterAuthClient(baseURL) {
536
+ return createAuthClient({
537
+ baseURL,
538
+ basePath: "/auth",
539
+ plugins: [magicLinkClient()]
540
+ });
541
+ }
542
+ var singleton = null;
543
+ var singletonBase = "";
544
+ function getAuthClient(baseURL) {
545
+ const base = baseURL ?? getApiBase();
546
+ if (singleton && singletonBase === base) return singleton;
547
+ singleton = createBetterAuthClient(base);
548
+ singletonBase = base;
549
+ return singleton;
550
+ }
551
+
552
+ // src/front/auth/AuthProvider.tsx
553
+ import { jsx as jsx5 } from "react/jsx-runtime";
554
+ var AuthContext = createContext4(null);
555
+ function toISOString(value) {
556
+ if (!value) return "";
557
+ if (value instanceof Date) return value.toISOString();
558
+ return value;
559
+ }
560
+ function normalizeUser(raw) {
561
+ return {
562
+ id: raw.id,
563
+ email: raw.email,
564
+ name: raw.name ?? null,
565
+ emailVerified: Boolean(raw.emailVerified),
566
+ image: raw.image ?? null,
567
+ createdAt: toISOString(raw.createdAt),
568
+ updatedAt: toISOString(raw.updatedAt)
569
+ };
570
+ }
571
+ function AuthProvider({
572
+ children,
573
+ baseURL,
574
+ queryClient,
575
+ navigate
576
+ }) {
577
+ const client = useMemo2(() => getAuthClient(baseURL), [baseURL]);
578
+ const signOut = useCallback2(async () => {
579
+ await client.signOut();
580
+ queryClient?.clear();
581
+ navigate?.(routes.signin);
582
+ }, [client, queryClient, navigate]);
583
+ const value = useMemo2(
584
+ () => ({ client, signOut }),
585
+ [client, signOut]
586
+ );
587
+ return /* @__PURE__ */ jsx5(AuthContext.Provider, { value, children });
588
+ }
589
+ function useAuthContext() {
590
+ const ctx = useContext4(AuthContext);
591
+ if (!ctx) throw new Error("useSession/signIn/signOut must be used within an AuthProvider");
592
+ return ctx;
593
+ }
594
+ function useSession() {
595
+ const { client } = useAuthContext();
596
+ const session = client.useSession();
597
+ const raw = session.data;
598
+ const rawUser = raw?.user;
599
+ const rawSession = raw?.session;
600
+ return {
601
+ data: rawUser ? {
602
+ user: normalizeUser(rawUser),
603
+ expiresAt: toISOString(rawSession?.expiresAt)
604
+ } : null,
605
+ isPending: session.isPending,
606
+ error: session.error ? { status: session.error.status ?? 0, code: "unauthorized", message: session.error.message ?? "Session error" } : null
607
+ };
608
+ }
609
+ function useSignIn() {
610
+ const { client } = useAuthContext();
611
+ return client.signIn;
612
+ }
613
+ function useSignUp() {
614
+ const { client } = useAuthContext();
615
+ return client.signUp;
616
+ }
617
+ function useForgetPassword() {
618
+ const { client } = useAuthContext();
619
+ return client.forgetPassword;
620
+ }
621
+ function useResetPassword() {
622
+ const { client } = useAuthContext();
623
+ return client.resetPassword;
624
+ }
625
+ function useVerifyEmail() {
626
+ const { client } = useAuthContext();
627
+ return client.verifyEmail;
628
+ }
629
+ function useSendVerificationEmail() {
630
+ const { client } = useAuthContext();
631
+ return client.sendVerificationEmail;
632
+ }
633
+ function useChangePassword() {
634
+ const { client } = useAuthContext();
635
+ return client.changePassword;
636
+ }
637
+ function useSignOut() {
638
+ const { signOut } = useAuthContext();
639
+ return signOut;
640
+ }
641
+
642
+ // src/front/auth/UserIdentityProvider.tsx
643
+ import { createContext as createContext5, useContext as useContext5, useEffect as useEffect7, useState as useState6, useRef as useRef3 } from "react";
644
+ import { jsx as jsx6 } from "react/jsx-runtime";
645
+ var UserContext = createContext5(null);
646
+ var STALE_MS = 6e4;
647
+ function UserIdentityProvider({ children }) {
648
+ const { data: session } = useSession();
649
+ const [identity, setIdentity] = useState6(null);
650
+ const fetchedForRef = useRef3(null);
651
+ const lastFetchRef = useRef3(0);
652
+ useEffect7(() => {
653
+ if (!session?.user) {
654
+ setIdentity(null);
655
+ fetchedForRef.current = null;
656
+ return;
657
+ }
658
+ const userId = session.user.id;
659
+ const now = Date.now();
660
+ if (fetchedForRef.current === userId && now - lastFetchRef.current < STALE_MS) {
661
+ return;
662
+ }
663
+ let cancelled = false;
664
+ apiFetchJson("/api/v1/me").then((data) => {
665
+ if (cancelled) return;
666
+ setIdentity({ user: data.user, settings: data.settings });
667
+ fetchedForRef.current = userId;
668
+ lastFetchRef.current = Date.now();
669
+ }).catch(() => {
670
+ if (cancelled) return;
671
+ setIdentity(null);
672
+ });
673
+ return () => {
674
+ cancelled = true;
675
+ };
676
+ }, [session?.user?.id]);
677
+ return /* @__PURE__ */ jsx6(UserContext.Provider, { value: identity, children });
678
+ }
679
+ function useUser() {
680
+ return useContext5(UserContext);
681
+ }
682
+
683
+ // src/front/auth/SignInPage.tsx
684
+ import { useState as useState7 } from "react";
685
+ import { useForm } from "react-hook-form";
686
+ import { zodResolver } from "@hookform/resolvers/zod";
687
+ import { z } from "zod";
688
+ import { Button as Button2, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Input, Label } from "@hachej/boring-ui-kit";
689
+ import { jsx as jsx7, jsxs } from "react/jsx-runtime";
690
+ var signInSchema = z.object({
691
+ email: z.string().email("Please enter a valid email"),
692
+ password: z.string().min(1, "Password is required")
693
+ });
694
+ function SignInPage() {
695
+ const signIn = useSignIn();
696
+ const [serverError, setServerError] = useState7(null);
697
+ const [isSubmitting, setIsSubmitting] = useState7(false);
698
+ const {
699
+ register,
700
+ handleSubmit,
701
+ formState: { errors }
702
+ } = useForm({
703
+ resolver: zodResolver(signInSchema)
704
+ });
705
+ async function onSubmit(data) {
706
+ setServerError(null);
707
+ setIsSubmitting(true);
708
+ try {
709
+ const result = await signIn.email({
710
+ email: data.email,
711
+ password: data.password
712
+ });
713
+ if (result.error) {
714
+ setServerError(result.error.message ?? "Sign in failed");
715
+ }
716
+ } catch (err) {
717
+ setServerError(err instanceof Error ? err.message : "Sign in failed");
718
+ } finally {
719
+ setIsSubmitting(false);
720
+ }
721
+ }
722
+ return /* @__PURE__ */ jsx7("div", { className: "flex min-h-screen items-center justify-center p-4", children: /* @__PURE__ */ jsxs(Card, { className: "w-full max-w-sm", children: [
723
+ /* @__PURE__ */ jsxs(CardHeader, { children: [
724
+ /* @__PURE__ */ jsx7(CardTitle, { children: "Sign in" }),
725
+ /* @__PURE__ */ jsx7(CardDescription, { children: "Enter your credentials to continue" })
726
+ ] }),
727
+ /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit(onSubmit), noValidate: true, children: [
728
+ /* @__PURE__ */ jsxs(CardContent, { className: "space-y-4", children: [
729
+ serverError && /* @__PURE__ */ jsx7("div", { role: "alert", className: "text-sm text-destructive", children: serverError }),
730
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
731
+ /* @__PURE__ */ jsx7(Label, { htmlFor: "email", children: "Email" }),
732
+ /* @__PURE__ */ jsx7(
733
+ Input,
734
+ {
735
+ id: "email",
736
+ type: "email",
737
+ autoComplete: "email",
738
+ placeholder: "you@example.com",
739
+ ...register("email")
740
+ }
741
+ ),
742
+ errors.email && /* @__PURE__ */ jsx7("p", { className: "text-sm text-destructive", children: errors.email.message })
743
+ ] }),
744
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
745
+ /* @__PURE__ */ jsx7(Label, { htmlFor: "password", children: "Password" }),
746
+ /* @__PURE__ */ jsx7(
747
+ Input,
748
+ {
749
+ id: "password",
750
+ type: "password",
751
+ autoComplete: "current-password",
752
+ ...register("password")
753
+ }
754
+ ),
755
+ errors.password && /* @__PURE__ */ jsx7("p", { className: "text-sm text-destructive", children: errors.password.message })
756
+ ] })
757
+ ] }),
758
+ /* @__PURE__ */ jsxs(CardFooter, { className: "flex flex-col gap-4", children: [
759
+ /* @__PURE__ */ jsx7(Button2, { type: "submit", className: "w-full", disabled: isSubmitting, children: isSubmitting ? "Signing in\u2026" : "Sign in" }),
760
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-between text-sm w-full", children: [
761
+ /* @__PURE__ */ jsx7("a", { href: routes.forgotPassword, className: "text-muted-foreground hover:underline", children: "Forgot password?" }),
762
+ /* @__PURE__ */ jsx7("a", { href: routes.signup, className: "text-muted-foreground hover:underline", children: "Sign up" })
763
+ ] })
764
+ ] })
765
+ ] })
766
+ ] }) });
767
+ }
768
+
769
+ // src/front/auth/SignUpPage.tsx
770
+ import { useState as useState8 } from "react";
771
+ import { useForm as useForm2 } from "react-hook-form";
772
+ import { zodResolver as zodResolver2 } from "@hookform/resolvers/zod";
773
+ import { z as z2 } from "zod";
774
+ import { Button as Button3, Card as Card2, CardContent as CardContent2, CardDescription as CardDescription2, CardFooter as CardFooter2, CardHeader as CardHeader2, CardTitle as CardTitle2, Input as Input2, Label as Label2 } from "@hachej/boring-ui-kit";
775
+ import { jsx as jsx8, jsxs as jsxs2 } from "react/jsx-runtime";
776
+ var signUpSchema = z2.object({
777
+ name: z2.string().min(1, "Name is required"),
778
+ email: z2.string().email("Please enter a valid email"),
779
+ password: z2.string().min(8, "Password must be at least 8 characters")
780
+ });
781
+ function SignUpPage() {
782
+ const signUp = useSignUp();
783
+ const [serverError, setServerError] = useState8(null);
784
+ const [isSubmitting, setIsSubmitting] = useState8(false);
785
+ const [success, setSuccess] = useState8(false);
786
+ const inviteToken = typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("invite_token") : null;
787
+ const {
788
+ register,
789
+ handleSubmit,
790
+ formState: { errors }
791
+ } = useForm2({
792
+ resolver: zodResolver2(signUpSchema)
793
+ });
794
+ async function onSubmit(data) {
795
+ setServerError(null);
796
+ setIsSubmitting(true);
797
+ try {
798
+ const fetchOptions = inviteToken ? { headers: { "x-invite-token": inviteToken } } : void 0;
799
+ const result = await signUp.email(
800
+ { email: data.email, password: data.password, name: data.name },
801
+ fetchOptions
802
+ );
803
+ if (result.error) {
804
+ setServerError(result.error.message ?? "Sign up failed");
805
+ } else {
806
+ setSuccess(true);
807
+ }
808
+ } catch (err) {
809
+ setServerError(err instanceof Error ? err.message : "Sign up failed");
810
+ } finally {
811
+ setIsSubmitting(false);
812
+ }
813
+ }
814
+ if (success) {
815
+ return /* @__PURE__ */ jsx8("div", { className: "flex min-h-screen items-center justify-center p-4", children: /* @__PURE__ */ jsxs2(Card2, { className: "w-full max-w-sm", children: [
816
+ /* @__PURE__ */ jsxs2(CardHeader2, { children: [
817
+ /* @__PURE__ */ jsx8(CardTitle2, { children: "Check your email" }),
818
+ /* @__PURE__ */ jsx8(CardDescription2, { children: "We sent a verification link to your email address. Please check your inbox to continue." })
819
+ ] }),
820
+ /* @__PURE__ */ jsx8(CardFooter2, { children: /* @__PURE__ */ jsx8("a", { href: routes.signin, className: "text-sm text-muted-foreground hover:underline", children: "Back to sign in" }) })
821
+ ] }) });
822
+ }
823
+ return /* @__PURE__ */ jsx8("div", { className: "flex min-h-screen items-center justify-center p-4", children: /* @__PURE__ */ jsxs2(Card2, { className: "w-full max-w-sm", children: [
824
+ /* @__PURE__ */ jsxs2(CardHeader2, { children: [
825
+ /* @__PURE__ */ jsx8(CardTitle2, { children: "Create an account" }),
826
+ /* @__PURE__ */ jsx8(CardDescription2, { children: "Enter your details to get started" })
827
+ ] }),
828
+ /* @__PURE__ */ jsxs2("form", { onSubmit: handleSubmit(onSubmit), noValidate: true, children: [
829
+ /* @__PURE__ */ jsxs2(CardContent2, { className: "space-y-4", children: [
830
+ serverError && /* @__PURE__ */ jsx8("div", { role: "alert", className: "text-sm text-destructive", children: serverError }),
831
+ /* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
832
+ /* @__PURE__ */ jsx8(Label2, { htmlFor: "name", children: "Name" }),
833
+ /* @__PURE__ */ jsx8(
834
+ Input2,
835
+ {
836
+ id: "name",
837
+ type: "text",
838
+ autoComplete: "name",
839
+ placeholder: "Your name",
840
+ ...register("name")
841
+ }
842
+ ),
843
+ errors.name && /* @__PURE__ */ jsx8("p", { className: "text-sm text-destructive", children: errors.name.message })
844
+ ] }),
845
+ /* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
846
+ /* @__PURE__ */ jsx8(Label2, { htmlFor: "email", children: "Email" }),
847
+ /* @__PURE__ */ jsx8(
848
+ Input2,
849
+ {
850
+ id: "email",
851
+ type: "email",
852
+ autoComplete: "email",
853
+ placeholder: "you@example.com",
854
+ ...register("email")
855
+ }
856
+ ),
857
+ errors.email && /* @__PURE__ */ jsx8("p", { className: "text-sm text-destructive", children: errors.email.message })
858
+ ] }),
859
+ /* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
860
+ /* @__PURE__ */ jsx8(Label2, { htmlFor: "password", children: "Password" }),
861
+ /* @__PURE__ */ jsx8(
862
+ Input2,
863
+ {
864
+ id: "password",
865
+ type: "password",
866
+ autoComplete: "new-password",
867
+ placeholder: "At least 8 characters",
868
+ ...register("password")
869
+ }
870
+ ),
871
+ errors.password && /* @__PURE__ */ jsx8("p", { className: "text-sm text-destructive", children: errors.password.message })
872
+ ] })
873
+ ] }),
874
+ /* @__PURE__ */ jsxs2(CardFooter2, { className: "flex flex-col gap-4", children: [
875
+ /* @__PURE__ */ jsx8(Button3, { type: "submit", className: "w-full", disabled: isSubmitting, children: isSubmitting ? "Creating account\u2026" : "Sign up" }),
876
+ /* @__PURE__ */ jsxs2("div", { className: "text-sm text-center", children: [
877
+ /* @__PURE__ */ jsx8("span", { className: "text-muted-foreground", children: "Already have an account? " }),
878
+ /* @__PURE__ */ jsx8("a", { href: routes.signin, className: "hover:underline", children: "Sign in" })
879
+ ] })
880
+ ] })
881
+ ] })
882
+ ] }) });
883
+ }
884
+
885
+ // src/front/auth/ForgotPasswordPage.tsx
886
+ import { useState as useState9 } from "react";
887
+ import { useForm as useForm3 } from "react-hook-form";
888
+ import { zodResolver as zodResolver3 } from "@hookform/resolvers/zod";
889
+ import { z as z3 } from "zod";
890
+ import { Button as Button4, Card as Card3, CardContent as CardContent3, CardDescription as CardDescription3, CardFooter as CardFooter3, CardHeader as CardHeader3, CardTitle as CardTitle3, Input as Input3, Label as Label3 } from "@hachej/boring-ui-kit";
891
+ import { jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime";
892
+ var forgotSchema = z3.object({
893
+ email: z3.string().email("Please enter a valid email")
894
+ });
895
+ function ForgotPasswordPage() {
896
+ const forgetPassword = useForgetPassword();
897
+ const [isSubmitting, setIsSubmitting] = useState9(false);
898
+ const [submitted, setSubmitted] = useState9(false);
899
+ const {
900
+ register,
901
+ handleSubmit,
902
+ formState: { errors }
903
+ } = useForm3({
904
+ resolver: zodResolver3(forgotSchema)
905
+ });
906
+ async function onSubmit(data) {
907
+ setIsSubmitting(true);
908
+ try {
909
+ await forgetPassword({ email: data.email, redirectTo: routes.resetPassword });
910
+ } catch {
911
+ } finally {
912
+ setIsSubmitting(false);
913
+ setSubmitted(true);
914
+ }
915
+ }
916
+ if (submitted) {
917
+ return /* @__PURE__ */ jsx9("div", { className: "flex min-h-screen items-center justify-center p-4", children: /* @__PURE__ */ jsxs3(Card3, { className: "w-full max-w-sm", children: [
918
+ /* @__PURE__ */ jsxs3(CardHeader3, { children: [
919
+ /* @__PURE__ */ jsx9(CardTitle3, { children: "Check your inbox" }),
920
+ /* @__PURE__ */ jsx9(CardDescription3, { children: "If an account exists with that email, we sent a password reset link. Check your inbox and follow the instructions." })
921
+ ] }),
922
+ /* @__PURE__ */ jsx9(CardFooter3, { children: /* @__PURE__ */ jsx9("a", { href: routes.signin, className: "text-sm text-muted-foreground hover:underline", children: "Back to sign in" }) })
923
+ ] }) });
924
+ }
925
+ return /* @__PURE__ */ jsx9("div", { className: "flex min-h-screen items-center justify-center p-4", children: /* @__PURE__ */ jsxs3(Card3, { className: "w-full max-w-sm", children: [
926
+ /* @__PURE__ */ jsxs3(CardHeader3, { children: [
927
+ /* @__PURE__ */ jsx9(CardTitle3, { children: "Forgot password" }),
928
+ /* @__PURE__ */ jsx9(CardDescription3, { children: "Enter your email and we'll send you a reset link" })
929
+ ] }),
930
+ /* @__PURE__ */ jsxs3("form", { onSubmit: handleSubmit(onSubmit), noValidate: true, children: [
931
+ /* @__PURE__ */ jsx9(CardContent3, { className: "space-y-4", children: /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
932
+ /* @__PURE__ */ jsx9(Label3, { htmlFor: "email", children: "Email" }),
933
+ /* @__PURE__ */ jsx9(
934
+ Input3,
935
+ {
936
+ id: "email",
937
+ type: "email",
938
+ autoComplete: "email",
939
+ placeholder: "you@example.com",
940
+ ...register("email")
941
+ }
942
+ ),
943
+ errors.email && /* @__PURE__ */ jsx9("p", { className: "text-sm text-destructive", children: errors.email.message })
944
+ ] }) }),
945
+ /* @__PURE__ */ jsxs3(CardFooter3, { className: "flex flex-col gap-4", children: [
946
+ /* @__PURE__ */ jsx9(Button4, { type: "submit", className: "w-full", disabled: isSubmitting, children: isSubmitting ? "Sending\u2026" : "Send reset link" }),
947
+ /* @__PURE__ */ jsx9("a", { href: routes.signin, className: "text-sm text-muted-foreground hover:underline", children: "Back to sign in" })
948
+ ] })
949
+ ] })
950
+ ] }) });
951
+ }
952
+
953
+ // src/front/auth/ResetPasswordPage.tsx
954
+ import { useState as useState10 } from "react";
955
+ import { useForm as useForm4 } from "react-hook-form";
956
+ import { zodResolver as zodResolver4 } from "@hookform/resolvers/zod";
957
+ import { z as z4 } from "zod";
958
+ import { Button as Button5, Card as Card4, CardContent as CardContent4, CardDescription as CardDescription4, CardFooter as CardFooter4, CardHeader as CardHeader4, CardTitle as CardTitle4, Input as Input4, Label as Label4 } from "@hachej/boring-ui-kit";
959
+ import { jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
960
+ var resetSchema = z4.object({
961
+ password: z4.string().min(8, "Password must be at least 8 characters"),
962
+ confirmPassword: z4.string()
963
+ }).refine((data) => data.password === data.confirmPassword, {
964
+ message: "Passwords do not match",
965
+ path: ["confirmPassword"]
966
+ });
967
+ function ResetPasswordPage() {
968
+ const resetPassword = useResetPassword();
969
+ const [serverError, setServerError] = useState10(null);
970
+ const [isSubmitting, setIsSubmitting] = useState10(false);
971
+ const [expired, setExpired] = useState10(false);
972
+ const token = typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("token") : null;
973
+ const {
974
+ register,
975
+ handleSubmit,
976
+ formState: { errors }
977
+ } = useForm4({
978
+ resolver: zodResolver4(resetSchema)
979
+ });
980
+ async function onSubmit(data) {
981
+ if (!token) {
982
+ setExpired(true);
983
+ return;
984
+ }
985
+ setServerError(null);
986
+ setIsSubmitting(true);
987
+ try {
988
+ const result = await resetPassword({ token, newPassword: data.password });
989
+ if (result.error) {
990
+ const status = result.error.status;
991
+ if (status === 410 || status === 400) {
992
+ const msg = result.error.message?.toLowerCase() ?? "";
993
+ if (msg.includes("expired") || msg.includes("invalid") || status === 410) {
994
+ setExpired(true);
995
+ return;
996
+ }
997
+ }
998
+ setServerError(result.error.message ?? "Reset failed");
999
+ } else {
1000
+ window.location.assign(routes.signin);
1001
+ }
1002
+ } catch (err) {
1003
+ setServerError(err instanceof Error ? err.message : "Reset failed");
1004
+ } finally {
1005
+ setIsSubmitting(false);
1006
+ }
1007
+ }
1008
+ if (!token || expired) {
1009
+ return /* @__PURE__ */ jsx10("div", { className: "flex min-h-screen items-center justify-center p-4", children: /* @__PURE__ */ jsxs4(Card4, { className: "w-full max-w-sm", children: [
1010
+ /* @__PURE__ */ jsxs4(CardHeader4, { children: [
1011
+ /* @__PURE__ */ jsx10(CardTitle4, { children: "Link expired" }),
1012
+ /* @__PURE__ */ jsx10(CardDescription4, { children: "This reset link is no longer valid. Please request a new one." })
1013
+ ] }),
1014
+ /* @__PURE__ */ jsx10(CardFooter4, { children: /* @__PURE__ */ jsx10("a", { href: routes.forgotPassword, children: /* @__PURE__ */ jsx10(Button5, { variant: "outline", children: "Request new link" }) }) })
1015
+ ] }) });
1016
+ }
1017
+ return /* @__PURE__ */ jsx10("div", { className: "flex min-h-screen items-center justify-center p-4", children: /* @__PURE__ */ jsxs4(Card4, { className: "w-full max-w-sm", children: [
1018
+ /* @__PURE__ */ jsxs4(CardHeader4, { children: [
1019
+ /* @__PURE__ */ jsx10(CardTitle4, { children: "Reset password" }),
1020
+ /* @__PURE__ */ jsx10(CardDescription4, { children: "Enter your new password below" })
1021
+ ] }),
1022
+ /* @__PURE__ */ jsxs4("form", { onSubmit: handleSubmit(onSubmit), noValidate: true, children: [
1023
+ /* @__PURE__ */ jsxs4(CardContent4, { className: "space-y-4", children: [
1024
+ serverError && /* @__PURE__ */ jsx10("div", { role: "alert", className: "text-sm text-destructive", children: serverError }),
1025
+ /* @__PURE__ */ jsxs4("div", { className: "space-y-2", children: [
1026
+ /* @__PURE__ */ jsx10(Label4, { htmlFor: "password", children: "New password" }),
1027
+ /* @__PURE__ */ jsx10(
1028
+ Input4,
1029
+ {
1030
+ id: "password",
1031
+ type: "password",
1032
+ autoComplete: "new-password",
1033
+ placeholder: "At least 8 characters",
1034
+ ...register("password")
1035
+ }
1036
+ ),
1037
+ errors.password && /* @__PURE__ */ jsx10("p", { className: "text-sm text-destructive", children: errors.password.message })
1038
+ ] }),
1039
+ /* @__PURE__ */ jsxs4("div", { className: "space-y-2", children: [
1040
+ /* @__PURE__ */ jsx10(Label4, { htmlFor: "confirmPassword", children: "Confirm password" }),
1041
+ /* @__PURE__ */ jsx10(
1042
+ Input4,
1043
+ {
1044
+ id: "confirmPassword",
1045
+ type: "password",
1046
+ autoComplete: "new-password",
1047
+ ...register("confirmPassword")
1048
+ }
1049
+ ),
1050
+ errors.confirmPassword && /* @__PURE__ */ jsx10("p", { className: "text-sm text-destructive", children: errors.confirmPassword.message })
1051
+ ] })
1052
+ ] }),
1053
+ /* @__PURE__ */ jsx10(CardFooter4, { children: /* @__PURE__ */ jsx10(Button5, { type: "submit", className: "w-full", disabled: isSubmitting, children: isSubmitting ? "Resetting\u2026" : "Reset password" }) })
1054
+ ] })
1055
+ ] }) });
1056
+ }
1057
+
1058
+ // src/front/auth/VerifyEmailPage.tsx
1059
+ import { useCallback as useCallback3, useEffect as useEffect8, useRef as useRef4, useState as useState11 } from "react";
1060
+ import { Button as Button6, Card as Card5, CardContent as CardContent5, CardDescription as CardDescription5, CardFooter as CardFooter5, CardHeader as CardHeader5, CardTitle as CardTitle5, Input as Input5, Label as Label5 } from "@hachej/boring-ui-kit";
1061
+ import { jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
1062
+ function getCookie(name) {
1063
+ const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
1064
+ return match ? decodeURIComponent(match[1]) : null;
1065
+ }
1066
+ function deleteCookie(name) {
1067
+ document.cookie = `${name}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
1068
+ }
1069
+ function VerifyEmailPage() {
1070
+ const session = useSession();
1071
+ const verifyEmail = useVerifyEmail();
1072
+ const sendVerificationEmail = useSendVerificationEmail();
1073
+ const token = typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("token") : null;
1074
+ const [status, setStatus] = useState11(token ? "verifying" : "no-token");
1075
+ const [inviteWarning, setInviteWarning] = useState11(null);
1076
+ const [cooldown, setCooldown] = useState11(0);
1077
+ const [resendEmail, setResendEmail] = useState11("");
1078
+ const [resendSent, setResendSent] = useState11(false);
1079
+ const verifiedRef = useRef4(false);
1080
+ const sessionEmail = session.data?.user?.email ?? null;
1081
+ useEffect8(() => {
1082
+ const cookie = getCookie("boring_invite_failed");
1083
+ if (cookie) {
1084
+ setInviteWarning(cookie || "Your invite link was invalid; you\u2019re signed in.");
1085
+ deleteCookie("boring_invite_failed");
1086
+ }
1087
+ }, []);
1088
+ useEffect8(() => {
1089
+ if (!token || verifiedRef.current) return;
1090
+ verifiedRef.current = true;
1091
+ (async () => {
1092
+ try {
1093
+ const result = await verifyEmail({ query: { token } });
1094
+ if (result.error) {
1095
+ const s = result.error.status;
1096
+ const msg = (result.error.message ?? "").toLowerCase();
1097
+ if (s === 410 || msg.includes("expired")) {
1098
+ setStatus("expired");
1099
+ } else {
1100
+ setStatus("invalid");
1101
+ }
1102
+ } else {
1103
+ setStatus("verified");
1104
+ }
1105
+ } catch {
1106
+ setStatus("invalid");
1107
+ }
1108
+ })();
1109
+ }, [token, verifyEmail]);
1110
+ useEffect8(() => {
1111
+ if (cooldown <= 0) return;
1112
+ const id = setInterval(() => {
1113
+ setCooldown((c) => {
1114
+ if (c <= 1) {
1115
+ clearInterval(id);
1116
+ return 0;
1117
+ }
1118
+ return c - 1;
1119
+ });
1120
+ }, 1e3);
1121
+ return () => clearInterval(id);
1122
+ }, [cooldown > 0]);
1123
+ const handleResend = useCallback3(async () => {
1124
+ const email = sessionEmail ?? resendEmail.trim();
1125
+ if (!email || cooldown > 0) return;
1126
+ try {
1127
+ await sendVerificationEmail({ email, callbackURL: routes.verifyEmail });
1128
+ } catch {
1129
+ }
1130
+ setResendSent(true);
1131
+ setCooldown(60);
1132
+ }, [sessionEmail, resendEmail, cooldown, sendVerificationEmail]);
1133
+ const resendButton = /* @__PURE__ */ jsx11(
1134
+ Button6,
1135
+ {
1136
+ variant: "outline",
1137
+ className: "w-full",
1138
+ disabled: cooldown > 0 || !sessionEmail && !resendEmail.trim(),
1139
+ onClick: handleResend,
1140
+ children: cooldown > 0 ? `Resend in ${cooldown}s` : "Resend verification email"
1141
+ }
1142
+ );
1143
+ const resendSection = /* @__PURE__ */ jsxs5("div", { className: "space-y-3", children: [
1144
+ !sessionEmail && /* @__PURE__ */ jsxs5("div", { className: "space-y-2", children: [
1145
+ /* @__PURE__ */ jsx11(Label5, { htmlFor: "resend-email", children: "Email" }),
1146
+ /* @__PURE__ */ jsx11(
1147
+ Input5,
1148
+ {
1149
+ id: "resend-email",
1150
+ type: "email",
1151
+ placeholder: "you@example.com",
1152
+ value: resendEmail,
1153
+ onChange: (e) => setResendEmail(e.target.value)
1154
+ }
1155
+ )
1156
+ ] }),
1157
+ resendButton,
1158
+ resendSent && /* @__PURE__ */ jsx11("p", { className: "text-sm text-muted-foreground text-center", children: "If an account exists with that email, we sent a new verification link." })
1159
+ ] });
1160
+ if (status === "verifying") {
1161
+ return /* @__PURE__ */ jsx11("div", { className: "flex min-h-screen items-center justify-center p-4", children: /* @__PURE__ */ jsx11(Card5, { className: "w-full max-w-sm", children: /* @__PURE__ */ jsxs5(CardHeader5, { children: [
1162
+ /* @__PURE__ */ jsx11(CardTitle5, { children: "Verifying your email" }),
1163
+ /* @__PURE__ */ jsx11(CardDescription5, { children: "Please wait\u2026" })
1164
+ ] }) }) });
1165
+ }
1166
+ if (status === "verified") {
1167
+ return /* @__PURE__ */ jsx11("div", { className: "flex min-h-screen items-center justify-center p-4", children: /* @__PURE__ */ jsxs5(Card5, { className: "w-full max-w-sm", children: [
1168
+ inviteWarning && /* @__PURE__ */ jsx11("div", { role: "status", className: "px-6 pt-4", children: /* @__PURE__ */ jsx11("div", { className: "rounded-md bg-muted px-3 py-2 text-sm text-muted-foreground", children: inviteWarning }) }),
1169
+ /* @__PURE__ */ jsxs5(CardHeader5, { children: [
1170
+ /* @__PURE__ */ jsx11(CardTitle5, { children: "Email verified" }),
1171
+ /* @__PURE__ */ jsx11(CardDescription5, { children: "Your email has been verified. You can now continue." })
1172
+ ] }),
1173
+ /* @__PURE__ */ jsx11(CardFooter5, { children: /* @__PURE__ */ jsx11("a", { href: "/", className: "w-full", children: /* @__PURE__ */ jsx11(Button6, { className: "w-full", children: "Continue" }) }) })
1174
+ ] }) });
1175
+ }
1176
+ if (status === "expired") {
1177
+ return /* @__PURE__ */ jsx11("div", { className: "flex min-h-screen items-center justify-center p-4", children: /* @__PURE__ */ jsxs5(Card5, { className: "w-full max-w-sm", children: [
1178
+ inviteWarning && /* @__PURE__ */ jsx11("div", { role: "status", className: "px-6 pt-4", children: /* @__PURE__ */ jsx11("div", { className: "rounded-md bg-muted px-3 py-2 text-sm text-muted-foreground", children: inviteWarning }) }),
1179
+ /* @__PURE__ */ jsxs5(CardHeader5, { children: [
1180
+ /* @__PURE__ */ jsx11(CardTitle5, { children: "Link expired" }),
1181
+ /* @__PURE__ */ jsx11(CardDescription5, { children: "This verification link is no longer valid. Request a new one below." })
1182
+ ] }),
1183
+ /* @__PURE__ */ jsx11(CardContent5, { children: resendSection }),
1184
+ /* @__PURE__ */ jsx11(CardFooter5, { children: /* @__PURE__ */ jsx11("a", { href: routes.signin, className: "text-sm text-muted-foreground hover:underline", children: "Back to sign in" }) })
1185
+ ] }) });
1186
+ }
1187
+ return /* @__PURE__ */ jsx11("div", { className: "flex min-h-screen items-center justify-center p-4", children: /* @__PURE__ */ jsxs5(Card5, { className: "w-full max-w-sm", children: [
1188
+ inviteWarning && /* @__PURE__ */ jsx11("div", { role: "status", className: "px-6 pt-4", children: /* @__PURE__ */ jsx11("div", { className: "rounded-md bg-muted px-3 py-2 text-sm text-muted-foreground", children: inviteWarning }) }),
1189
+ /* @__PURE__ */ jsxs5(CardHeader5, { children: [
1190
+ /* @__PURE__ */ jsx11(CardTitle5, { children: "Invalid verification link" }),
1191
+ /* @__PURE__ */ jsx11(CardDescription5, { children: status === "no-token" ? "No verification token found. Check the link in your email." : "This verification link is invalid. Request a new one below." })
1192
+ ] }),
1193
+ /* @__PURE__ */ jsx11(CardContent5, { children: resendSection }),
1194
+ /* @__PURE__ */ jsx11(CardFooter5, { children: /* @__PURE__ */ jsx11("a", { href: routes.signin, className: "text-sm text-muted-foreground hover:underline", children: "Back to sign in" }) })
1195
+ ] }) });
1196
+ }
1197
+
1198
+ // src/front/auth/UserSettingsPage.tsx
1199
+ import { useCallback as useCallback4, useState as useState12 } from "react";
1200
+ import { useForm as useForm5 } from "react-hook-form";
1201
+ import { zodResolver as zodResolver5 } from "@hookform/resolvers/zod";
1202
+ import { z as z5 } from "zod";
1203
+ import {
1204
+ AlertDialog,
1205
+ AlertDialogCancel,
1206
+ AlertDialogContent,
1207
+ AlertDialogDescription,
1208
+ AlertDialogFooter,
1209
+ AlertDialogHeader,
1210
+ AlertDialogTitle,
1211
+ AlertDialogTrigger,
1212
+ Button as Button7,
1213
+ DetailLine as UiDetailLine,
1214
+ DetailList,
1215
+ Input as Input6,
1216
+ Label as Label6,
1217
+ Notice,
1218
+ SettingsActionRow as UiSettingsActionRow,
1219
+ SettingsNav as UiSettingsNav,
1220
+ SettingsPanel as UiSettingsPanel
1221
+ } from "@hachej/boring-ui-kit";
1222
+ import {
1223
+ CalendarDays,
1224
+ CheckCircle2,
1225
+ KeyRound,
1226
+ Mail,
1227
+ ShieldAlert,
1228
+ Trash2,
1229
+ UserRound
1230
+ } from "lucide-react";
1231
+ import { jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
1232
+ var changePasswordSchema = z5.object({
1233
+ currentPassword: z5.string().min(1, "Current password is required"),
1234
+ newPassword: z5.string().min(8, "Password must be at least 8 characters"),
1235
+ confirmPassword: z5.string()
1236
+ }).refine((data) => data.newPassword === data.confirmPassword, {
1237
+ message: "Passwords do not match",
1238
+ path: ["confirmPassword"]
1239
+ });
1240
+ function initialsFor(name, email) {
1241
+ const source = name?.trim() ? name : email;
1242
+ return source.trim().split(/\s+/).slice(0, 2).map((part) => part[0]?.toUpperCase() ?? "").join("");
1243
+ }
1244
+ function formatMemberSince(value) {
1245
+ const date = new Date(value);
1246
+ if (Number.isNaN(date.getTime())) return "Unknown";
1247
+ return date.toLocaleDateString(void 0, {
1248
+ year: "numeric",
1249
+ month: "long",
1250
+ day: "numeric"
1251
+ });
1252
+ }
1253
+ function SettingsTopBar() {
1254
+ return /* @__PURE__ */ jsx12(
1255
+ "header",
1256
+ {
1257
+ className: "relative flex h-[52px] items-center justify-between gap-3 border-b border-border/40 bg-background px-4",
1258
+ "aria-label": "App top bar",
1259
+ children: /* @__PURE__ */ jsxs6("div", { className: "flex min-w-0 flex-1 items-center gap-2.5", children: [
1260
+ /* @__PURE__ */ jsx12(
1261
+ "div",
1262
+ {
1263
+ "aria-hidden": "true",
1264
+ className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-foreground text-[12px] font-semibold text-background",
1265
+ children: "B"
1266
+ }
1267
+ ),
1268
+ /* @__PURE__ */ jsx12("span", { className: "truncate text-[13px] font-medium tracking-tight text-foreground", children: "Boring" }),
1269
+ /* @__PURE__ */ jsx12("span", { "aria-hidden": "true", className: "text-muted-foreground/30", children: "/" }),
1270
+ /* @__PURE__ */ jsx12("span", { className: "truncate text-[13px] text-muted-foreground", children: "Account settings" })
1271
+ ] })
1272
+ }
1273
+ );
1274
+ }
1275
+ function SettingsPageHeader({
1276
+ initials,
1277
+ displayName,
1278
+ email
1279
+ }) {
1280
+ return /* @__PURE__ */ jsxs6("header", { className: "boring-settings-page-header", children: [
1281
+ /* @__PURE__ */ jsxs6("div", { className: "boring-settings-context", children: [
1282
+ /* @__PURE__ */ jsx12("div", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-foreground text-[12px] font-semibold text-background", children: initials }),
1283
+ /* @__PURE__ */ jsxs6("div", { className: "min-w-0", children: [
1284
+ /* @__PURE__ */ jsxs6("p", { className: "truncate text-[13px] font-medium text-foreground", children: [
1285
+ "Signed in as ",
1286
+ displayName
1287
+ ] }),
1288
+ /* @__PURE__ */ jsxs6("p", { className: "truncate text-[12px] leading-5 text-muted-foreground", children: [
1289
+ email,
1290
+ " account"
1291
+ ] })
1292
+ ] })
1293
+ ] }),
1294
+ /* @__PURE__ */ jsxs6("div", { className: "max-w-2xl", children: [
1295
+ /* @__PURE__ */ jsx12("p", { className: "text-[11px] font-medium uppercase leading-4 text-muted-foreground", children: "Account" }),
1296
+ /* @__PURE__ */ jsx12("h1", { className: "mt-1 text-[20px] font-semibold leading-7 tracking-tight text-foreground", children: "Account settings" }),
1297
+ /* @__PURE__ */ jsx12("p", { className: "mt-2 text-[13px] leading-5 text-muted-foreground", children: "Review your profile, change your password, and manage account-level actions." })
1298
+ ] })
1299
+ ] });
1300
+ }
1301
+ var ACCOUNT_NAV_ITEMS = [
1302
+ { href: "#profile", label: "Profile", description: "Identity and email" },
1303
+ { href: "#password", label: "Password", description: "Sign-in security" },
1304
+ { href: "#danger-zone", label: "Deletion", description: "Permanent actions" }
1305
+ ];
1306
+ function UserSettingsPage({ topBar } = {}) {
1307
+ const session = useSession();
1308
+ const identity = useUser();
1309
+ const signOut = useSignOut();
1310
+ const changePassword = useChangePassword();
1311
+ const user = identity?.user ?? session.data?.user ?? null;
1312
+ const [passwordError, setPasswordError] = useState12(null);
1313
+ const [passwordSuccess, setPasswordSuccess] = useState12(false);
1314
+ const [isChangingPassword, setIsChangingPassword] = useState12(false);
1315
+ const [deleteConfirm, setDeleteConfirm] = useState12("");
1316
+ const [deleteError, setDeleteError] = useState12(null);
1317
+ const [isDeleting, setIsDeleting] = useState12(false);
1318
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState12(false);
1319
+ const {
1320
+ register,
1321
+ handleSubmit,
1322
+ formState: { errors },
1323
+ reset: resetForm
1324
+ } = useForm5({
1325
+ resolver: zodResolver5(changePasswordSchema)
1326
+ });
1327
+ const onChangePassword = useCallback4(
1328
+ async (data) => {
1329
+ setPasswordError(null);
1330
+ setPasswordSuccess(false);
1331
+ setIsChangingPassword(true);
1332
+ try {
1333
+ const result = await changePassword({
1334
+ currentPassword: data.currentPassword,
1335
+ newPassword: data.newPassword,
1336
+ revokeOtherSessions: true
1337
+ });
1338
+ if (result.error) {
1339
+ setPasswordError(result.error.message ?? "Failed to change password");
1340
+ } else {
1341
+ setPasswordSuccess(true);
1342
+ resetForm();
1343
+ }
1344
+ } catch (err) {
1345
+ setPasswordError(err instanceof Error ? err.message : "Failed to change password");
1346
+ } finally {
1347
+ setIsChangingPassword(false);
1348
+ }
1349
+ },
1350
+ [changePassword, resetForm]
1351
+ );
1352
+ const onDeleteAccount = useCallback4(async () => {
1353
+ if (!user?.email) return;
1354
+ setDeleteError(null);
1355
+ setIsDeleting(true);
1356
+ try {
1357
+ await apiFetch("/api/v1/me", {
1358
+ method: "DELETE",
1359
+ headers: { "Content-Type": "application/json" },
1360
+ body: JSON.stringify({ confirm: user.email })
1361
+ });
1362
+ await signOut();
1363
+ } catch (err) {
1364
+ const message = err instanceof Error ? err.message : "Failed to delete account";
1365
+ if (message.includes("sole owner") || message.includes("last_owner")) {
1366
+ setDeleteError(
1367
+ "You are the sole owner of one or more workspaces. Transfer ownership or delete those workspaces first."
1368
+ );
1369
+ } else {
1370
+ setDeleteError(message);
1371
+ }
1372
+ } finally {
1373
+ setIsDeleting(false);
1374
+ }
1375
+ }, [user?.email, signOut]);
1376
+ const topBarNode = topBar === void 0 ? /* @__PURE__ */ jsx12(SettingsTopBar, {}) : topBar;
1377
+ if (!user) {
1378
+ return /* @__PURE__ */ jsxs6("main", { className: "boring-settings-shell", children: [
1379
+ topBarNode,
1380
+ /* @__PURE__ */ jsx12("div", { className: "boring-settings-scroll", children: /* @__PURE__ */ jsx12("div", { className: "mx-auto flex min-h-full w-full max-w-5xl items-center justify-center px-4", children: /* @__PURE__ */ jsxs6("div", { className: "w-full max-w-sm rounded-lg border border-border/60 bg-background p-4", children: [
1381
+ /* @__PURE__ */ jsx12("h1", { className: "text-[13px] font-medium", children: "Account settings" }),
1382
+ /* @__PURE__ */ jsx12("p", { className: "mt-1 text-[12px] text-muted-foreground", children: "Loading your account..." })
1383
+ ] }) }) })
1384
+ ] });
1385
+ }
1386
+ const initials = initialsFor(user.name, user.email);
1387
+ return /* @__PURE__ */ jsxs6("main", { className: "boring-settings-shell", children: [
1388
+ topBarNode,
1389
+ /* @__PURE__ */ jsx12("div", { className: "boring-settings-scroll", children: /* @__PURE__ */ jsxs6("div", { className: "boring-settings-layout", children: [
1390
+ /* @__PURE__ */ jsx12("aside", { className: "boring-settings-sidebar", children: /* @__PURE__ */ jsx12(UiSettingsNav, { label: "Account settings", items: ACCOUNT_NAV_ITEMS }) }),
1391
+ /* @__PURE__ */ jsxs6("div", { className: "boring-settings-content space-y-4", children: [
1392
+ /* @__PURE__ */ jsx12(
1393
+ SettingsPageHeader,
1394
+ {
1395
+ initials,
1396
+ displayName: user.name ?? user.email,
1397
+ email: user.email
1398
+ }
1399
+ ),
1400
+ /* @__PURE__ */ jsx12(
1401
+ UiSettingsPanel,
1402
+ {
1403
+ id: "profile",
1404
+ icon: /* @__PURE__ */ jsx12(UserRound, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1405
+ title: "Profile",
1406
+ description: "The identity shown inside this app.",
1407
+ children: /* @__PURE__ */ jsxs6(DetailList, { children: [
1408
+ /* @__PURE__ */ jsx12(
1409
+ UiDetailLine,
1410
+ {
1411
+ icon: /* @__PURE__ */ jsx12(Mail, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1412
+ label: "Email",
1413
+ children: /* @__PURE__ */ jsx12("p", { className: "truncate", children: user.email })
1414
+ }
1415
+ ),
1416
+ /* @__PURE__ */ jsx12(
1417
+ UiDetailLine,
1418
+ {
1419
+ icon: /* @__PURE__ */ jsx12(UserRound, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1420
+ label: "Name",
1421
+ children: /* @__PURE__ */ jsx12("p", { className: "truncate", children: user.name ?? "Not set" })
1422
+ }
1423
+ ),
1424
+ /* @__PURE__ */ jsx12(
1425
+ UiDetailLine,
1426
+ {
1427
+ icon: /* @__PURE__ */ jsx12(CalendarDays, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1428
+ label: "Member since",
1429
+ children: /* @__PURE__ */ jsx12("p", { children: formatMemberSince(user.createdAt) })
1430
+ }
1431
+ ),
1432
+ /* @__PURE__ */ jsx12(
1433
+ UiDetailLine,
1434
+ {
1435
+ icon: /* @__PURE__ */ jsx12(CheckCircle2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1436
+ label: "Email status",
1437
+ children: /* @__PURE__ */ jsx12("p", { children: user.emailVerified ? "Verified" : "Not verified" })
1438
+ }
1439
+ )
1440
+ ] })
1441
+ }
1442
+ ),
1443
+ /* @__PURE__ */ jsx12("form", { onSubmit: handleSubmit(onChangePassword), noValidate: true, children: /* @__PURE__ */ jsx12(
1444
+ UiSettingsPanel,
1445
+ {
1446
+ id: "password",
1447
+ icon: /* @__PURE__ */ jsx12(KeyRound, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1448
+ title: "Change password",
1449
+ description: "Update the password used for email sign-in.",
1450
+ footer: /* @__PURE__ */ jsx12(Button7, { type: "submit", size: "sm", disabled: isChangingPassword, children: isChangingPassword ? "Changing..." : "Change password" }),
1451
+ children: /* @__PURE__ */ jsxs6("div", { className: "space-y-4", children: [
1452
+ passwordError && /* @__PURE__ */ jsx12(Notice, { role: "alert", tone: "error", description: passwordError }),
1453
+ passwordSuccess && /* @__PURE__ */ jsx12(Notice, { role: "status", tone: "success", description: "Password changed successfully." }),
1454
+ /* @__PURE__ */ jsxs6("div", { className: "grid gap-3 sm:grid-cols-2", children: [
1455
+ /* @__PURE__ */ jsxs6("div", { className: "space-y-2 sm:col-span-2", children: [
1456
+ /* @__PURE__ */ jsx12(Label6, { htmlFor: "currentPassword", className: "text-[12px]", children: "Current password" }),
1457
+ /* @__PURE__ */ jsx12(
1458
+ Input6,
1459
+ {
1460
+ id: "currentPassword",
1461
+ type: "password",
1462
+ className: "h-8 text-[13px]",
1463
+ autoComplete: "current-password",
1464
+ "aria-invalid": errors.currentPassword ? "true" : "false",
1465
+ ...register("currentPassword")
1466
+ }
1467
+ ),
1468
+ errors.currentPassword && /* @__PURE__ */ jsx12("p", { className: "text-[12px] text-destructive", children: errors.currentPassword.message })
1469
+ ] }),
1470
+ /* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
1471
+ /* @__PURE__ */ jsx12(Label6, { htmlFor: "newPassword", className: "text-[12px]", children: "New password" }),
1472
+ /* @__PURE__ */ jsx12(
1473
+ Input6,
1474
+ {
1475
+ id: "newPassword",
1476
+ type: "password",
1477
+ className: "h-8 text-[13px]",
1478
+ autoComplete: "new-password",
1479
+ placeholder: "At least 8 characters",
1480
+ "aria-invalid": errors.newPassword ? "true" : "false",
1481
+ ...register("newPassword")
1482
+ }
1483
+ ),
1484
+ errors.newPassword && /* @__PURE__ */ jsx12("p", { className: "text-[12px] text-destructive", children: errors.newPassword.message })
1485
+ ] }),
1486
+ /* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
1487
+ /* @__PURE__ */ jsx12(Label6, { htmlFor: "confirmPassword", className: "text-[12px]", children: "Confirm new password" }),
1488
+ /* @__PURE__ */ jsx12(
1489
+ Input6,
1490
+ {
1491
+ id: "confirmPassword",
1492
+ type: "password",
1493
+ className: "h-8 text-[13px]",
1494
+ autoComplete: "new-password",
1495
+ "aria-invalid": errors.confirmPassword ? "true" : "false",
1496
+ ...register("confirmPassword")
1497
+ }
1498
+ ),
1499
+ errors.confirmPassword && /* @__PURE__ */ jsx12("p", { className: "text-[12px] text-destructive", children: errors.confirmPassword.message })
1500
+ ] })
1501
+ ] })
1502
+ ] })
1503
+ }
1504
+ ) }),
1505
+ /* @__PURE__ */ jsx12(
1506
+ UiSettingsPanel,
1507
+ {
1508
+ id: "danger-zone",
1509
+ icon: /* @__PURE__ */ jsx12(ShieldAlert, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1510
+ title: "Danger zone",
1511
+ description: "Permanently delete this account and remove its workspace access.",
1512
+ danger: true,
1513
+ children: /* @__PURE__ */ jsx12(
1514
+ UiSettingsActionRow,
1515
+ {
1516
+ title: "Delete account",
1517
+ description: "Delete your account, user settings, and workspace memberships after confirmation.",
1518
+ action: /* @__PURE__ */ jsxs6(AlertDialog, { open: deleteDialogOpen, onOpenChange: setDeleteDialogOpen, children: [
1519
+ /* @__PURE__ */ jsx12(AlertDialogTrigger, { asChild: true, children: /* @__PURE__ */ jsxs6(Button7, { variant: "destructive", size: "sm", children: [
1520
+ /* @__PURE__ */ jsx12(Trash2, { className: "h-4 w-4", "aria-hidden": "true" }),
1521
+ "Delete account"
1522
+ ] }) }),
1523
+ /* @__PURE__ */ jsxs6(AlertDialogContent, { children: [
1524
+ /* @__PURE__ */ jsxs6(AlertDialogHeader, { children: [
1525
+ /* @__PURE__ */ jsx12(AlertDialogTitle, { children: "Delete your account?" }),
1526
+ /* @__PURE__ */ jsx12(AlertDialogDescription, { children: "This action cannot be undone. All your data, workspaces, and settings will be permanently deleted." })
1527
+ ] }),
1528
+ /* @__PURE__ */ jsxs6("div", { className: "space-y-3 py-2", children: [
1529
+ deleteError && /* @__PURE__ */ jsx12(Notice, { role: "alert", tone: "error", description: deleteError }),
1530
+ /* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
1531
+ /* @__PURE__ */ jsxs6(Label6, { htmlFor: "delete-confirm", children: [
1532
+ "Type ",
1533
+ /* @__PURE__ */ jsx12("span", { className: "font-mono font-bold", children: "DELETE" }),
1534
+ " to confirm"
1535
+ ] }),
1536
+ /* @__PURE__ */ jsx12(
1537
+ Input6,
1538
+ {
1539
+ id: "delete-confirm",
1540
+ className: "h-8 text-[13px]",
1541
+ value: deleteConfirm,
1542
+ onChange: (e) => setDeleteConfirm(e.target.value),
1543
+ placeholder: "DELETE",
1544
+ autoComplete: "off"
1545
+ }
1546
+ )
1547
+ ] })
1548
+ ] }),
1549
+ /* @__PURE__ */ jsxs6(AlertDialogFooter, { children: [
1550
+ /* @__PURE__ */ jsx12(
1551
+ AlertDialogCancel,
1552
+ {
1553
+ onClick: () => {
1554
+ setDeleteConfirm("");
1555
+ setDeleteError(null);
1556
+ },
1557
+ children: "Cancel"
1558
+ }
1559
+ ),
1560
+ /* @__PURE__ */ jsx12(
1561
+ Button7,
1562
+ {
1563
+ variant: "destructive",
1564
+ size: "sm",
1565
+ disabled: deleteConfirm !== "DELETE" || isDeleting,
1566
+ onClick: onDeleteAccount,
1567
+ children: isDeleting ? "Deleting..." : "Delete my account"
1568
+ }
1569
+ )
1570
+ ] })
1571
+ ] })
1572
+ ] })
1573
+ }
1574
+ )
1575
+ }
1576
+ )
1577
+ ] })
1578
+ ] }) })
1579
+ ] });
1580
+ }
1581
+
1582
+ // src/front/auth/InviteAcceptPage.tsx
1583
+ import { useCallback as useCallback5, useState as useState13 } from "react";
1584
+ import { useParams as useParams2, useNavigate } from "react-router-dom";
1585
+ import { useQuery as useQuery3, useMutation, useQueryClient as useQueryClient2 } from "@tanstack/react-query";
1586
+ import {
1587
+ Button as Button8,
1588
+ Card as Card6,
1589
+ CardContent as CardContent6,
1590
+ CardDescription as CardDescription6,
1591
+ CardFooter as CardFooter6,
1592
+ CardHeader as CardHeader6,
1593
+ CardTitle as CardTitle6,
1594
+ LoadingState,
1595
+ Notice as Notice2
1596
+ } from "@hachej/boring-ui-kit";
1597
+ import { jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
1598
+ function InviteAcceptPage() {
1599
+ const { token } = useParams2();
1600
+ const session = useSession();
1601
+ const navigate = useNavigate();
1602
+ const queryClient = useQueryClient2();
1603
+ const [acceptError, setAcceptError] = useState13(null);
1604
+ const isSignedIn = Boolean(session.data);
1605
+ const isSessionPending = session.isPending;
1606
+ const resolveQuery = useQuery3({
1607
+ queryKey: ["invite-resolve", token],
1608
+ queryFn: () => apiFetchJson("/api/v1/invites/resolve", {
1609
+ method: "POST",
1610
+ headers: { "Content-Type": "application/json" },
1611
+ body: JSON.stringify({ token })
1612
+ }),
1613
+ enabled: Boolean(token) && !isSessionPending && isSignedIn,
1614
+ retry: false
1615
+ });
1616
+ const acceptMutation = useMutation({
1617
+ mutationFn: () => apiFetchJson("/api/v1/invites/accept", {
1618
+ method: "POST",
1619
+ headers: { "Content-Type": "application/json" },
1620
+ body: JSON.stringify({ token })
1621
+ }),
1622
+ onSuccess: (data) => {
1623
+ queryClient.invalidateQueries({ queryKey: WORKSPACES_QUERY_KEY });
1624
+ navigate(`/w/${data.workspace.id}`);
1625
+ },
1626
+ onError: (err) => {
1627
+ const detail = getHttpErrorDetail(err);
1628
+ if (detail.status === 403 || detail.code === "invite_email_mismatch") {
1629
+ setAcceptError("This invite is for a different email. Sign in with that account to accept.");
1630
+ } else if (detail.status === 410) {
1631
+ setAcceptError("This invite has expired.");
1632
+ } else if (detail.status === 429) {
1633
+ setAcceptError("Too many attempts. Please wait a minute.");
1634
+ } else {
1635
+ setAcceptError(detail.message);
1636
+ }
1637
+ }
1638
+ });
1639
+ const handleAccept = useCallback5(() => {
1640
+ setAcceptError(null);
1641
+ acceptMutation.mutate();
1642
+ }, [acceptMutation]);
1643
+ const handleDecline = useCallback5(() => {
1644
+ navigate("/");
1645
+ }, [navigate]);
1646
+ if (isSessionPending) {
1647
+ return /* @__PURE__ */ jsx13("div", { className: "flex min-h-screen items-center justify-center p-4", children: /* @__PURE__ */ jsx13(Card6, { className: "w-full max-w-md", children: /* @__PURE__ */ jsx13(CardContent6, { className: "py-8 text-center", children: /* @__PURE__ */ jsx13(LoadingState, { "data-testid": "loading", className: "justify-center" }) }) }) });
1648
+ }
1649
+ if (!isSignedIn) {
1650
+ const signinUrl = `${routes.signin}?redirect=${encodeURIComponent(`/invites/${token}`)}`;
1651
+ return /* @__PURE__ */ jsx13("div", { className: "flex min-h-screen items-center justify-center p-4", children: /* @__PURE__ */ jsxs7(Card6, { className: "w-full max-w-md", children: [
1652
+ /* @__PURE__ */ jsxs7(CardHeader6, { children: [
1653
+ /* @__PURE__ */ jsx13(CardTitle6, { children: "Sign in to accept this invite" }),
1654
+ /* @__PURE__ */ jsx13(CardDescription6, { children: "You need to sign in before you can accept a workspace invite." })
1655
+ ] }),
1656
+ /* @__PURE__ */ jsx13(CardFooter6, { children: /* @__PURE__ */ jsx13(Button8, { className: "w-full", onClick: () => navigate(signinUrl), "data-testid": "signin-redirect", children: "Sign in" }) })
1657
+ ] }) });
1658
+ }
1659
+ if (resolveQuery.isLoading) {
1660
+ return /* @__PURE__ */ jsx13("div", { className: "flex min-h-screen items-center justify-center p-4", children: /* @__PURE__ */ jsx13(Card6, { className: "w-full max-w-md", children: /* @__PURE__ */ jsx13(CardContent6, { className: "py-8 text-center", children: /* @__PURE__ */ jsx13(LoadingState, { "data-testid": "loading", className: "justify-center" }) }) }) });
1661
+ }
1662
+ if (resolveQuery.error) {
1663
+ const detail = getHttpErrorDetail(resolveQuery.error);
1664
+ let message;
1665
+ if (detail.status === 404) {
1666
+ message = "This invite is no longer valid. It may have been revoked or never existed.";
1667
+ } else if (detail.status === 410) {
1668
+ message = "This invite has expired.";
1669
+ } else if (detail.status === 423) {
1670
+ message = "This invite is temporarily locked due to too many failed attempts. Try again later.";
1671
+ } else {
1672
+ message = detail.message;
1673
+ }
1674
+ return /* @__PURE__ */ jsx13("div", { className: "flex min-h-screen items-center justify-center p-4", children: /* @__PURE__ */ jsxs7(Card6, { className: "w-full max-w-md", children: [
1675
+ /* @__PURE__ */ jsx13(CardHeader6, { children: /* @__PURE__ */ jsx13(CardTitle6, { children: "Invite unavailable" }) }),
1676
+ /* @__PURE__ */ jsx13(CardContent6, { children: /* @__PURE__ */ jsx13(Notice2, { "data-testid": "resolve-error", tone: "error", description: message }) }),
1677
+ /* @__PURE__ */ jsx13(CardFooter6, { children: /* @__PURE__ */ jsx13(Button8, { variant: "outline", className: "w-full", onClick: () => navigate("/"), children: "Go home" }) })
1678
+ ] }) });
1679
+ }
1680
+ const preview = resolveQuery.data;
1681
+ if (!preview) return null;
1682
+ return /* @__PURE__ */ jsx13("div", { className: "flex min-h-screen items-center justify-center p-4", children: /* @__PURE__ */ jsxs7(Card6, { className: "w-full max-w-md", children: [
1683
+ /* @__PURE__ */ jsxs7(CardHeader6, { children: [
1684
+ /* @__PURE__ */ jsx13(CardTitle6, { children: "You've been invited" }),
1685
+ /* @__PURE__ */ jsxs7(CardDescription6, { children: [
1686
+ "Join ",
1687
+ /* @__PURE__ */ jsx13("strong", { children: preview.workspaceName }),
1688
+ " as ",
1689
+ /* @__PURE__ */ jsx13("strong", { children: preview.role })
1690
+ ] })
1691
+ ] }),
1692
+ /* @__PURE__ */ jsxs7(CardContent6, { className: "space-y-3", children: [
1693
+ acceptError && /* @__PURE__ */ jsx13(Notice2, { role: "alert", "data-testid": "accept-error", tone: "error", description: acceptError }),
1694
+ /* @__PURE__ */ jsxs7("div", { className: "text-sm text-muted-foreground", children: [
1695
+ "This invite expires on",
1696
+ " ",
1697
+ new Date(preview.expiresAt).toLocaleDateString(void 0, {
1698
+ year: "numeric",
1699
+ month: "long",
1700
+ day: "numeric"
1701
+ })
1702
+ ] })
1703
+ ] }),
1704
+ /* @__PURE__ */ jsxs7(CardFooter6, { className: "flex gap-3", children: [
1705
+ /* @__PURE__ */ jsx13(
1706
+ Button8,
1707
+ {
1708
+ variant: "outline",
1709
+ className: "flex-1",
1710
+ onClick: handleDecline,
1711
+ "data-testid": "decline-btn",
1712
+ children: "Decline"
1713
+ }
1714
+ ),
1715
+ /* @__PURE__ */ jsx13(
1716
+ Button8,
1717
+ {
1718
+ className: "flex-1",
1719
+ disabled: acceptMutation.isPending,
1720
+ onClick: handleAccept,
1721
+ "data-testid": "accept-btn",
1722
+ children: acceptMutation.isPending ? "Accepting\u2026" : "Accept invite"
1723
+ }
1724
+ )
1725
+ ] })
1726
+ ] }) });
1727
+ }
1728
+
1729
+ // src/front/AuthGate.tsx
1730
+ import { useEffect as useEffect9, useMemo as useMemo3, useRef as useRef5 } from "react";
1731
+ import { Fragment, jsx as jsx14 } from "react/jsx-runtime";
1732
+ var DEFAULT_GRACE_MS = 3e4;
1733
+ var UNSAFE_REDIRECT_RE = /[\0\r\n<>"'`]/;
1734
+ function normalizePath(pathname) {
1735
+ if (!pathname) return "/";
1736
+ return pathname.startsWith("/") ? pathname : `/${pathname}`;
1737
+ }
1738
+ function normalizeSearch(search) {
1739
+ if (!search) return "";
1740
+ return search.startsWith("?") ? search : `?${search}`;
1741
+ }
1742
+ function normalizeHash(hash) {
1743
+ if (!hash) return "";
1744
+ return hash.startsWith("#") ? hash : `#${hash}`;
1745
+ }
1746
+ function buildCurrentPath(location) {
1747
+ return `${normalizePath(location.pathname)}${normalizeSearch(location.search)}${normalizeHash(location.hash)}`;
1748
+ }
1749
+ function normalizePublicPath(path) {
1750
+ const normalized = normalizePath(path);
1751
+ if (normalized.length > 1 && normalized.endsWith("/")) return normalized.slice(0, -1);
1752
+ return normalized;
1753
+ }
1754
+ function isPublicPath(pathname, publicPaths) {
1755
+ const normalizedPath = normalizePath(pathname);
1756
+ if (normalizedPath === "/auth" || normalizedPath.startsWith("/auth/")) return true;
1757
+ return publicPaths.some((candidate) => normalizedPath === candidate || normalizedPath.startsWith(`${candidate}/`));
1758
+ }
1759
+ function readSafeRedirect(search) {
1760
+ const redirect = new URLSearchParams(normalizeSearch(search)).get("redirect");
1761
+ if (!redirect) return null;
1762
+ if (!redirect.startsWith("/") || redirect.startsWith("//")) return null;
1763
+ if (UNSAFE_REDIRECT_RE.test(redirect)) return null;
1764
+ return redirect;
1765
+ }
1766
+ function defaultLocation() {
1767
+ if (typeof window === "undefined") return { pathname: "/" };
1768
+ return {
1769
+ pathname: window.location.pathname,
1770
+ search: window.location.search,
1771
+ hash: window.location.hash
1772
+ };
1773
+ }
1774
+ function defaultNavigate(to, options) {
1775
+ if (typeof window === "undefined") return;
1776
+ if (options?.replace) {
1777
+ window.location.replace(to);
1778
+ return;
1779
+ }
1780
+ window.location.assign(to);
1781
+ }
1782
+ function AuthGate({
1783
+ children,
1784
+ publicPaths = [],
1785
+ graceMs = DEFAULT_GRACE_MS,
1786
+ location,
1787
+ navigate,
1788
+ now
1789
+ }) {
1790
+ const session = useSession();
1791
+ const nullSinceRef = useRef5(null);
1792
+ const redirectTimerRef = useRef5(null);
1793
+ const readNow = now ?? Date.now;
1794
+ const currentLocation = location ?? defaultLocation();
1795
+ const goTo = navigate ?? defaultNavigate;
1796
+ const normalizedPublicPaths = useMemo3(
1797
+ () => publicPaths.map(normalizePublicPath),
1798
+ [publicPaths]
1799
+ );
1800
+ useEffect9(() => {
1801
+ return () => {
1802
+ if (!redirectTimerRef.current) return;
1803
+ clearTimeout(redirectTimerRef.current);
1804
+ redirectTimerRef.current = null;
1805
+ };
1806
+ }, []);
1807
+ useEffect9(() => {
1808
+ if (redirectTimerRef.current) {
1809
+ clearTimeout(redirectTimerRef.current);
1810
+ redirectTimerRef.current = null;
1811
+ }
1812
+ const pathname = normalizePath(currentLocation.pathname);
1813
+ const currentPath = buildCurrentPath(currentLocation);
1814
+ if (session.data) {
1815
+ nullSinceRef.current = null;
1816
+ if (pathname === routes.signin) {
1817
+ const destination = readSafeRedirect(currentLocation.search) ?? "/";
1818
+ if (destination !== currentPath) {
1819
+ goTo(destination, { replace: true });
1820
+ }
1821
+ }
1822
+ return;
1823
+ }
1824
+ if (session.isPending || isPublicPath(pathname, normalizedPublicPaths)) {
1825
+ nullSinceRef.current = null;
1826
+ return;
1827
+ }
1828
+ const nowMs = readNow();
1829
+ if (nullSinceRef.current === null) {
1830
+ nullSinceRef.current = nowMs;
1831
+ }
1832
+ const elapsedMs = nowMs - nullSinceRef.current;
1833
+ if (elapsedMs >= graceMs) {
1834
+ goTo(`${routes.signin}?redirect=${encodeURIComponent(currentPath)}`, { replace: true });
1835
+ return;
1836
+ }
1837
+ const remainingMs = graceMs - elapsedMs;
1838
+ redirectTimerRef.current = setTimeout(() => {
1839
+ goTo(`${routes.signin}?redirect=${encodeURIComponent(currentPath)}`, { replace: true });
1840
+ }, remainingMs);
1841
+ redirectTimerRef.current.unref?.();
1842
+ }, [currentLocation, goTo, graceMs, normalizedPublicPaths, readNow, session.data, session.isPending]);
1843
+ return /* @__PURE__ */ jsx14(Fragment, { children });
1844
+ }
1845
+
1846
+ // src/front/CoreFront.tsx
1847
+ import { Suspense, useMemo as useMemo6 } from "react";
1848
+ import { BrowserRouter, Routes, Route } from "react-router-dom";
1849
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
1850
+ import { Helmet, HelmetProvider } from "react-helmet-async";
1851
+
1852
+ // src/front/components/UserMenu.tsx
1853
+ import { useMemo as useMemo4, useState as useState14 } from "react";
1854
+ import {
1855
+ Button as Button9,
1856
+ DropdownMenu,
1857
+ DropdownMenuContent,
1858
+ DropdownMenuItem,
1859
+ DropdownMenuLabel,
1860
+ DropdownMenuSeparator,
1861
+ DropdownMenuTrigger
1862
+ } from "@hachej/boring-ui-kit";
1863
+ import {
1864
+ ChevronDown,
1865
+ Check,
1866
+ LogOut,
1867
+ Monitor,
1868
+ Moon,
1869
+ Settings,
1870
+ Sun
1871
+ } from "lucide-react";
1872
+ import { useNavigate as useNavigate2 } from "react-router-dom";
1873
+ import { jsx as jsx15, jsxs as jsxs8 } from "react/jsx-runtime";
1874
+ var THEME_ORDER = ["light", "dark", "system"];
1875
+ function labelForTheme(preference) {
1876
+ if (preference === "light") return "Light";
1877
+ if (preference === "dark") return "Dark";
1878
+ return "System";
1879
+ }
1880
+ function iconForTheme(preference) {
1881
+ if (preference === "light") return Sun;
1882
+ if (preference === "dark") return Moon;
1883
+ return Monitor;
1884
+ }
1885
+ function initialsFor2(name, email) {
1886
+ if (name && name.trim().length > 0) {
1887
+ return name.trim().split(/\s+/).slice(0, 2).map((part) => part[0]?.toUpperCase() ?? "").join("");
1888
+ }
1889
+ return email.slice(0, 2).toUpperCase();
1890
+ }
1891
+ function UserMenu() {
1892
+ const identity = useUser();
1893
+ const signOut = useSignOut();
1894
+ const navigate = useNavigate2();
1895
+ const { preference, setTheme } = useTheme();
1896
+ const [isSigningOut, setIsSigningOut] = useState14(false);
1897
+ const user = identity?.user;
1898
+ const userName = user?.name ?? "Unknown user";
1899
+ const userEmail = user?.email ?? "unknown@example.com";
1900
+ const initials = useMemo4(() => initialsFor2(user?.name ?? null, userEmail), [user?.name, userEmail]);
1901
+ async function handleSignOut() {
1902
+ if (isSigningOut) return;
1903
+ setIsSigningOut(true);
1904
+ try {
1905
+ await signOut();
1906
+ } finally {
1907
+ setIsSigningOut(false);
1908
+ navigate(routes.signin);
1909
+ }
1910
+ }
1911
+ return /* @__PURE__ */ jsxs8(DropdownMenu, { children: [
1912
+ /* @__PURE__ */ jsx15(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs8(
1913
+ Button9,
1914
+ {
1915
+ type: "button",
1916
+ variant: "ghost",
1917
+ "aria-label": "User menu",
1918
+ className: "h-8 rounded-md border border-transparent bg-transparent px-1 pr-1.5 text-foreground shadow-none hover:bg-foreground/5 focus-visible:ring-1 focus-visible:ring-ring",
1919
+ children: [
1920
+ /* @__PURE__ */ jsx15("span", { className: "inline-flex h-7 w-7 items-center justify-center rounded-md bg-foreground text-[11px] font-semibold text-background", children: initials }),
1921
+ /* @__PURE__ */ jsx15(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground", "aria-hidden": "true" })
1922
+ ]
1923
+ }
1924
+ ) }),
1925
+ /* @__PURE__ */ jsxs8(
1926
+ DropdownMenuContent,
1927
+ {
1928
+ align: "end",
1929
+ sideOffset: 8,
1930
+ className: "w-80 rounded-lg border-border/70 bg-[color:var(--surface-workbench-left)] p-2 shadow-2xl",
1931
+ children: [
1932
+ /* @__PURE__ */ jsx15(DropdownMenuLabel, { className: "p-2", children: /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-3", children: [
1933
+ /* @__PURE__ */ jsx15("span", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-foreground text-[12px] font-semibold text-background", children: initials }),
1934
+ /* @__PURE__ */ jsxs8("span", { className: "min-w-0 space-y-1", children: [
1935
+ /* @__PURE__ */ jsx15("span", { className: "block truncate text-sm font-medium leading-none", children: userName }),
1936
+ /* @__PURE__ */ jsx15("span", { className: "block truncate text-xs font-normal text-muted-foreground", children: userEmail })
1937
+ ] })
1938
+ ] }) }),
1939
+ /* @__PURE__ */ jsx15(DropdownMenuSeparator, { className: "-mx-2" }),
1940
+ /* @__PURE__ */ jsx15(DropdownMenuLabel, { className: "px-2 pb-1 pt-2 text-[11px] font-medium text-muted-foreground", children: "Theme" }),
1941
+ THEME_ORDER.map((theme) => {
1942
+ const Icon = iconForTheme(theme);
1943
+ const selected = preference === theme;
1944
+ return /* @__PURE__ */ jsxs8(
1945
+ DropdownMenuItem,
1946
+ {
1947
+ "aria-label": labelForTheme(theme),
1948
+ "data-current": selected ? "true" : "false",
1949
+ onSelect: (event) => {
1950
+ event.preventDefault();
1951
+ setTheme(theme);
1952
+ },
1953
+ className: "gap-3 rounded-md py-2 text-[13px] focus:bg-foreground/[0.06] focus:text-foreground",
1954
+ children: [
1955
+ /* @__PURE__ */ jsx15(Icon, { className: "h-4 w-4 text-muted-foreground", "aria-hidden": "true" }),
1956
+ /* @__PURE__ */ jsx15("span", { className: "flex-1", children: labelForTheme(theme) }),
1957
+ selected ? /* @__PURE__ */ jsx15(Check, { className: "h-4 w-4 text-foreground", "aria-hidden": "true" }) : null
1958
+ ]
1959
+ },
1960
+ theme
1961
+ );
1962
+ }),
1963
+ /* @__PURE__ */ jsx15(DropdownMenuSeparator, { className: "-mx-2" }),
1964
+ /* @__PURE__ */ jsxs8(
1965
+ DropdownMenuItem,
1966
+ {
1967
+ "aria-label": "User settings",
1968
+ onSelect: () => navigate(routes.me),
1969
+ className: "gap-3 rounded-md py-2 text-[13px] focus:bg-foreground/[0.06] focus:text-foreground",
1970
+ children: [
1971
+ /* @__PURE__ */ jsx15(Settings, { className: "h-4 w-4", "aria-hidden": "true" }),
1972
+ /* @__PURE__ */ jsxs8("span", { className: "flex min-w-0 flex-col", children: [
1973
+ /* @__PURE__ */ jsx15("span", { children: "User settings" }),
1974
+ /* @__PURE__ */ jsx15("span", { className: "text-xs text-muted-foreground", children: "Password and account controls" })
1975
+ ] })
1976
+ ]
1977
+ }
1978
+ ),
1979
+ /* @__PURE__ */ jsx15(DropdownMenuSeparator, { className: "-mx-2" }),
1980
+ /* @__PURE__ */ jsxs8(
1981
+ DropdownMenuItem,
1982
+ {
1983
+ variant: "destructive",
1984
+ onSelect: (event) => {
1985
+ event.preventDefault();
1986
+ void handleSignOut();
1987
+ },
1988
+ disabled: isSigningOut,
1989
+ className: "gap-3 rounded-md py-2 text-[13px]",
1990
+ children: [
1991
+ /* @__PURE__ */ jsx15(LogOut, { className: "h-4 w-4", "aria-hidden": "true" }),
1992
+ isSigningOut ? "Signing out..." : "Sign out"
1993
+ ]
1994
+ }
1995
+ )
1996
+ ]
1997
+ }
1998
+ )
1999
+ ] });
2000
+ }
2001
+
2002
+ // src/front/components/WorkspaceSwitcher.tsx
2003
+ import { useMemo as useMemo5, useState as useState15 } from "react";
2004
+ import { useQuery as useQuery4, useQueryClient as useQueryClient3 } from "@tanstack/react-query";
2005
+ import {
2006
+ Button as Button10,
2007
+ Dialog,
2008
+ DialogContent,
2009
+ DialogDescription,
2010
+ DialogFooter,
2011
+ DialogHeader,
2012
+ DialogTitle,
2013
+ DropdownMenu as DropdownMenu2,
2014
+ DropdownMenuContent as DropdownMenuContent2,
2015
+ DropdownMenuItem as DropdownMenuItem2,
2016
+ DropdownMenuLabel as DropdownMenuLabel2,
2017
+ DropdownMenuSeparator as DropdownMenuSeparator2,
2018
+ DropdownMenuTrigger as DropdownMenuTrigger2,
2019
+ Input as Input7,
2020
+ Label as Label7,
2021
+ useToast
2022
+ } from "@hachej/boring-ui-kit";
2023
+ import { Check as Check2, ChevronsUpDown, LayoutGrid, Plus, Settings as Settings2 } from "lucide-react";
2024
+ import { useNavigate as useNavigate3 } from "react-router-dom";
2025
+ import { z as z6 } from "zod";
2026
+ import { Fragment as Fragment2, jsx as jsx16, jsxs as jsxs9 } from "react/jsx-runtime";
2027
+ var workspaceNameSchema = z6.object({
2028
+ name: z6.string().trim().min(1, "Workspace name is required").max(100, "Workspace name must be 100 characters or fewer")
2029
+ });
2030
+ function useToastCompat() {
2031
+ return useToast();
2032
+ }
2033
+ function validateWorkspaceName(name) {
2034
+ const parsed = workspaceNameSchema.safeParse({ name });
2035
+ if (!parsed.success) {
2036
+ return {
2037
+ parsedName: null,
2038
+ message: parsed.error.issues[0]?.message ?? "Invalid workspace name"
2039
+ };
2040
+ }
2041
+ return {
2042
+ parsedName: parsed.data.name,
2043
+ message: null
2044
+ };
2045
+ }
2046
+ function useWorkspaces() {
2047
+ return useQuery4({
2048
+ queryKey: WORKSPACES_QUERY_KEY,
2049
+ queryFn: async () => {
2050
+ const data = await apiFetchJson("/api/v1/workspaces");
2051
+ return data.workspaces;
2052
+ }
2053
+ });
2054
+ }
2055
+ function hrefForWorkspace(prefix, workspaceId, suffix = "") {
2056
+ const normalized = prefix.startsWith("/") ? prefix : `/${prefix}`;
2057
+ return `${normalized.replace(/\/$/, "")}/${encodeURIComponent(workspaceId)}${suffix}`;
2058
+ }
2059
+ function workspaceInitial(name) {
2060
+ return (name.trim()[0] ?? "W").toUpperCase();
2061
+ }
2062
+ function WorkspaceSwitcher({
2063
+ appTitle = "Boring",
2064
+ workspacePathPrefix = "/workspace"
2065
+ }) {
2066
+ const navigate = useNavigate3();
2067
+ const queryClient = useQueryClient3();
2068
+ const { toast } = useToastCompat();
2069
+ const currentWorkspace = useCurrentWorkspace();
2070
+ const workspacesQuery = useWorkspaces();
2071
+ const workspaces = workspacesQuery.data ?? [];
2072
+ const [isModalOpen, setIsModalOpen] = useState15(false);
2073
+ const [name, setName] = useState15("");
2074
+ const [attemptedSubmit, setAttemptedSubmit] = useState15(false);
2075
+ const [isSubmitting, setIsSubmitting] = useState15(false);
2076
+ const [serverError, setServerError] = useState15(null);
2077
+ const nameValidation = useMemo5(() => validateWorkspaceName(name), [name]);
2078
+ const shouldShowNameError = name.length > 100 || attemptedSubmit && nameValidation.message !== null;
2079
+ const nameError = shouldShowNameError ? nameValidation.message : null;
2080
+ function openCreateWorkspace() {
2081
+ setIsModalOpen(true);
2082
+ setServerError(null);
2083
+ }
2084
+ function resetModalState() {
2085
+ setName("");
2086
+ setAttemptedSubmit(false);
2087
+ setIsSubmitting(false);
2088
+ setServerError(null);
2089
+ }
2090
+ function onModalChange(nextOpen) {
2091
+ setIsModalOpen(nextOpen);
2092
+ if (!nextOpen) {
2093
+ resetModalState();
2094
+ }
2095
+ }
2096
+ async function handleCreateWorkspace(event) {
2097
+ event.preventDefault();
2098
+ setAttemptedSubmit(true);
2099
+ setServerError(null);
2100
+ const parsed = workspaceNameSchema.safeParse({ name });
2101
+ if (!parsed.success) {
2102
+ return;
2103
+ }
2104
+ setIsSubmitting(true);
2105
+ try {
2106
+ const data = await apiFetchJson("/api/v1/workspaces", {
2107
+ method: "POST",
2108
+ headers: { "content-type": "application/json" },
2109
+ body: JSON.stringify({ name: parsed.data.name })
2110
+ });
2111
+ await Promise.all([
2112
+ queryClient.invalidateQueries({ queryKey: WORKSPACES_QUERY_KEY }),
2113
+ queryClient.invalidateQueries({ queryKey: workspaceQueryKey(data.workspace.id) })
2114
+ ]);
2115
+ onModalChange(false);
2116
+ navigate(hrefForWorkspace(workspacePathPrefix, data.workspace.id));
2117
+ } catch (error) {
2118
+ const detail = getHttpErrorDetail(error);
2119
+ if (typeof detail.status === "number" && detail.status >= 400 && detail.status < 500) {
2120
+ toast({
2121
+ title: "Unable to create workspace",
2122
+ description: detail.message,
2123
+ variant: "destructive"
2124
+ });
2125
+ } else {
2126
+ setServerError(detail.message);
2127
+ }
2128
+ } finally {
2129
+ setIsSubmitting(false);
2130
+ }
2131
+ }
2132
+ const switcherLabel = currentWorkspace?.name ?? "Select workspace";
2133
+ return /* @__PURE__ */ jsxs9(Fragment2, { children: [
2134
+ workspaces.length === 0 ? /* @__PURE__ */ jsxs9(
2135
+ Button10,
2136
+ {
2137
+ type: "button",
2138
+ variant: "ghost",
2139
+ onClick: openCreateWorkspace,
2140
+ className: "-ml-1 h-8 gap-2 rounded-md px-1 pr-2.5 hover:bg-foreground/5 focus-visible:ring-1 focus-visible:ring-ring",
2141
+ children: [
2142
+ /* @__PURE__ */ jsx16(
2143
+ "span",
2144
+ {
2145
+ "aria-hidden": "true",
2146
+ className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-foreground text-[12px] font-semibold text-background",
2147
+ children: appTitle.charAt(0).toUpperCase()
2148
+ }
2149
+ ),
2150
+ /* @__PURE__ */ jsx16("span", { className: "text-[13px] font-medium text-foreground", children: "Create your first workspace" })
2151
+ ]
2152
+ }
2153
+ ) : /* @__PURE__ */ jsxs9(DropdownMenu2, { children: [
2154
+ /* @__PURE__ */ jsx16(DropdownMenuTrigger2, { asChild: true, children: /* @__PURE__ */ jsxs9(
2155
+ Button10,
2156
+ {
2157
+ type: "button",
2158
+ variant: "ghost",
2159
+ "aria-label": `Workspace menu: ${switcherLabel}`,
2160
+ className: "-ml-1 h-8 min-w-0 justify-start gap-2.5 border border-transparent px-1 py-1 text-left",
2161
+ children: [
2162
+ /* @__PURE__ */ jsx16(
2163
+ "span",
2164
+ {
2165
+ "aria-hidden": "true",
2166
+ className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-foreground text-[12px] font-semibold text-background",
2167
+ children: appTitle.charAt(0).toUpperCase()
2168
+ }
2169
+ ),
2170
+ /* @__PURE__ */ jsxs9("span", { className: "flex min-w-0 items-center gap-1.5", children: [
2171
+ /* @__PURE__ */ jsx16("span", { className: "truncate text-[13px] font-medium text-foreground", children: appTitle }),
2172
+ /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", className: "text-muted-foreground/30", children: "/" }),
2173
+ /* @__PURE__ */ jsx16("span", { className: "truncate text-[13px] font-normal text-muted-foreground", children: switcherLabel })
2174
+ ] }),
2175
+ /* @__PURE__ */ jsx16(ChevronsUpDown, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground/55", "aria-hidden": "true" })
2176
+ ]
2177
+ }
2178
+ ) }),
2179
+ /* @__PURE__ */ jsxs9(
2180
+ DropdownMenuContent2,
2181
+ {
2182
+ align: "start",
2183
+ sideOffset: 8,
2184
+ className: "w-80 rounded-lg border-border/70 bg-[color:var(--surface-workbench-left)] p-2 shadow-2xl",
2185
+ children: [
2186
+ /* @__PURE__ */ jsx16(DropdownMenuLabel2, { className: "px-2 pb-2 pt-1", children: /* @__PURE__ */ jsxs9("span", { className: "flex items-center gap-2 text-xs font-medium text-muted-foreground", children: [
2187
+ /* @__PURE__ */ jsx16(LayoutGrid, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2188
+ "Workspaces"
2189
+ ] }) }),
2190
+ /* @__PURE__ */ jsx16("div", { className: "max-h-72 overflow-y-auto pr-1", children: workspaces.map((workspace) => {
2191
+ const isCurrent = currentWorkspace?.id === workspace.id;
2192
+ return /* @__PURE__ */ jsxs9(
2193
+ DropdownMenuItem2,
2194
+ {
2195
+ "aria-label": workspace.name,
2196
+ "data-current": isCurrent ? "true" : "false",
2197
+ onSelect: () => navigate(hrefForWorkspace(workspacePathPrefix, workspace.id)),
2198
+ className: "gap-3 rounded-md py-2 text-[13px] focus:bg-foreground/[0.06] focus:text-foreground",
2199
+ children: [
2200
+ /* @__PURE__ */ jsx16("span", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border/60 bg-background text-xs font-semibold text-muted-foreground", children: workspaceInitial(workspace.name) }),
2201
+ /* @__PURE__ */ jsx16("span", { className: "min-w-0 flex-1 truncate text-sm", children: workspace.name }),
2202
+ isCurrent ? /* @__PURE__ */ jsx16(Check2, { className: "h-4 w-4 text-foreground", "aria-hidden": "true" }) : null
2203
+ ]
2204
+ },
2205
+ workspace.id
2206
+ );
2207
+ }) }),
2208
+ /* @__PURE__ */ jsx16(DropdownMenuSeparator2, { className: "-mx-2" }),
2209
+ /* @__PURE__ */ jsxs9(
2210
+ DropdownMenuItem2,
2211
+ {
2212
+ "aria-label": "Create workspace",
2213
+ onSelect: (event) => {
2214
+ event.preventDefault();
2215
+ openCreateWorkspace();
2216
+ },
2217
+ className: "gap-3 rounded-md py-2 text-[13px] focus:bg-foreground/[0.06] focus:text-foreground",
2218
+ children: [
2219
+ /* @__PURE__ */ jsx16(Plus, { className: "h-4 w-4", "aria-hidden": "true" }),
2220
+ /* @__PURE__ */ jsxs9("span", { className: "flex min-w-0 flex-col", children: [
2221
+ /* @__PURE__ */ jsx16("span", { children: "Create workspace" }),
2222
+ /* @__PURE__ */ jsx16("span", { className: "text-xs text-muted-foreground", children: "Start a clean project space" })
2223
+ ] })
2224
+ ]
2225
+ }
2226
+ ),
2227
+ currentWorkspace ? /* @__PURE__ */ jsxs9(
2228
+ DropdownMenuItem2,
2229
+ {
2230
+ "aria-label": "Workspace settings",
2231
+ onSelect: () => navigate(hrefForWorkspace(workspacePathPrefix, currentWorkspace.id, "/settings")),
2232
+ className: "gap-3 rounded-md py-2 text-[13px] focus:bg-foreground/[0.06] focus:text-foreground",
2233
+ children: [
2234
+ /* @__PURE__ */ jsx16(Settings2, { className: "h-4 w-4", "aria-hidden": "true" }),
2235
+ /* @__PURE__ */ jsxs9("span", { className: "flex min-w-0 flex-col", children: [
2236
+ /* @__PURE__ */ jsx16("span", { children: "Workspace settings" }),
2237
+ /* @__PURE__ */ jsx16("span", { className: "text-xs text-muted-foreground", children: "Rename, runtime, deletion" })
2238
+ ] })
2239
+ ]
2240
+ }
2241
+ ) : null
2242
+ ]
2243
+ }
2244
+ )
2245
+ ] }),
2246
+ /* @__PURE__ */ jsx16(Dialog, { open: isModalOpen, onOpenChange: onModalChange, children: /* @__PURE__ */ jsxs9(DialogContent, { children: [
2247
+ /* @__PURE__ */ jsxs9(DialogHeader, { children: [
2248
+ /* @__PURE__ */ jsx16(DialogTitle, { children: "Create workspace" }),
2249
+ /* @__PURE__ */ jsx16(DialogDescription, { children: "Choose a name for your new workspace." })
2250
+ ] }),
2251
+ /* @__PURE__ */ jsxs9("form", { onSubmit: (event) => void handleCreateWorkspace(event), className: "space-y-4", children: [
2252
+ /* @__PURE__ */ jsxs9("div", { className: "space-y-2", children: [
2253
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between gap-3", children: [
2254
+ /* @__PURE__ */ jsx16(Label7, { htmlFor: "workspace-name", children: "Name" }),
2255
+ /* @__PURE__ */ jsxs9("span", { className: "text-xs text-muted-foreground", children: [
2256
+ name.length,
2257
+ "/100"
2258
+ ] })
2259
+ ] }),
2260
+ /* @__PURE__ */ jsx16(
2261
+ Input7,
2262
+ {
2263
+ id: "workspace-name",
2264
+ name: "name",
2265
+ value: name,
2266
+ maxLength: 101,
2267
+ onChange: (event) => {
2268
+ setName(event.target.value);
2269
+ if (serverError) setServerError(null);
2270
+ },
2271
+ placeholder: "My Workspace",
2272
+ "aria-invalid": nameError ? "true" : "false",
2273
+ autoFocus: true
2274
+ }
2275
+ ),
2276
+ nameError ? /* @__PURE__ */ jsx16("p", { role: "alert", className: "text-sm text-destructive", children: nameError }) : null,
2277
+ serverError ? /* @__PURE__ */ jsx16("p", { role: "alert", className: "text-sm text-destructive", children: serverError }) : null
2278
+ ] }),
2279
+ /* @__PURE__ */ jsxs9(DialogFooter, { children: [
2280
+ /* @__PURE__ */ jsx16(
2281
+ Button10,
2282
+ {
2283
+ type: "button",
2284
+ variant: "ghost",
2285
+ onClick: () => onModalChange(false),
2286
+ disabled: isSubmitting,
2287
+ children: "Cancel"
2288
+ }
2289
+ ),
2290
+ /* @__PURE__ */ jsx16(
2291
+ Button10,
2292
+ {
2293
+ type: "submit",
2294
+ disabled: isSubmitting || nameValidation.parsedName === null,
2295
+ children: isSubmitting ? "Creating..." : "Create workspace"
2296
+ }
2297
+ )
2298
+ ] })
2299
+ ] })
2300
+ ] }) })
2301
+ ] });
2302
+ }
2303
+
2304
+ // src/front/components/ThemeToggle.tsx
2305
+ import { Button as Button11 } from "@hachej/boring-ui-kit";
2306
+ import { jsxs as jsxs10 } from "react/jsx-runtime";
2307
+ var THEME_ORDER2 = ["light", "dark", "system"];
2308
+ function nextTheme(preference) {
2309
+ const index = THEME_ORDER2.indexOf(preference);
2310
+ const nextIndex = index === -1 ? 0 : (index + 1) % THEME_ORDER2.length;
2311
+ return THEME_ORDER2[nextIndex];
2312
+ }
2313
+ function labelForTheme2(preference) {
2314
+ if (preference === "light") return "Light";
2315
+ if (preference === "dark") return "Dark";
2316
+ return "System";
2317
+ }
2318
+ function ThemeToggle() {
2319
+ const { preference, setTheme } = useTheme();
2320
+ return /* @__PURE__ */ jsxs10(
2321
+ Button11,
2322
+ {
2323
+ type: "button",
2324
+ variant: "outline",
2325
+ onClick: () => setTheme(nextTheme(preference)),
2326
+ "aria-label": "Theme toggle",
2327
+ "data-theme-preference": preference,
2328
+ children: [
2329
+ "Theme: ",
2330
+ labelForTheme2(preference)
2331
+ ]
2332
+ }
2333
+ );
2334
+ }
2335
+
2336
+ // src/front/workspace/InvitesPage.tsx
2337
+ import { useCallback as useCallback6, useState as useState16 } from "react";
2338
+ import { useQuery as useQuery5, useMutation as useMutation2, useQueryClient as useQueryClient4 } from "@tanstack/react-query";
2339
+ import {
2340
+ Button as Button12,
2341
+ Card as Card7,
2342
+ CardContent as CardContent7,
2343
+ CardDescription as CardDescription7,
2344
+ CardFooter as CardFooter7,
2345
+ CardHeader as CardHeader7,
2346
+ CardTitle as CardTitle7,
2347
+ Input as Input8,
2348
+ Label as Label8,
2349
+ LoadingState as LoadingState2,
2350
+ Notice as Notice3,
2351
+ Select,
2352
+ SelectContent,
2353
+ SelectItem,
2354
+ SelectTrigger,
2355
+ SelectValue,
2356
+ StatusBadge
2357
+ } from "@hachej/boring-ui-kit";
2358
+ import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
2359
+ function invitesQueryKey(workspaceId) {
2360
+ return ["invites", workspaceId];
2361
+ }
2362
+ function generateIdempotencyKey() {
2363
+ return globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2);
2364
+ }
2365
+ function getInviteStatus(invite) {
2366
+ if (invite.acceptedAt) return "accepted";
2367
+ if (new Date(invite.expiresAt) <= /* @__PURE__ */ new Date()) return "expired";
2368
+ return "pending";
2369
+ }
2370
+ var STATUS_TONES = {
2371
+ pending: "info",
2372
+ expired: "neutral",
2373
+ accepted: "success"
2374
+ };
2375
+ function InvitesPage() {
2376
+ const workspace = useCurrentWorkspace();
2377
+ const role = useWorkspaceRole();
2378
+ const queryClient = useQueryClient4();
2379
+ const [email, setEmail] = useState16("");
2380
+ const [inviteRole, setInviteRole] = useState16("editor");
2381
+ const [formError, setFormError] = useState16(null);
2382
+ const [successMessage, setSuccessMessage] = useState16(null);
2383
+ const workspaceId = workspace?.id ?? "";
2384
+ const encodedWorkspaceId = encodeURIComponent(workspaceId);
2385
+ const invitesQuery = useQuery5({
2386
+ queryKey: invitesQueryKey(workspaceId),
2387
+ queryFn: () => apiFetchJson(
2388
+ `/api/v1/workspaces/${encodedWorkspaceId}/invites`
2389
+ ).then((data) => data.invites),
2390
+ enabled: workspaceId.length > 0
2391
+ });
2392
+ const createMutation = useMutation2({
2393
+ mutationFn: async ({ email: invEmail, role: invRole }) => {
2394
+ const idempotencyKey = generateIdempotencyKey();
2395
+ const response = await apiFetch(`/api/v1/workspaces/${encodedWorkspaceId}/invites`, {
2396
+ method: "POST",
2397
+ headers: {
2398
+ "Content-Type": "application/json",
2399
+ "Idempotency-Key": idempotencyKey
2400
+ },
2401
+ body: JSON.stringify({ email: invEmail, role: invRole })
2402
+ });
2403
+ return response.json();
2404
+ },
2405
+ onSuccess: (_data, variables) => {
2406
+ queryClient.invalidateQueries({ queryKey: invitesQueryKey(workspaceId) });
2407
+ setEmail("");
2408
+ setInviteRole("editor");
2409
+ setFormError(null);
2410
+ setSuccessMessage(`Invite sent to ${variables.email}`);
2411
+ },
2412
+ onError: (err) => {
2413
+ setSuccessMessage(null);
2414
+ const detail = getHttpErrorDetail(err);
2415
+ setFormError(detail.message);
2416
+ }
2417
+ });
2418
+ const [revokeError, setRevokeError] = useState16(null);
2419
+ const revokeMutation = useMutation2({
2420
+ mutationFn: async (inviteId) => {
2421
+ await apiFetch(
2422
+ `/api/v1/workspaces/${encodedWorkspaceId}/invites/${encodeURIComponent(inviteId)}`,
2423
+ { method: "DELETE" }
2424
+ );
2425
+ },
2426
+ onSuccess: () => {
2427
+ setRevokeError(null);
2428
+ queryClient.invalidateQueries({ queryKey: invitesQueryKey(workspaceId) });
2429
+ },
2430
+ onError: (err) => {
2431
+ const detail = getHttpErrorDetail(err);
2432
+ setRevokeError(detail.message);
2433
+ }
2434
+ });
2435
+ const handleSubmit = useCallback6(
2436
+ (e) => {
2437
+ e.preventDefault();
2438
+ setFormError(null);
2439
+ setSuccessMessage(null);
2440
+ const trimmed = email.trim();
2441
+ if (!trimmed) {
2442
+ setFormError("Email is required");
2443
+ return;
2444
+ }
2445
+ createMutation.mutate({ email: trimmed, role: inviteRole });
2446
+ },
2447
+ [email, inviteRole, createMutation]
2448
+ );
2449
+ if (role !== "owner") {
2450
+ return /* @__PURE__ */ jsx17("div", { className: "flex min-h-screen items-center justify-center p-4", children: /* @__PURE__ */ jsx17(Card7, { className: "w-full max-w-md", children: /* @__PURE__ */ jsxs11(CardHeader7, { children: [
2451
+ /* @__PURE__ */ jsx17(CardTitle7, { children: "Access denied" }),
2452
+ /* @__PURE__ */ jsx17(CardDescription7, { children: "Only workspace owners can manage invites." })
2453
+ ] }) }) });
2454
+ }
2455
+ return /* @__PURE__ */ jsx17("div", { className: "flex min-h-screen items-center justify-center p-4", children: /* @__PURE__ */ jsxs11("div", { className: "w-full max-w-2xl space-y-6", children: [
2456
+ /* @__PURE__ */ jsxs11(Card7, { children: [
2457
+ /* @__PURE__ */ jsxs11(CardHeader7, { children: [
2458
+ /* @__PURE__ */ jsx17(CardTitle7, { children: "Invite a member" }),
2459
+ /* @__PURE__ */ jsxs11(CardDescription7, { children: [
2460
+ "Send an invite to join ",
2461
+ workspace?.name ?? "this workspace",
2462
+ "."
2463
+ ] })
2464
+ ] }),
2465
+ /* @__PURE__ */ jsxs11("form", { onSubmit: handleSubmit, "data-testid": "invite-form", children: [
2466
+ /* @__PURE__ */ jsxs11(CardContent7, { className: "space-y-4", children: [
2467
+ formError && /* @__PURE__ */ jsx17(Notice3, { role: "alert", tone: "error", description: formError }),
2468
+ successMessage && /* @__PURE__ */ jsx17(Notice3, { role: "status", tone: "success", description: successMessage }),
2469
+ /* @__PURE__ */ jsxs11("div", { className: "space-y-2", children: [
2470
+ /* @__PURE__ */ jsx17(Label8, { htmlFor: "invite-email", children: "Email address" }),
2471
+ /* @__PURE__ */ jsx17(
2472
+ Input8,
2473
+ {
2474
+ id: "invite-email",
2475
+ type: "email",
2476
+ placeholder: "colleague@example.com",
2477
+ value: email,
2478
+ onChange: (e) => setEmail(e.target.value),
2479
+ autoComplete: "email"
2480
+ }
2481
+ )
2482
+ ] }),
2483
+ /* @__PURE__ */ jsxs11("div", { className: "space-y-2", children: [
2484
+ /* @__PURE__ */ jsx17(Label8, { htmlFor: "invite-role", children: "Role" }),
2485
+ /* @__PURE__ */ jsxs11(Select, { value: inviteRole, onValueChange: (value) => setInviteRole(value), children: [
2486
+ /* @__PURE__ */ jsx17(SelectTrigger, { id: "invite-role", "data-testid": "invite-role-select", children: /* @__PURE__ */ jsx17(SelectValue, { placeholder: "Select role" }) }),
2487
+ /* @__PURE__ */ jsxs11(SelectContent, { children: [
2488
+ /* @__PURE__ */ jsx17(SelectItem, { value: "editor", children: "Editor" }),
2489
+ /* @__PURE__ */ jsx17(SelectItem, { value: "viewer", children: "Viewer" }),
2490
+ /* @__PURE__ */ jsx17(SelectItem, { value: "owner", children: "Owner" })
2491
+ ] })
2492
+ ] })
2493
+ ] })
2494
+ ] }),
2495
+ /* @__PURE__ */ jsx17(CardFooter7, { children: /* @__PURE__ */ jsx17(
2496
+ Button12,
2497
+ {
2498
+ type: "submit",
2499
+ className: "w-full",
2500
+ disabled: createMutation.isPending,
2501
+ children: createMutation.isPending ? "Sending\u2026" : "Send invite"
2502
+ }
2503
+ ) })
2504
+ ] })
2505
+ ] }),
2506
+ /* @__PURE__ */ jsxs11(Card7, { children: [
2507
+ /* @__PURE__ */ jsxs11(CardHeader7, { children: [
2508
+ /* @__PURE__ */ jsx17(CardTitle7, { children: "Pending invites" }),
2509
+ /* @__PURE__ */ jsx17(CardDescription7, { children: invitesQuery.data?.length ? `${invitesQuery.data.length} invite${invitesQuery.data.length === 1 ? "" : "s"}` : "No invites yet" })
2510
+ ] }),
2511
+ /* @__PURE__ */ jsxs11(CardContent7, { children: [
2512
+ revokeError && /* @__PURE__ */ jsx17(Notice3, { role: "alert", tone: "error", className: "mb-4", description: revokeError }),
2513
+ invitesQuery.isLoading && /* @__PURE__ */ jsx17(LoadingState2, {}),
2514
+ invitesQuery.isError && /* @__PURE__ */ jsx17(Notice3, { tone: "error", description: "Failed to load invites." }),
2515
+ invitesQuery.data && invitesQuery.data.length > 0 && /* @__PURE__ */ jsx17("div", { className: "divide-y", "data-testid": "invites-list", children: invitesQuery.data.map((invite) => {
2516
+ const status = getInviteStatus(invite);
2517
+ return /* @__PURE__ */ jsxs11(
2518
+ "div",
2519
+ {
2520
+ className: "flex items-center justify-between py-3",
2521
+ "data-testid": `invite-row-${invite.id}`,
2522
+ children: [
2523
+ /* @__PURE__ */ jsxs11("div", { className: "space-y-1", children: [
2524
+ /* @__PURE__ */ jsx17("p", { className: "text-sm font-medium", children: invite.email }),
2525
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
2526
+ /* @__PURE__ */ jsx17("span", { children: invite.role }),
2527
+ /* @__PURE__ */ jsx17(StatusBadge, { "data-testid": `status-${status}`, tone: STATUS_TONES[status] ?? "neutral", children: status }),
2528
+ /* @__PURE__ */ jsxs11("span", { children: [
2529
+ "expires",
2530
+ " ",
2531
+ new Date(invite.expiresAt).toLocaleDateString()
2532
+ ] })
2533
+ ] })
2534
+ ] }),
2535
+ status === "pending" && /* @__PURE__ */ jsx17(
2536
+ Button12,
2537
+ {
2538
+ variant: "destructive",
2539
+ size: "sm",
2540
+ disabled: revokeMutation.isPending,
2541
+ onClick: () => revokeMutation.mutate(invite.id),
2542
+ "data-testid": `revoke-${invite.id}`,
2543
+ children: "Revoke"
2544
+ }
2545
+ )
2546
+ ]
2547
+ },
2548
+ invite.id
2549
+ );
2550
+ }) })
2551
+ ] })
2552
+ ] })
2553
+ ] }) });
2554
+ }
2555
+
2556
+ // src/front/workspace/MembersPage.tsx
2557
+ import { useCallback as useCallback7, useState as useState17 } from "react";
2558
+ import { useMutation as useMutation3, useQueryClient as useQueryClient5 } from "@tanstack/react-query";
2559
+ import {
2560
+ AlertDialog as AlertDialog2,
2561
+ AlertDialogCancel as AlertDialogCancel2,
2562
+ AlertDialogContent as AlertDialogContent2,
2563
+ AlertDialogDescription as AlertDialogDescription2,
2564
+ AlertDialogFooter as AlertDialogFooter2,
2565
+ AlertDialogHeader as AlertDialogHeader2,
2566
+ AlertDialogTitle as AlertDialogTitle2,
2567
+ Button as Button13,
2568
+ Card as Card8,
2569
+ CardContent as CardContent8,
2570
+ CardDescription as CardDescription8,
2571
+ CardHeader as CardHeader8,
2572
+ CardTitle as CardTitle8,
2573
+ Select as Select2,
2574
+ SelectContent as SelectContent2,
2575
+ SelectItem as SelectItem2,
2576
+ SelectTrigger as SelectTrigger2,
2577
+ SelectValue as SelectValue2,
2578
+ InitialsAvatar,
2579
+ LoadingState as LoadingState3,
2580
+ Notice as Notice4
2581
+ } from "@hachej/boring-ui-kit";
2582
+ import { jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
2583
+ var ROLE_OPTIONS = ["owner", "editor", "viewer"];
2584
+ function MembersPage() {
2585
+ const workspace = useCurrentWorkspace();
2586
+ const myRole = useWorkspaceRole();
2587
+ const session = useSession();
2588
+ const queryClient = useQueryClient5();
2589
+ const workspaceId = workspace?.id ?? "";
2590
+ const currentUserId = session.data?.user?.id ?? "";
2591
+ const encodedWorkspaceId = encodeURIComponent(workspaceId);
2592
+ const membersQuery = useWorkspaceMembers(workspaceId);
2593
+ const [toast, setToast] = useState17(null);
2594
+ const [confirmTarget, setConfirmTarget] = useState17(null);
2595
+ const showToast = useCallback7((msg) => {
2596
+ setToast(msg);
2597
+ setTimeout(() => setToast(null), 4e3);
2598
+ }, []);
2599
+ const changeRoleMutation = useMutation3({
2600
+ mutationFn: async ({ userId, role }) => {
2601
+ await apiFetch(
2602
+ `/api/v1/workspaces/${encodedWorkspaceId}/members/${encodeURIComponent(userId)}/role`,
2603
+ {
2604
+ method: "PATCH",
2605
+ headers: { "Content-Type": "application/json" },
2606
+ body: JSON.stringify({ role })
2607
+ }
2608
+ );
2609
+ },
2610
+ onSuccess: () => {
2611
+ queryClient.invalidateQueries({ queryKey: ["members", workspaceId] });
2612
+ },
2613
+ onError: (err) => {
2614
+ const detail = getHttpErrorDetail(err);
2615
+ if (detail.code === "last_owner") {
2616
+ showToast("Cannot demote: workspace would have no owners.");
2617
+ } else {
2618
+ showToast(detail.message);
2619
+ }
2620
+ queryClient.invalidateQueries({ queryKey: ["members", workspaceId] });
2621
+ }
2622
+ });
2623
+ const removeMutation = useMutation3({
2624
+ mutationFn: async (userId) => {
2625
+ await apiFetch(
2626
+ `/api/v1/workspaces/${encodedWorkspaceId}/members/${encodeURIComponent(userId)}`,
2627
+ { method: "DELETE" }
2628
+ );
2629
+ },
2630
+ onSuccess: () => {
2631
+ queryClient.invalidateQueries({ queryKey: ["members", workspaceId] });
2632
+ setConfirmTarget(null);
2633
+ },
2634
+ onError: (err) => {
2635
+ const detail = getHttpErrorDetail(err);
2636
+ if (detail.code === "last_owner") {
2637
+ showToast("Cannot remove: workspace would have no owners.");
2638
+ } else {
2639
+ showToast(detail.message);
2640
+ }
2641
+ setConfirmTarget(null);
2642
+ }
2643
+ });
2644
+ const handleRoleChange = useCallback7(
2645
+ (userId, newRole) => {
2646
+ changeRoleMutation.mutate({ userId, role: newRole });
2647
+ },
2648
+ [changeRoleMutation]
2649
+ );
2650
+ const handleRemoveConfirm = useCallback7(() => {
2651
+ if (!confirmTarget) return;
2652
+ removeMutation.mutate(confirmTarget.userId);
2653
+ }, [confirmTarget, removeMutation]);
2654
+ const isOwner = myRole === "owner";
2655
+ return /* @__PURE__ */ jsx18("div", { className: "flex min-h-screen items-center justify-center p-4", children: /* @__PURE__ */ jsxs12("div", { className: "w-full max-w-2xl space-y-6", children: [
2656
+ /* @__PURE__ */ jsxs12(Card8, { children: [
2657
+ /* @__PURE__ */ jsxs12(CardHeader8, { children: [
2658
+ /* @__PURE__ */ jsx18(CardTitle8, { children: "Members" }),
2659
+ /* @__PURE__ */ jsxs12(CardDescription8, { children: [
2660
+ workspace?.name ?? "Workspace",
2661
+ " \xB7",
2662
+ " ",
2663
+ membersQuery.data?.length ?? 0,
2664
+ " member",
2665
+ (membersQuery.data?.length ?? 0) !== 1 ? "s" : ""
2666
+ ] })
2667
+ ] }),
2668
+ /* @__PURE__ */ jsxs12(CardContent8, { children: [
2669
+ toast && /* @__PURE__ */ jsx18(Notice4, { role: "alert", "data-testid": "toast", tone: "error", className: "mb-4", description: toast }),
2670
+ membersQuery.isLoading && /* @__PURE__ */ jsx18(LoadingState3, {}),
2671
+ membersQuery.isError && /* @__PURE__ */ jsx18(Notice4, { tone: "error", description: "Failed to load members." }),
2672
+ membersQuery.data && membersQuery.data.length > 0 && /* @__PURE__ */ jsx18("div", { className: "divide-y", "data-testid": "members-list", children: membersQuery.data.map((member) => {
2673
+ const isSelf = member.userId === currentUserId;
2674
+ const canChangeRole = isOwner && !isSelf;
2675
+ const canRemove = isOwner || isSelf;
2676
+ return /* @__PURE__ */ jsxs12(
2677
+ "div",
2678
+ {
2679
+ className: "flex items-center justify-between py-3",
2680
+ "data-testid": `member-row-${member.userId}`,
2681
+ children: [
2682
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-3", children: [
2683
+ /* @__PURE__ */ jsx18(InitialsAvatar, { initials: (member.user.name?.[0] ?? member.user.email[0]).toUpperCase() }),
2684
+ /* @__PURE__ */ jsxs12("div", { children: [
2685
+ /* @__PURE__ */ jsxs12("p", { className: "text-sm font-medium", children: [
2686
+ member.user.name ?? member.user.email,
2687
+ isSelf && /* @__PURE__ */ jsx18("span", { className: "ml-1 text-xs text-muted-foreground", children: "(you)" })
2688
+ ] }),
2689
+ /* @__PURE__ */ jsx18("p", { className: "text-xs text-muted-foreground", children: member.user.email })
2690
+ ] })
2691
+ ] }),
2692
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-2", children: [
2693
+ /* @__PURE__ */ jsxs12(
2694
+ Select2,
2695
+ {
2696
+ value: member.role,
2697
+ disabled: !canChangeRole,
2698
+ onValueChange: (value) => handleRoleChange(member.userId, value),
2699
+ children: [
2700
+ /* @__PURE__ */ jsx18(SelectTrigger2, { "data-testid": `role-select-${member.userId}`, className: "h-8 w-28 text-xs", children: /* @__PURE__ */ jsx18(SelectValue2, { placeholder: "Role" }) }),
2701
+ /* @__PURE__ */ jsx18(SelectContent2, { children: ROLE_OPTIONS.map((r) => /* @__PURE__ */ jsx18(SelectItem2, { value: r, children: r }, r)) })
2702
+ ]
2703
+ }
2704
+ ),
2705
+ canRemove && /* @__PURE__ */ jsx18(
2706
+ Button13,
2707
+ {
2708
+ variant: "destructive",
2709
+ size: "sm",
2710
+ "data-testid": `remove-${member.userId}`,
2711
+ onClick: () => setConfirmTarget(member),
2712
+ children: isSelf ? "Leave" : "Remove"
2713
+ }
2714
+ )
2715
+ ] })
2716
+ ]
2717
+ },
2718
+ member.userId
2719
+ );
2720
+ }) })
2721
+ ] })
2722
+ ] }),
2723
+ /* @__PURE__ */ jsx18(
2724
+ AlertDialog2,
2725
+ {
2726
+ open: confirmTarget !== null,
2727
+ onOpenChange: (open) => {
2728
+ if (!open) setConfirmTarget(null);
2729
+ },
2730
+ children: /* @__PURE__ */ jsxs12(AlertDialogContent2, { children: [
2731
+ /* @__PURE__ */ jsxs12(AlertDialogHeader2, { children: [
2732
+ /* @__PURE__ */ jsx18(AlertDialogTitle2, { children: confirmTarget?.userId === currentUserId ? "Leave workspace?" : `Remove ${confirmTarget?.user.name ?? confirmTarget?.user.email}?` }),
2733
+ /* @__PURE__ */ jsx18(AlertDialogDescription2, { children: confirmTarget?.userId === currentUserId ? "You will lose access to this workspace." : "This member will lose access to the workspace." })
2734
+ ] }),
2735
+ /* @__PURE__ */ jsxs12(AlertDialogFooter2, { children: [
2736
+ /* @__PURE__ */ jsx18(AlertDialogCancel2, { children: "Cancel" }),
2737
+ /* @__PURE__ */ jsx18(
2738
+ Button13,
2739
+ {
2740
+ variant: "destructive",
2741
+ disabled: removeMutation.isPending,
2742
+ onClick: handleRemoveConfirm,
2743
+ "data-testid": "confirm-remove",
2744
+ children: removeMutation.isPending ? "Removing\u2026" : confirmTarget?.userId === currentUserId ? "Leave" : "Remove"
2745
+ }
2746
+ )
2747
+ ] })
2748
+ ] })
2749
+ }
2750
+ )
2751
+ ] }) });
2752
+ }
2753
+
2754
+ // src/front/workspace/WorkspaceSettingsPage.tsx
2755
+ import { useCallback as useCallback8, useState as useState18 } from "react";
2756
+ import { useQuery as useQuery6, useMutation as useMutation4, useQueryClient as useQueryClient6 } from "@tanstack/react-query";
2757
+ import { useNavigate as useNavigate4 } from "react-router-dom";
2758
+ import {
2759
+ AlertDialog as AlertDialog3,
2760
+ AlertDialogCancel as AlertDialogCancel3,
2761
+ AlertDialogContent as AlertDialogContent3,
2762
+ AlertDialogDescription as AlertDialogDescription3,
2763
+ AlertDialogFooter as AlertDialogFooter3,
2764
+ AlertDialogHeader as AlertDialogHeader3,
2765
+ AlertDialogTitle as AlertDialogTitle3,
2766
+ Button as Button14,
2767
+ IconButton,
2768
+ SettingsActionRow as UiSettingsActionRow2,
2769
+ SettingsNav as UiSettingsNav2,
2770
+ SettingsPanel as UiSettingsPanel2,
2771
+ StatusBadge as StatusBadge2,
2772
+ Input as Input9,
2773
+ Label as Label9,
2774
+ Notice as Notice5
2775
+ } from "@hachej/boring-ui-kit";
2776
+ import {
2777
+ AlertCircle,
2778
+ HardDrive,
2779
+ RefreshCw,
2780
+ Settings2 as Settings22,
2781
+ ShieldAlert as ShieldAlert2,
2782
+ Trash2 as Trash22
2783
+ } from "lucide-react";
2784
+ import { Fragment as Fragment3, jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
2785
+ var STATE_TONES = {
2786
+ pending: "info",
2787
+ ready: "success",
2788
+ error: "danger"
2789
+ };
2790
+ function SettingsTopBar2({ workspaceId, workspaceName }) {
2791
+ const navigate = useNavigate4();
2792
+ const workspaceHref = workspaceId ? `/workspace/${encodeURIComponent(workspaceId)}` : "/";
2793
+ return /* @__PURE__ */ jsx19(
2794
+ "header",
2795
+ {
2796
+ className: "relative flex h-[52px] items-center justify-between gap-3 border-b border-border/40 bg-background px-4",
2797
+ "aria-label": "App top bar",
2798
+ children: /* @__PURE__ */ jsxs13("div", { className: "flex min-w-0 flex-1 items-center gap-2.5", children: [
2799
+ /* @__PURE__ */ jsx19(
2800
+ IconButton,
2801
+ {
2802
+ type: "button",
2803
+ variant: "default",
2804
+ size: "icon-xs",
2805
+ "aria-label": "Back to workspace",
2806
+ title: "Back to workspace",
2807
+ onClick: () => navigate(workspaceHref),
2808
+ className: "shrink-0 bg-foreground text-[12px] font-semibold text-background hover:bg-foreground/90",
2809
+ children: "B"
2810
+ }
2811
+ ),
2812
+ /* @__PURE__ */ jsx19("span", { className: "truncate text-[13px] font-medium tracking-tight text-foreground", children: "Boring" }),
2813
+ /* @__PURE__ */ jsx19("span", { "aria-hidden": "true", className: "text-muted-foreground/30", children: "/" }),
2814
+ /* @__PURE__ */ jsx19("span", { className: "truncate text-[13px] text-muted-foreground", children: workspaceName }),
2815
+ /* @__PURE__ */ jsx19("span", { "aria-hidden": "true", className: "text-muted-foreground/30", children: "/" }),
2816
+ /* @__PURE__ */ jsx19("span", { className: "truncate text-[13px] text-muted-foreground", children: "Settings" })
2817
+ ] })
2818
+ }
2819
+ );
2820
+ }
2821
+ function SettingsPageHeader2({
2822
+ workspaceName,
2823
+ workspaceInitial: workspaceInitial2,
2824
+ role,
2825
+ isDefault
2826
+ }) {
2827
+ return /* @__PURE__ */ jsxs13("header", { className: "boring-settings-page-header", children: [
2828
+ /* @__PURE__ */ jsxs13("div", { className: "boring-settings-context", children: [
2829
+ /* @__PURE__ */ jsx19("div", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-foreground text-[12px] font-semibold text-background", children: workspaceInitial2 }),
2830
+ /* @__PURE__ */ jsxs13("div", { className: "min-w-0 flex-1", children: [
2831
+ /* @__PURE__ */ jsx19("p", { className: "truncate text-[13px] font-medium text-foreground", children: workspaceName }),
2832
+ /* @__PURE__ */ jsxs13("div", { className: "mt-1 flex flex-wrap items-center gap-1.5", children: [
2833
+ /* @__PURE__ */ jsx19("span", { className: "inline-flex h-5 items-center rounded border border-border/60 px-1.5 text-[11px] text-muted-foreground", children: roleLabel(role) }),
2834
+ isDefault ? /* @__PURE__ */ jsx19("span", { className: "inline-flex h-5 items-center rounded border border-border/60 px-1.5 text-[11px] text-muted-foreground", children: "Default" }) : null
2835
+ ] })
2836
+ ] })
2837
+ ] }),
2838
+ /* @__PURE__ */ jsxs13("div", { className: "max-w-2xl", children: [
2839
+ /* @__PURE__ */ jsx19("p", { className: "text-[11px] font-medium uppercase leading-4 text-muted-foreground", children: "Workspace" }),
2840
+ /* @__PURE__ */ jsx19("h1", { className: "mt-1 text-[20px] font-semibold leading-7 tracking-tight text-foreground", children: "Workspace settings" }),
2841
+ /* @__PURE__ */ jsx19("p", { className: "mt-2 text-[13px] leading-5 text-muted-foreground", children: "Manage workspace identity, runtime recovery, and irreversible workspace actions." })
2842
+ ] })
2843
+ ] });
2844
+ }
2845
+ function FieldNote({ children }) {
2846
+ return /* @__PURE__ */ jsx19("p", { className: "text-[12px] leading-5 text-muted-foreground", children });
2847
+ }
2848
+ function roleLabel(role) {
2849
+ if (!role) return "Loading role";
2850
+ return role.charAt(0).toUpperCase() + role.slice(1);
2851
+ }
2852
+ var WORKSPACE_NAV_ITEMS = [
2853
+ { href: "#general", label: "General", description: "Name and access" },
2854
+ { href: "#runtime", label: "Runtime", description: "Provisioning state" },
2855
+ { href: "#danger-zone", label: "Danger zone", description: "Permanent actions" }
2856
+ ];
2857
+ function WorkspaceSettingsPage({ topBar } = {}) {
2858
+ const workspace = useCurrentWorkspace();
2859
+ const role = useWorkspaceRole();
2860
+ const queryClient = useQueryClient6();
2861
+ const navigate = useNavigate4();
2862
+ const workspaceId = workspace?.id ?? "";
2863
+ const [nameValue, setNameValue] = useState18(null);
2864
+ const [nameError, setNameError] = useState18(null);
2865
+ const [retryError, setRetryError] = useState18(null);
2866
+ const [deleteConfirmName, setDeleteConfirmName] = useState18("");
2867
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState18(false);
2868
+ const [deleteError, setDeleteError] = useState18(null);
2869
+ const displayName = nameValue ?? workspace?.name ?? "";
2870
+ const encodedWorkspaceId = encodeURIComponent(workspaceId);
2871
+ const runtimeQuery = useQuery6({
2872
+ queryKey: ["runtime", workspaceId],
2873
+ queryFn: async () => {
2874
+ try {
2875
+ const data = await apiFetchJson(
2876
+ `/api/v1/workspaces/${encodedWorkspaceId}/runtime`
2877
+ );
2878
+ return data.runtime;
2879
+ } catch (err) {
2880
+ const detail = getHttpErrorDetail(err);
2881
+ if (detail.status === 404) return null;
2882
+ throw err;
2883
+ }
2884
+ },
2885
+ enabled: workspaceId.length > 0
2886
+ });
2887
+ const renameMutation = useMutation4({
2888
+ mutationFn: async (name) => {
2889
+ await apiFetch(`/api/v1/workspaces/${encodedWorkspaceId}`, {
2890
+ method: "PUT",
2891
+ headers: { "Content-Type": "application/json" },
2892
+ body: JSON.stringify({ name })
2893
+ });
2894
+ },
2895
+ onSuccess: () => {
2896
+ setNameError(null);
2897
+ queryClient.invalidateQueries({ queryKey: workspaceQueryKey(workspaceId) });
2898
+ queryClient.invalidateQueries({ queryKey: WORKSPACES_QUERY_KEY });
2899
+ setNameValue(null);
2900
+ },
2901
+ onError: (err) => {
2902
+ const detail = getHttpErrorDetail(err);
2903
+ setNameError(detail.message);
2904
+ }
2905
+ });
2906
+ const retryMutation = useMutation4({
2907
+ mutationFn: async () => {
2908
+ await apiFetch(`/api/v1/workspaces/${encodedWorkspaceId}/runtime/retry`, {
2909
+ method: "POST"
2910
+ });
2911
+ },
2912
+ onSuccess: () => {
2913
+ setRetryError(null);
2914
+ queryClient.invalidateQueries({ queryKey: ["runtime", workspaceId] });
2915
+ },
2916
+ onError: (err) => {
2917
+ const detail = getHttpErrorDetail(err);
2918
+ setRetryError(detail.message);
2919
+ }
2920
+ });
2921
+ const deleteMutation = useMutation4({
2922
+ mutationFn: async () => {
2923
+ await apiFetch(`/api/v1/workspaces/${encodedWorkspaceId}`, {
2924
+ method: "DELETE"
2925
+ });
2926
+ },
2927
+ onSuccess: () => {
2928
+ queryClient.invalidateQueries({ queryKey: WORKSPACES_QUERY_KEY });
2929
+ setDeleteDialogOpen(false);
2930
+ navigate("/");
2931
+ },
2932
+ onError: (err) => {
2933
+ const detail = getHttpErrorDetail(err);
2934
+ if (detail.code === "destroy_failed") {
2935
+ setDeleteError(`Destroy failed: ${detail.message}. Try again.`);
2936
+ } else if (detail.code === "provision_failed" || detail.status === 409) {
2937
+ setDeleteError(detail.message);
2938
+ } else {
2939
+ setDeleteError(detail.message);
2940
+ }
2941
+ setDeleteDialogOpen(false);
2942
+ }
2943
+ });
2944
+ const handleSaveName = useCallback8(() => {
2945
+ const trimmed = displayName.trim();
2946
+ if (!trimmed || trimmed === workspace?.name) return;
2947
+ renameMutation.mutate(trimmed);
2948
+ }, [displayName, workspace?.name, renameMutation]);
2949
+ const handleDelete = useCallback8(() => {
2950
+ setDeleteError(null);
2951
+ deleteMutation.mutate();
2952
+ }, [deleteMutation]);
2953
+ const runtime = runtimeQuery.data ?? null;
2954
+ const hasRuntime = runtime !== null && runtimeQuery.isSuccess;
2955
+ const nameChanged = nameValue !== null && nameValue.trim() !== workspace?.name;
2956
+ const canEditName = role !== "viewer";
2957
+ const canDeleteWorkspace = role === "owner" || role === null;
2958
+ const workspaceName = workspace?.name ?? "Workspace";
2959
+ const workspaceInitial2 = (workspace?.name?.trim()?.[0] ?? "W").toUpperCase();
2960
+ const topBarNode = topBar === void 0 ? /* @__PURE__ */ jsx19(SettingsTopBar2, { workspaceId, workspaceName }) : topBar;
2961
+ const navItems = hasRuntime ? WORKSPACE_NAV_ITEMS : WORKSPACE_NAV_ITEMS.filter((item) => item.href !== "#runtime");
2962
+ return /* @__PURE__ */ jsxs13("main", { className: "boring-settings-shell", children: [
2963
+ topBarNode,
2964
+ /* @__PURE__ */ jsx19("div", { className: "boring-settings-scroll", children: /* @__PURE__ */ jsxs13("div", { className: "boring-settings-layout", children: [
2965
+ /* @__PURE__ */ jsx19("aside", { className: "boring-settings-sidebar", children: /* @__PURE__ */ jsx19(UiSettingsNav2, { label: "Workspace settings", items: navItems }) }),
2966
+ /* @__PURE__ */ jsxs13("div", { className: "boring-settings-content space-y-4", children: [
2967
+ /* @__PURE__ */ jsx19(
2968
+ SettingsPageHeader2,
2969
+ {
2970
+ workspaceName,
2971
+ workspaceInitial: workspaceInitial2,
2972
+ role,
2973
+ isDefault: Boolean(workspace?.isDefault)
2974
+ }
2975
+ ),
2976
+ /* @__PURE__ */ jsx19(
2977
+ UiSettingsPanel2,
2978
+ {
2979
+ id: "general",
2980
+ icon: /* @__PURE__ */ jsx19(Settings22, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2981
+ title: "General",
2982
+ description: "Keep the workspace name clear enough to scan in menus.",
2983
+ footer: /* @__PURE__ */ jsxs13(Fragment3, { children: [
2984
+ nameChanged ? /* @__PURE__ */ jsx19(
2985
+ Button14,
2986
+ {
2987
+ type: "button",
2988
+ variant: "ghost",
2989
+ size: "sm",
2990
+ onClick: () => {
2991
+ setNameValue(null);
2992
+ setNameError(null);
2993
+ },
2994
+ disabled: renameMutation.isPending,
2995
+ children: "Reset"
2996
+ }
2997
+ ) : null,
2998
+ /* @__PURE__ */ jsx19(
2999
+ Button14,
3000
+ {
3001
+ "data-testid": "save-name",
3002
+ size: "sm",
3003
+ disabled: !nameChanged || renameMutation.isPending || !canEditName,
3004
+ onClick: handleSaveName,
3005
+ children: renameMutation.isPending ? "Saving..." : "Save changes"
3006
+ }
3007
+ )
3008
+ ] }),
3009
+ children: /* @__PURE__ */ jsxs13("div", { className: "space-y-4", children: [
3010
+ nameError && /* @__PURE__ */ jsx19(Notice5, { "data-testid": "name-error", role: "alert", tone: "error", description: nameError }),
3011
+ /* @__PURE__ */ jsxs13("div", { className: "space-y-2", children: [
3012
+ /* @__PURE__ */ jsx19(Label9, { htmlFor: "workspace-name", className: "text-[12px]", children: "Workspace name" }),
3013
+ /* @__PURE__ */ jsx19(
3014
+ Input9,
3015
+ {
3016
+ id: "workspace-name",
3017
+ "data-testid": "workspace-name-input",
3018
+ className: "h-8 text-[13px]",
3019
+ value: displayName,
3020
+ onChange: (e) => setNameValue(e.target.value),
3021
+ disabled: !canEditName,
3022
+ "aria-invalid": nameError ? "true" : "false"
3023
+ }
3024
+ ),
3025
+ /* @__PURE__ */ jsx19(FieldNote, { children: canEditName ? "Editors and owners can rename a workspace." : "Viewers can inspect settings, but cannot rename this workspace." })
3026
+ ] })
3027
+ ] })
3028
+ }
3029
+ ),
3030
+ hasRuntime && /* @__PURE__ */ jsx19(
3031
+ UiSettingsPanel2,
3032
+ {
3033
+ id: "runtime",
3034
+ testId: "runtime-card",
3035
+ icon: /* @__PURE__ */ jsx19(HardDrive, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
3036
+ title: "Runtime",
3037
+ description: "Provisioning status for this workspace.",
3038
+ children: /* @__PURE__ */ jsxs13("div", { className: "space-y-3", children: [
3039
+ /* @__PURE__ */ jsxs13("div", { className: "flex min-h-10 flex-wrap items-center justify-between gap-3 rounded-md border border-border/50 bg-muted/10 px-3 py-2", children: [
3040
+ /* @__PURE__ */ jsx19("span", { className: "text-[13px] font-medium", children: "State" }),
3041
+ /* @__PURE__ */ jsx19(StatusBadge2, { "data-testid": `runtime-state-${runtime.state}`, tone: STATE_TONES[runtime.state] ?? "neutral", children: runtime.state })
3042
+ ] }),
3043
+ runtime.state === "ready" && runtime.volumePath && /* @__PURE__ */ jsxs13(
3044
+ "div",
3045
+ {
3046
+ "data-testid": "volume-path",
3047
+ className: "space-y-1 rounded-md border border-border/50 bg-muted/10 px-3 py-2",
3048
+ children: [
3049
+ /* @__PURE__ */ jsx19("p", { className: "text-[13px] font-medium", children: "Volume" }),
3050
+ /* @__PURE__ */ jsx19("code", { className: "block overflow-x-auto whitespace-nowrap text-[12px] text-muted-foreground", children: runtime.volumePath })
3051
+ ]
3052
+ }
3053
+ ),
3054
+ runtime.state === "error" && runtime.lastError && /* @__PURE__ */ jsxs13(
3055
+ "div",
3056
+ {
3057
+ "data-testid": "runtime-error",
3058
+ role: "alert",
3059
+ className: "flex gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-[13px] leading-5 text-destructive",
3060
+ children: [
3061
+ /* @__PURE__ */ jsx19(AlertCircle, { className: "mt-0.5 h-4 w-4 shrink-0", "aria-hidden": "true" }),
3062
+ runtime.lastError
3063
+ ]
3064
+ }
3065
+ ),
3066
+ runtime.state === "error" && runtime.lastErrorOp === "provision" && /* @__PURE__ */ jsxs13("div", { className: "space-y-3", children: [
3067
+ /* @__PURE__ */ jsxs13(
3068
+ Button14,
3069
+ {
3070
+ "data-testid": "retry-provision",
3071
+ variant: "outline",
3072
+ size: "sm",
3073
+ disabled: retryMutation.isPending,
3074
+ onClick: () => retryMutation.mutate(),
3075
+ children: [
3076
+ /* @__PURE__ */ jsx19(RefreshCw, { className: "h-4 w-4", "aria-hidden": "true" }),
3077
+ retryMutation.isPending ? "Retrying..." : "Retry provisioning"
3078
+ ]
3079
+ }
3080
+ ),
3081
+ retryError && /* @__PURE__ */ jsx19(Notice5, { "data-testid": "retry-error", role: "alert", tone: "error", description: retryError })
3082
+ ] }),
3083
+ runtime.state === "error" && runtime.lastErrorOp === "destroy" && /* @__PURE__ */ jsx19(
3084
+ "p",
3085
+ {
3086
+ "data-testid": "destroy-guidance",
3087
+ className: "rounded-md border border-border/50 bg-muted/10 px-3 py-2 text-[13px] leading-5 text-muted-foreground",
3088
+ children: "Destroy failed. Use the Delete button below to re-issue the delete."
3089
+ }
3090
+ )
3091
+ ] })
3092
+ }
3093
+ ),
3094
+ /* @__PURE__ */ jsx19(
3095
+ UiSettingsPanel2,
3096
+ {
3097
+ id: "danger-zone",
3098
+ testId: "danger-zone",
3099
+ icon: /* @__PURE__ */ jsx19(ShieldAlert2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
3100
+ title: "Danger zone",
3101
+ description: "Permanently delete this workspace and all provisioned data.",
3102
+ danger: true,
3103
+ children: /* @__PURE__ */ jsxs13("div", { className: "space-y-4", children: [
3104
+ deleteError && /* @__PURE__ */ jsx19(Notice5, { "data-testid": "delete-error", role: "alert", tone: "error", description: deleteError }),
3105
+ !canDeleteWorkspace ? /* @__PURE__ */ jsx19("div", { className: "rounded-md border border-border/50 bg-muted/10 px-3 py-2 text-[13px] leading-5 text-muted-foreground", children: "Only workspace owners can delete this workspace." }) : null,
3106
+ /* @__PURE__ */ jsx19(
3107
+ UiSettingsActionRow2,
3108
+ {
3109
+ title: "Delete workspace",
3110
+ description: "Delete the workspace record and re-issue cleanup for provisioned runtime data.",
3111
+ action: /* @__PURE__ */ jsxs13(AlertDialog3, { open: deleteDialogOpen, onOpenChange: setDeleteDialogOpen, children: [
3112
+ /* @__PURE__ */ jsxs13(
3113
+ Button14,
3114
+ {
3115
+ variant: "destructive",
3116
+ size: "sm",
3117
+ "data-testid": "delete-workspace",
3118
+ disabled: !canDeleteWorkspace,
3119
+ onClick: () => {
3120
+ setDeleteDialogOpen(true);
3121
+ setDeleteConfirmName("");
3122
+ },
3123
+ children: [
3124
+ /* @__PURE__ */ jsx19(Trash22, { className: "h-4 w-4", "aria-hidden": "true" }),
3125
+ "Delete workspace"
3126
+ ]
3127
+ }
3128
+ ),
3129
+ /* @__PURE__ */ jsxs13(AlertDialogContent3, { children: [
3130
+ /* @__PURE__ */ jsxs13(AlertDialogHeader3, { children: [
3131
+ /* @__PURE__ */ jsx19(AlertDialogTitle3, { children: "Delete workspace?" }),
3132
+ /* @__PURE__ */ jsxs13(AlertDialogDescription3, { children: [
3133
+ "This action cannot be undone. Type ",
3134
+ /* @__PURE__ */ jsx19("strong", { children: workspace?.name }),
3135
+ " to confirm."
3136
+ ] })
3137
+ ] }),
3138
+ /* @__PURE__ */ jsx19("div", { className: "px-6 pb-2", children: /* @__PURE__ */ jsx19(
3139
+ Input9,
3140
+ {
3141
+ "data-testid": "delete-confirm-input",
3142
+ className: "h-8 text-[13px]",
3143
+ placeholder: workspace?.name ?? "",
3144
+ value: deleteConfirmName,
3145
+ onChange: (e) => setDeleteConfirmName(e.target.value),
3146
+ autoComplete: "off"
3147
+ }
3148
+ ) }),
3149
+ /* @__PURE__ */ jsxs13(AlertDialogFooter3, { children: [
3150
+ /* @__PURE__ */ jsx19(AlertDialogCancel3, { children: "Cancel" }),
3151
+ /* @__PURE__ */ jsx19(
3152
+ Button14,
3153
+ {
3154
+ variant: "destructive",
3155
+ size: "sm",
3156
+ "data-testid": "confirm-delete",
3157
+ disabled: deleteConfirmName !== workspace?.name || deleteMutation.isPending,
3158
+ onClick: handleDelete,
3159
+ children: deleteMutation.isPending ? "Deleting..." : "Delete workspace"
3160
+ }
3161
+ )
3162
+ ] })
3163
+ ] })
3164
+ ] })
3165
+ }
3166
+ )
3167
+ ] })
3168
+ }
3169
+ )
3170
+ ] })
3171
+ ] }) })
3172
+ ] });
3173
+ }
3174
+
3175
+ // src/front/CoreFront.tsx
3176
+ import { Fragment as Fragment4, jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
3177
+ var CSP_NONCE_META_NAME = "boring-csp-nonce";
3178
+ function PlaceholderPage({ name }) {
3179
+ return /* @__PURE__ */ jsxs14("div", { "data-testid": `placeholder-${name}`, children: [
3180
+ name,
3181
+ " (not yet implemented)"
3182
+ ] });
3183
+ }
3184
+ function readCspNonceFromDom() {
3185
+ if (typeof document === "undefined") return void 0;
3186
+ const meta = document.querySelector(`meta[name="${CSP_NONCE_META_NAME}"]`);
3187
+ const value = meta?.getAttribute("content")?.trim();
3188
+ return value ? value : void 0;
3189
+ }
3190
+ function createDefaultQueryClient() {
3191
+ return new QueryClient({
3192
+ defaultOptions: {
3193
+ queries: {
3194
+ staleTime: 6e4,
3195
+ retry: 1
3196
+ }
3197
+ }
3198
+ });
3199
+ }
3200
+ function CoreFront({ children, authPages, cspNonce }) {
3201
+ const queryClient = useMemo6(createDefaultQueryClient, []);
3202
+ const resolvedCspNonce = useMemo6(
3203
+ () => cspNonce ?? readCspNonceFromDom(),
3204
+ [cspNonce]
3205
+ );
3206
+ const SignInPage2 = authPages?.signIn ?? SignInPage;
3207
+ const SignUpPage2 = authPages?.signUp ?? SignUpPage;
3208
+ const ForgotPasswordPage2 = authPages?.forgotPassword ?? ForgotPasswordPage;
3209
+ const ResetPasswordPage2 = authPages?.resetPassword ?? ResetPasswordPage;
3210
+ const VerifyEmailPage2 = authPages?.verifyEmail ?? VerifyEmailPage;
3211
+ const UserSettingsPage2 = authPages?.userSettings ?? UserSettingsPage;
3212
+ return /* @__PURE__ */ jsx20(HelmetProvider, { children: /* @__PURE__ */ jsx20(AppErrorBoundary, { children: /* @__PURE__ */ jsx20(QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsx20(ConfigProvider, { children: /* @__PURE__ */ jsx20(ThemeProvider, { children: /* @__PURE__ */ jsx20(AuthProvider, { queryClient, children: /* @__PURE__ */ jsx20(UserIdentityProvider, { children: /* @__PURE__ */ jsx20(BrowserRouter, { children: /* @__PURE__ */ jsx20(WorkspaceAuthProvider, { children: /* @__PURE__ */ jsxs14(TopBarSlotProvider, { slot: /* @__PURE__ */ jsx20(UserMenu, {}), children: [
3213
+ /* @__PURE__ */ jsx20(Helmet, { children: resolvedCspNonce ? /* @__PURE__ */ jsxs14(Fragment4, { children: [
3214
+ /* @__PURE__ */ jsx20("meta", { name: CSP_NONCE_META_NAME, content: resolvedCspNonce }),
3215
+ /* @__PURE__ */ jsx20(
3216
+ "script",
3217
+ {
3218
+ type: "application/json",
3219
+ nonce: resolvedCspNonce,
3220
+ "data-boring-csp-nonce": "true",
3221
+ children: JSON.stringify({ nonce: resolvedCspNonce })
3222
+ }
3223
+ )
3224
+ ] }) : null }),
3225
+ /* @__PURE__ */ jsx20(AuthGate, { publicPaths: ["/invites"], children: /* @__PURE__ */ jsx20(Suspense, { fallback: null, children: /* @__PURE__ */ jsxs14(Routes, { children: [
3226
+ /* @__PURE__ */ jsx20(Route, { path: routes.signin, element: /* @__PURE__ */ jsx20(SignInPage2, {}) }),
3227
+ /* @__PURE__ */ jsx20(Route, { path: routes.signup, element: /* @__PURE__ */ jsx20(SignUpPage2, {}) }),
3228
+ /* @__PURE__ */ jsx20(Route, { path: routes.forgotPassword, element: /* @__PURE__ */ jsx20(ForgotPasswordPage2, {}) }),
3229
+ /* @__PURE__ */ jsx20(Route, { path: routes.resetPassword, element: /* @__PURE__ */ jsx20(ResetPasswordPage2, {}) }),
3230
+ /* @__PURE__ */ jsx20(Route, { path: routes.verifyEmail, element: /* @__PURE__ */ jsx20(VerifyEmailPage2, {}) }),
3231
+ /* @__PURE__ */ jsx20(Route, { path: routes.callbackGithub, element: /* @__PURE__ */ jsx20(PlaceholderPage, { name: "github-callback" }) }),
3232
+ /* @__PURE__ */ jsx20(Route, { path: routes.me, element: /* @__PURE__ */ jsx20(UserSettingsPage2, {}) }),
3233
+ /* @__PURE__ */ jsx20(Route, { path: routes.workspaceMembers, element: /* @__PURE__ */ jsx20(MembersPage, {}) }),
3234
+ /* @__PURE__ */ jsx20(Route, { path: "/workspace/:id/members", element: /* @__PURE__ */ jsx20(MembersPage, {}) }),
3235
+ /* @__PURE__ */ jsx20(Route, { path: routes.workspaceInvites, element: /* @__PURE__ */ jsx20(InvitesPage, {}) }),
3236
+ /* @__PURE__ */ jsx20(Route, { path: "/workspace/:id/invites", element: /* @__PURE__ */ jsx20(InvitesPage, {}) }),
3237
+ /* @__PURE__ */ jsx20(Route, { path: routes.workspaceSettings, element: /* @__PURE__ */ jsx20(WorkspaceSettingsPage, {}) }),
3238
+ /* @__PURE__ */ jsx20(Route, { path: "/workspace/:id/settings", element: /* @__PURE__ */ jsx20(WorkspaceSettingsPage, {}) }),
3239
+ /* @__PURE__ */ jsx20(Route, { path: routes.inviteAccept, element: /* @__PURE__ */ jsx20(InviteAcceptPage, {}) }),
3240
+ children
3241
+ ] }) }) })
3242
+ ] }) }) }) }) }) }) }) }) }) });
3243
+ }
3244
+
3245
+ // src/front/commands/CoreCommandContributions.tsx
3246
+ import { useMemo as useMemo7, useState as useState19 } from "react";
3247
+ import { useNavigate as useNavigate5 } from "react-router-dom";
3248
+
3249
+ // src/front/workspace/commands.ts
3250
+ function getWorkspaceCommands(workspaceId, navigate) {
3251
+ return [
3252
+ {
3253
+ id: "workspace:settings",
3254
+ label: "Workspace settings",
3255
+ keywords: ["workspace", "settings", "edit", "rename", "delete"],
3256
+ run: () => navigate(`/w/${workspaceId}/settings`)
3257
+ },
3258
+ {
3259
+ id: "workspace:members",
3260
+ label: "Manage members",
3261
+ keywords: ["members", "team", "people", "roles"],
3262
+ run: () => navigate(`/w/${workspaceId}/members`)
3263
+ },
3264
+ {
3265
+ id: "workspace:invites",
3266
+ label: "Invite to workspace",
3267
+ keywords: ["invite", "add", "new member"],
3268
+ run: () => navigate(`/w/${workspaceId}/invites`)
3269
+ }
3270
+ ];
3271
+ }
3272
+
3273
+ // src/front/commands/CoreCommandContributions.tsx
3274
+ var CORE_COMMAND_SOURCE = "core";
3275
+ function toPaletteCommand(command) {
3276
+ return {
3277
+ id: command.id,
3278
+ title: command.label,
3279
+ keywords: command.keywords,
3280
+ run: command.run,
3281
+ pluginId: CORE_COMMAND_SOURCE
3282
+ };
3283
+ }
3284
+ function useCoreCommands() {
3285
+ const navigate = useNavigate5();
3286
+ const signOut = useSignOut();
3287
+ const workspace = useCurrentWorkspace();
3288
+ const [isSigningOut, setIsSigningOut] = useState19(false);
3289
+ return useMemo7(() => {
3290
+ const result = [
3291
+ {
3292
+ id: "user:settings",
3293
+ title: "Account settings",
3294
+ keywords: ["user", "profile", "settings", "account", "me"],
3295
+ pluginId: CORE_COMMAND_SOURCE,
3296
+ run: () => navigate(routes.me)
3297
+ },
3298
+ {
3299
+ id: "auth:sign-out",
3300
+ title: "Sign out",
3301
+ keywords: ["logout", "log out", "auth", "user"],
3302
+ pluginId: CORE_COMMAND_SOURCE,
3303
+ when: () => !isSigningOut,
3304
+ run: () => {
3305
+ if (isSigningOut) return;
3306
+ setIsSigningOut(true);
3307
+ void signOut().finally(() => {
3308
+ setIsSigningOut(false);
3309
+ navigate(routes.signin);
3310
+ });
3311
+ }
3312
+ }
3313
+ ];
3314
+ if (workspace?.id) {
3315
+ result.push(...getWorkspaceCommands(workspace.id, navigate).map(toPaletteCommand));
3316
+ }
3317
+ return result;
3318
+ }, [isSigningOut, navigate, signOut, workspace?.id]);
3319
+ }
3320
+
3321
+ // src/front/sanitize.ts
3322
+ import DOMPurify from "isomorphic-dompurify";
3323
+ function sanitizeMarkdown(input) {
3324
+ return DOMPurify.sanitize(input, {
3325
+ ALLOWED_TAGS: [
3326
+ "p",
3327
+ "br",
3328
+ "strong",
3329
+ "em",
3330
+ "b",
3331
+ "i",
3332
+ "u",
3333
+ "a",
3334
+ "code",
3335
+ "pre",
3336
+ "blockquote",
3337
+ "ul",
3338
+ "ol",
3339
+ "li",
3340
+ "h1",
3341
+ "h2",
3342
+ "h3",
3343
+ "h4",
3344
+ "h5",
3345
+ "h6",
3346
+ "hr",
3347
+ "img",
3348
+ "table",
3349
+ "thead",
3350
+ "tbody",
3351
+ "tr",
3352
+ "th",
3353
+ "td",
3354
+ "span",
3355
+ "div",
3356
+ "sup",
3357
+ "sub",
3358
+ "del",
3359
+ "ins",
3360
+ "details",
3361
+ "summary"
3362
+ ],
3363
+ ALLOWED_ATTR: ["href", "src", "alt", "title", "class", "id", "target", "rel"],
3364
+ ALLOW_DATA_ATTR: false
3365
+ });
3366
+ }
3367
+ function sanitizeToolOutput(input) {
3368
+ return DOMPurify.sanitize(input, {
3369
+ ALLOWED_TAGS: ["span", "code", "pre", "br", "strong", "em"],
3370
+ ALLOWED_ATTR: ["class"],
3371
+ ALLOW_DATA_ATTR: false
3372
+ });
3373
+ }
3374
+
3375
+ // src/front/debounce.ts
3376
+ function debounce(fn, ms) {
3377
+ let timer;
3378
+ const debounced = (...args) => {
3379
+ if (timer) clearTimeout(timer);
3380
+ timer = setTimeout(() => fn(...args), ms);
3381
+ };
3382
+ return debounced;
3383
+ }
3384
+
3385
+ export {
3386
+ AppErrorBoundary,
3387
+ setApiBase,
3388
+ getApiBase,
3389
+ buildApiUrl,
3390
+ getWsBase,
3391
+ buildWsUrl,
3392
+ openWebSocket,
3393
+ apiFetch,
3394
+ apiFetchJson,
3395
+ getHttpErrorDetail,
3396
+ routes,
3397
+ routeHref,
3398
+ ConfigProvider,
3399
+ useConfig,
3400
+ useConfigLoaded,
3401
+ ThemeProvider,
3402
+ useTheme,
3403
+ useKeyboardShortcuts,
3404
+ useViewportBreakpoint,
3405
+ useReducedMotion,
3406
+ useBlobUrl,
3407
+ useCapabilities,
3408
+ useWorkspaceMembers,
3409
+ WorkspaceAuthProvider,
3410
+ useCurrentWorkspace,
3411
+ useWorkspaceRole,
3412
+ getAuthClient,
3413
+ AuthProvider,
3414
+ useSession,
3415
+ useSignIn,
3416
+ useSignUp,
3417
+ useVerifyEmail,
3418
+ useSendVerificationEmail,
3419
+ useChangePassword,
3420
+ useSignOut,
3421
+ UserIdentityProvider,
3422
+ useUser,
3423
+ SignInPage,
3424
+ SignUpPage,
3425
+ ForgotPasswordPage,
3426
+ ResetPasswordPage,
3427
+ VerifyEmailPage,
3428
+ UserSettingsPage,
3429
+ InviteAcceptPage,
3430
+ AuthGate,
3431
+ UserMenu,
3432
+ WorkspaceSwitcher,
3433
+ ThemeToggle,
3434
+ InvitesPage,
3435
+ MembersPage,
3436
+ WorkspaceSettingsPage,
3437
+ CoreFront,
3438
+ getWorkspaceCommands,
3439
+ useCoreCommands,
3440
+ sanitizeMarkdown,
3441
+ sanitizeToolOutput,
3442
+ debounce
3443
+ };