@tangle-network/agent-app 0.60.184 → 0.60.185
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-profiles/d1.js +3 -3
- package/dist/agent-profiles/index.js +3 -3
- package/dist/assistant/index.js +9 -9
- package/dist/channels/index.js +1 -1
- package/dist/chat-react/index.js +4 -4
- package/dist/chat-routes/index.js +4 -4
- package/dist/{chunk-YXLN4PXK.js → chunk-2RCPPOQ4.js} +2 -2
- package/dist/{chunk-YESFGM56.js → chunk-5IS3GCMR.js} +3 -3
- package/dist/{chunk-3RHAF2P5.js → chunk-A3CUVYRZ.js} +3 -3
- package/dist/{chunk-LN5RBVGP.js → chunk-AVP5SNIG.js} +4 -4
- package/dist/{chunk-LN5RBVGP.js.map → chunk-AVP5SNIG.js.map} +1 -1
- package/dist/{chunk-7BYY7QKC.js → chunk-E65YV7VV.js} +5 -5
- package/dist/{chunk-7BYY7QKC.js.map → chunk-E65YV7VV.js.map} +1 -1
- package/dist/chunk-EZFBGW23.js +179 -0
- package/dist/chunk-EZFBGW23.js.map +1 -0
- package/dist/{chunk-UDLA3NGY.js → chunk-GNBBY3KN.js} +3 -3
- package/dist/{chunk-KOTNMTHV.js → chunk-HCFVEJQL.js} +3 -3
- package/dist/{chunk-YGBEGBJY.js → chunk-HEPEGY6Q.js} +2 -2
- package/dist/{chunk-AJZ3EJB5.js → chunk-IJAXOQJU.js} +2 -2
- package/dist/{chunk-EQRVOMMS.js → chunk-KMIDRUUH.js} +2 -2
- package/dist/{chunk-BBHVV37T.js → chunk-MSAGTFN4.js} +4 -4
- package/dist/chunk-OBKVNZPH.js +70 -0
- package/dist/chunk-OBKVNZPH.js.map +1 -0
- package/dist/{chunk-75ZIWV4U.js → chunk-QAF7ZTO3.js} +4 -4
- package/dist/{chunk-TJ4DYKF2.js → chunk-XG6FODYO.js} +3 -2
- package/dist/chunk-XG6FODYO.js.map +1 -0
- package/dist/{chunk-6O2NQ7WD.js → chunk-Y2SOQOJE.js} +3 -3
- package/dist/integrations-react/index.js +1 -1
- package/dist/{mention-editor-ZJAOUDEE.js → mention-editor-NOS5R3AU.js} +2 -2
- package/dist/preset-cloudflare/index.js +2 -2
- package/dist/sandbox/index.js +4 -4
- package/dist/settings-react/confirm-action.d.ts +51 -0
- package/dist/settings-react/delete-workspace-section.d.ts +31 -0
- package/dist/settings-react/export-section.d.ts +26 -0
- package/dist/settings-react/index.d.ts +20 -0
- package/dist/settings-react/index.js +1438 -0
- package/dist/settings-react/index.js.map +1 -0
- package/dist/settings-react/members-section.d.ts +66 -0
- package/dist/settings-react/save-status.d.ts +24 -0
- package/dist/settings-react/setting-outcome.d.ts +33 -0
- package/dist/settings-react/settings-layout.d.ts +71 -0
- package/dist/settings-react/settings-save-bar.d.ts +26 -0
- package/dist/settings-react/timezone-picker.d.ts +17 -0
- package/dist/settings-react/use-setting-field.d.ts +70 -0
- package/dist/stories/settings/settings-backend.d.ts +66 -0
- package/dist/stories/settings/settings-demo.d.ts +16 -0
- package/dist/stories/settings/team-panels-demo.d.ts +21 -0
- package/dist/studio-react/index.js +2 -2
- package/dist/teams-react/components/InviteForm.d.ts +2 -2
- package/dist/teams-react/components/MembersPanel.d.ts +9 -5
- package/dist/teams-react/components/RoleSelect.d.ts +10 -8
- package/dist/teams-react/components/invitation-detail.d.ts +8 -0
- package/dist/teams-react/components/member-access.d.ts +17 -0
- package/dist/teams-react/contracts.d.ts +2 -0
- package/dist/teams-react/index.js +119 -242
- package/dist/teams-react/index.js.map +1 -1
- package/dist/theme/tokens.css +43 -2
- package/dist/tools/index.js +4 -4
- package/dist/web-react/agent-profile-editor.js +4 -4
- package/dist/web-react/async/index.js +1 -1
- package/dist/web-react/controls.d.ts +4 -5
- package/dist/web-react/index.js +12 -12
- package/dist/work-product/index.js +2 -2
- package/dist/workspace-export/react.d.ts +26 -0
- package/dist/workspace-export/react.js +6 -169
- package/dist/workspace-export/react.js.map +1 -1
- package/dist/workspace-react/index.js +3 -3
- package/package.json +9 -4
- package/dist/chunk-TJ4DYKF2.js.map +0 -1
- /package/dist/{chunk-YXLN4PXK.js.map → chunk-2RCPPOQ4.js.map} +0 -0
- /package/dist/{chunk-YESFGM56.js.map → chunk-5IS3GCMR.js.map} +0 -0
- /package/dist/{chunk-3RHAF2P5.js.map → chunk-A3CUVYRZ.js.map} +0 -0
- /package/dist/{chunk-UDLA3NGY.js.map → chunk-GNBBY3KN.js.map} +0 -0
- /package/dist/{chunk-KOTNMTHV.js.map → chunk-HCFVEJQL.js.map} +0 -0
- /package/dist/{chunk-YGBEGBJY.js.map → chunk-HEPEGY6Q.js.map} +0 -0
- /package/dist/{chunk-AJZ3EJB5.js.map → chunk-IJAXOQJU.js.map} +0 -0
- /package/dist/{chunk-EQRVOMMS.js.map → chunk-KMIDRUUH.js.map} +0 -0
- /package/dist/{chunk-BBHVV37T.js.map → chunk-MSAGTFN4.js.map} +0 -0
- /package/dist/{chunk-75ZIWV4U.js.map → chunk-QAF7ZTO3.js.map} +0 -0
- /package/dist/{chunk-6O2NQ7WD.js.map → chunk-Y2SOQOJE.js.map} +0 -0
- /package/dist/{mention-editor-ZJAOUDEE.js.map → mention-editor-NOS5R3AU.js.map} +0 -0
|
@@ -0,0 +1,1438 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RoleSelect,
|
|
3
|
+
canManageMember,
|
|
4
|
+
invitationDetail,
|
|
5
|
+
isCurrentMember,
|
|
6
|
+
memberRoleLabel
|
|
7
|
+
} from "../chunk-OBKVNZPH.js";
|
|
8
|
+
import {
|
|
9
|
+
INVITATION_EMAIL_HINT,
|
|
10
|
+
INVITATION_EXPIRY_DAYS,
|
|
11
|
+
isInvitationEmail
|
|
12
|
+
} from "../chunk-KBJZMR4E.js";
|
|
13
|
+
import {
|
|
14
|
+
hasWorkspaceRole
|
|
15
|
+
} from "../chunk-6XIAPIW6.js";
|
|
16
|
+
import {
|
|
17
|
+
OVERLAY_SHADOW,
|
|
18
|
+
POPOVER_OPTION_FOCUS,
|
|
19
|
+
PopoverSurface,
|
|
20
|
+
fieldPickerTriggerClass,
|
|
21
|
+
useBrowserLayoutEffect,
|
|
22
|
+
usePopover
|
|
23
|
+
} from "../chunk-XG6FODYO.js";
|
|
24
|
+
import "../chunk-RASGKEAL.js";
|
|
25
|
+
import {
|
|
26
|
+
confirmWrite,
|
|
27
|
+
rejectWrite,
|
|
28
|
+
useConfirmedMutation
|
|
29
|
+
} from "../chunk-AVP5SNIG.js";
|
|
30
|
+
import {
|
|
31
|
+
useWorkspaceExport
|
|
32
|
+
} from "../chunk-EZFBGW23.js";
|
|
33
|
+
import "../chunk-AMUXRAUO.js";
|
|
34
|
+
|
|
35
|
+
// src/settings-react/settings-layout.tsx
|
|
36
|
+
import { Button, PageHeader, PageShell } from "@tangle-network/ui/primitives";
|
|
37
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
38
|
+
import {
|
|
39
|
+
useCallback,
|
|
40
|
+
useEffect,
|
|
41
|
+
useRef,
|
|
42
|
+
useState
|
|
43
|
+
} from "react";
|
|
44
|
+
|
|
45
|
+
// src/settings-react/save-status.tsx
|
|
46
|
+
import { Check, LoaderCircle } from "lucide-react";
|
|
47
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
48
|
+
var PENDING_DELAY_MS = 200;
|
|
49
|
+
var SAVED_VISIBLE_MS = 2e3;
|
|
50
|
+
function SavingStatus({ label = "Saving\u2026" }) {
|
|
51
|
+
return /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 text-sm text-muted-foreground", "data-setting-status": "saving", children: [
|
|
52
|
+
/* @__PURE__ */ jsx(LoaderCircle, { "aria-hidden": "true", className: "size-4 animate-spin motion-reduce:animate-none" }),
|
|
53
|
+
label
|
|
54
|
+
] });
|
|
55
|
+
}
|
|
56
|
+
function SavedStatus({ label = "Saved" }) {
|
|
57
|
+
return /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 text-sm text-[var(--surface-success-text)]", "data-setting-status": "saved", children: [
|
|
58
|
+
/* @__PURE__ */ jsx(Check, { "aria-hidden": "true", className: "size-4" }),
|
|
59
|
+
label
|
|
60
|
+
] });
|
|
61
|
+
}
|
|
62
|
+
function UnsavedStatus({ label = "Unsaved" }) {
|
|
63
|
+
return /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", "data-setting-status": "unsaved", children: label });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/settings-react/settings-layout.tsx
|
|
67
|
+
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
68
|
+
function PlainLink({ href, children, ...rest }) {
|
|
69
|
+
return /* @__PURE__ */ jsx2("a", { href, ...rest, children });
|
|
70
|
+
}
|
|
71
|
+
function isPlainClick(event) {
|
|
72
|
+
return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey && !event.defaultPrevented;
|
|
73
|
+
}
|
|
74
|
+
function useOptimisticSection(activeId) {
|
|
75
|
+
const [pendingId, setPendingId] = useState(null);
|
|
76
|
+
useEffect(() => setPendingId(null), [activeId]);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const clear = () => setPendingId(null);
|
|
79
|
+
window.addEventListener("popstate", clear);
|
|
80
|
+
return () => window.removeEventListener("popstate", clear);
|
|
81
|
+
}, []);
|
|
82
|
+
return { currentId: pendingId ?? activeId, pending: pendingId !== null && pendingId !== activeId, setPendingId };
|
|
83
|
+
}
|
|
84
|
+
var CUE_PANEL = 44;
|
|
85
|
+
var STRIP_GAP = 4;
|
|
86
|
+
function useStripOverflow(listRef) {
|
|
87
|
+
const [overflow, setOverflow] = useState({ start: false, end: false });
|
|
88
|
+
const strip = useCallback(() => listRef.current?.parentElement ?? null, [listRef]);
|
|
89
|
+
useBrowserLayoutEffect(() => {
|
|
90
|
+
const nav = strip();
|
|
91
|
+
if (!nav) return;
|
|
92
|
+
const measure = () => {
|
|
93
|
+
const scrolls = getComputedStyle(nav).overflowX !== "visible" && nav.scrollWidth > nav.clientWidth + 1;
|
|
94
|
+
const start = scrolls && nav.scrollLeft > 1;
|
|
95
|
+
const end = scrolls && nav.scrollLeft + nav.clientWidth < nav.scrollWidth - 1;
|
|
96
|
+
setOverflow((prev) => prev.start === start && prev.end === end ? prev : { start, end });
|
|
97
|
+
};
|
|
98
|
+
measure();
|
|
99
|
+
nav.addEventListener("scroll", measure, { passive: true });
|
|
100
|
+
const resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measure);
|
|
101
|
+
resize?.observe(nav);
|
|
102
|
+
if (listRef.current) resize?.observe(listRef.current);
|
|
103
|
+
return () => {
|
|
104
|
+
nav.removeEventListener("scroll", measure);
|
|
105
|
+
resize?.disconnect();
|
|
106
|
+
};
|
|
107
|
+
}, [listRef, strip]);
|
|
108
|
+
const page = (direction) => {
|
|
109
|
+
const nav = strip();
|
|
110
|
+
if (!nav) return;
|
|
111
|
+
const reduce = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
112
|
+
nav.scrollBy({ left: direction * nav.clientWidth * 0.75, behavior: reduce ? "auto" : "smooth" });
|
|
113
|
+
};
|
|
114
|
+
return { overflow, page };
|
|
115
|
+
}
|
|
116
|
+
function ScrollCue({ side, onPage }) {
|
|
117
|
+
const start = side === "start";
|
|
118
|
+
const Icon = start ? ChevronLeft : ChevronRight;
|
|
119
|
+
const gutter = start ? "calc(-1 * var(--tangle-settings-gutter, 0px))" : "var(--tangle-settings-gutter, 0px)";
|
|
120
|
+
return /* @__PURE__ */ jsx2(
|
|
121
|
+
Button,
|
|
122
|
+
{
|
|
123
|
+
type: "button",
|
|
124
|
+
variant: "bare",
|
|
125
|
+
tabIndex: -1,
|
|
126
|
+
"aria-hidden": "true",
|
|
127
|
+
"data-scroll-cue": side,
|
|
128
|
+
onClick: onPage,
|
|
129
|
+
className: `sticky z-[1] mb-px flex shrink-0 items-center self-stretch bg-background ${start ? "left-0 justify-start" : "right-0 justify-end"}`,
|
|
130
|
+
style: {
|
|
131
|
+
width: CUE_PANEL,
|
|
132
|
+
[start ? "marginRight" : "marginLeft"]: -(CUE_PANEL + STRIP_GAP),
|
|
133
|
+
boxShadow: `${gutter} 0 0 0 hsl(var(--background))`
|
|
134
|
+
},
|
|
135
|
+
children: /* @__PURE__ */ jsx2("span", { className: "flex size-8 items-center justify-center rounded-full border border-border bg-card text-muted-foreground shadow-sm", children: /* @__PURE__ */ jsx2(Icon, { "aria-hidden": "true", className: "size-4" }) })
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
function NavLinks({ sections, currentId, LinkComponent = PlainLink, onIntent, onPick }) {
|
|
140
|
+
const listRef = useRef(null);
|
|
141
|
+
const warmed = useRef(/* @__PURE__ */ new Set());
|
|
142
|
+
const { overflow, page } = useStripOverflow(listRef);
|
|
143
|
+
const intent = (section) => {
|
|
144
|
+
if (!onIntent || warmed.current.has(section.id)) return;
|
|
145
|
+
warmed.current.add(section.id);
|
|
146
|
+
onIntent(section);
|
|
147
|
+
};
|
|
148
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
149
|
+
overflow.start && /* @__PURE__ */ jsx2(ScrollCue, { side: "start", onPage: () => page(-1) }),
|
|
150
|
+
/* @__PURE__ */ jsx2("ul", { ref: listRef, role: "list", children: sections.filter((section) => !section.hidden).map((section) => {
|
|
151
|
+
const Icon = section.icon;
|
|
152
|
+
const current = section.id === currentId;
|
|
153
|
+
return /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */ jsxs2(
|
|
154
|
+
LinkComponent,
|
|
155
|
+
{
|
|
156
|
+
href: section.href,
|
|
157
|
+
"aria-current": current ? "page" : void 0,
|
|
158
|
+
"data-settings-section": section.id,
|
|
159
|
+
onPointerEnter: () => intent(section),
|
|
160
|
+
onFocus: () => intent(section),
|
|
161
|
+
onClick: (event) => {
|
|
162
|
+
if (isPlainClick(event)) onPick(section.id);
|
|
163
|
+
},
|
|
164
|
+
children: [
|
|
165
|
+
Icon && /* @__PURE__ */ jsx2(Icon, { "aria-hidden": "true" }),
|
|
166
|
+
/* @__PURE__ */ jsx2("span", { children: section.label }),
|
|
167
|
+
section.badge != null && /* @__PURE__ */ jsx2("span", { "data-count": "", children: section.badge })
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
) }, section.id);
|
|
171
|
+
}) }),
|
|
172
|
+
overflow.end && /* @__PURE__ */ jsx2(ScrollCue, { side: "end", onPage: () => page(1) })
|
|
173
|
+
] });
|
|
174
|
+
}
|
|
175
|
+
function SettingsNav(props) {
|
|
176
|
+
const { currentId, setPendingId } = useOptimisticSection(props.activeId);
|
|
177
|
+
return /* @__PURE__ */ jsx2(NavLinks, { ...props, currentId, onPick: setPendingId });
|
|
178
|
+
}
|
|
179
|
+
function readHash() {
|
|
180
|
+
const raw = window.location.hash.slice(1);
|
|
181
|
+
try {
|
|
182
|
+
return decodeURIComponent(raw);
|
|
183
|
+
} catch {
|
|
184
|
+
return raw;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function useSectionScroll(activeId, rootRef) {
|
|
188
|
+
const first = useRef(true);
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
const reveal = () => {
|
|
191
|
+
const hash = readHash();
|
|
192
|
+
const target = hash ? document.getElementById(hash) : null;
|
|
193
|
+
if (!target) return false;
|
|
194
|
+
target.scrollIntoView({ block: "start" });
|
|
195
|
+
const heading = document.getElementById(`${hash}-title`) ?? target.querySelector("h2, h3, label") ?? target;
|
|
196
|
+
if (!heading.hasAttribute("tabindex") && !/^(A|BUTTON|INPUT|SELECT|TEXTAREA)$/.test(heading.tagName)) heading.setAttribute("tabindex", "-1");
|
|
197
|
+
heading.focus({ preventScroll: true });
|
|
198
|
+
return true;
|
|
199
|
+
};
|
|
200
|
+
if (!reveal() && !first.current) rootRef.current?.scrollIntoView({ block: "start" });
|
|
201
|
+
first.current = false;
|
|
202
|
+
const onHash = () => void reveal();
|
|
203
|
+
window.addEventListener("hashchange", onHash);
|
|
204
|
+
return () => window.removeEventListener("hashchange", onHash);
|
|
205
|
+
}, [activeId, rootRef]);
|
|
206
|
+
}
|
|
207
|
+
function useDelayed(pending) {
|
|
208
|
+
const [shown, setShown] = useState(false);
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
if (!pending) {
|
|
211
|
+
setShown(false);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const timer = setTimeout(() => setShown(true), PENDING_DELAY_MS);
|
|
215
|
+
return () => clearTimeout(timer);
|
|
216
|
+
}, [pending]);
|
|
217
|
+
return shown;
|
|
218
|
+
}
|
|
219
|
+
function SettingsLayout({
|
|
220
|
+
title,
|
|
221
|
+
description,
|
|
222
|
+
meta,
|
|
223
|
+
sections,
|
|
224
|
+
activeId,
|
|
225
|
+
LinkComponent,
|
|
226
|
+
onIntent,
|
|
227
|
+
navigationLabel = "Settings sections",
|
|
228
|
+
className,
|
|
229
|
+
children
|
|
230
|
+
}) {
|
|
231
|
+
const rootRef = useRef(null);
|
|
232
|
+
const { currentId, pending, setPendingId } = useOptimisticSection(activeId);
|
|
233
|
+
const showProgress = useDelayed(pending);
|
|
234
|
+
useSectionScroll(activeId, rootRef);
|
|
235
|
+
const visible = sections.filter((section) => !section.hidden);
|
|
236
|
+
const header = /* @__PURE__ */ jsx2(
|
|
237
|
+
PageHeader,
|
|
238
|
+
{
|
|
239
|
+
level: 2,
|
|
240
|
+
titleAs: "h1",
|
|
241
|
+
title,
|
|
242
|
+
description: description != null || meta != null ? /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
243
|
+
description,
|
|
244
|
+
meta != null && /* @__PURE__ */ jsx2("span", { className: "mt-1 block", children: meta })
|
|
245
|
+
] }) : void 0
|
|
246
|
+
}
|
|
247
|
+
);
|
|
248
|
+
return /* @__PURE__ */ jsxs2("div", { ref: rootRef, className: "relative min-w-0", "data-settings-layout": true, children: [
|
|
249
|
+
showProgress && /* @__PURE__ */ jsx2("div", { role: "progressbar", "aria-label": "Opening section", className: "pointer-events-none absolute inset-x-0 top-0 z-[4] h-0.5 overflow-hidden", children: /* @__PURE__ */ jsx2("div", { className: "h-full w-1/3 animate-pulse rounded-full bg-[var(--accent-text)] motion-reduce:animate-none" }) }),
|
|
250
|
+
/* @__PURE__ */ jsx2(
|
|
251
|
+
PageShell,
|
|
252
|
+
{
|
|
253
|
+
measure: "settings",
|
|
254
|
+
className,
|
|
255
|
+
navigationLabel,
|
|
256
|
+
header,
|
|
257
|
+
navigation: visible.length > 1 ? /* @__PURE__ */ jsx2(NavLinks, { sections: visible, activeId, currentId, LinkComponent, onIntent, onPick: setPendingId }) : void 0,
|
|
258
|
+
article: children
|
|
259
|
+
}
|
|
260
|
+
)
|
|
261
|
+
] });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/settings-react/use-setting-field.tsx
|
|
265
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
|
|
266
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
267
|
+
function isPlain(value) {
|
|
268
|
+
const prototype = Object.getPrototypeOf(value);
|
|
269
|
+
return Array.isArray(value) || prototype === Object.prototype || prototype === null;
|
|
270
|
+
}
|
|
271
|
+
function samePlainValue(a, b) {
|
|
272
|
+
if (Object.is(a, b)) return true;
|
|
273
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
274
|
+
if (Array.isArray(a) !== Array.isArray(b) || !isPlain(a) || !isPlain(b)) return false;
|
|
275
|
+
const left = a;
|
|
276
|
+
const right = b;
|
|
277
|
+
const keys = Object.keys(left);
|
|
278
|
+
return keys.length === Object.keys(right).length && keys.every((key) => Object.prototype.hasOwnProperty.call(right, key) && samePlainValue(left[key], right[key]));
|
|
279
|
+
}
|
|
280
|
+
function useSettingField({
|
|
281
|
+
saved,
|
|
282
|
+
save,
|
|
283
|
+
commitOn = "change",
|
|
284
|
+
debounceMs = 0,
|
|
285
|
+
validate,
|
|
286
|
+
isEqual = samePlainValue,
|
|
287
|
+
onSaved
|
|
288
|
+
}) {
|
|
289
|
+
const [confirmed, setConfirmed] = useState2(saved);
|
|
290
|
+
const [draft, setDraftState] = useState2(saved);
|
|
291
|
+
const [failedValue, setFailedValue] = useState2(null);
|
|
292
|
+
const [showSaving, setShowSaving] = useState2(false);
|
|
293
|
+
const [showSaved, setShowSaved] = useState2(false);
|
|
294
|
+
const draftRef = useRef2(draft);
|
|
295
|
+
draftRef.current = draft;
|
|
296
|
+
const confirmedRef = useRef2(confirmed);
|
|
297
|
+
confirmedRef.current = confirmed;
|
|
298
|
+
const options = useRef2({ validate, isEqual, onSaved });
|
|
299
|
+
options.current = { validate, isEqual, onSaved };
|
|
300
|
+
const debounce = useRef2(null);
|
|
301
|
+
const savedTimer = useRef2(null);
|
|
302
|
+
const inFlight = useRef2(false);
|
|
303
|
+
const waiting = useRef2(null);
|
|
304
|
+
const mutation = useConfirmedMutation({
|
|
305
|
+
mutate: (value, context) => save(value, context),
|
|
306
|
+
onSucceeded: (value) => {
|
|
307
|
+
confirmedRef.current = value;
|
|
308
|
+
setConfirmed(value);
|
|
309
|
+
setFailedValue(null);
|
|
310
|
+
setShowSaved(true);
|
|
311
|
+
if (savedTimer.current) clearTimeout(savedTimer.current);
|
|
312
|
+
savedTimer.current = setTimeout(() => setShowSaved(false), SAVED_VISIBLE_MS);
|
|
313
|
+
options.current.onSaved?.(value);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
const pending = mutation.state.status === "pending";
|
|
317
|
+
const editing = !isEqual(draft, confirmed) || pending;
|
|
318
|
+
useEffect2(() => {
|
|
319
|
+
if (options.current.isEqual(saved, confirmedRef.current)) return;
|
|
320
|
+
setConfirmed(saved);
|
|
321
|
+
if (!editing) setDraftState(saved);
|
|
322
|
+
}, [saved]);
|
|
323
|
+
useEffect2(() => {
|
|
324
|
+
if (!pending) {
|
|
325
|
+
setShowSaving(false);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const timer = setTimeout(() => setShowSaving(true), PENDING_DELAY_MS);
|
|
329
|
+
return () => clearTimeout(timer);
|
|
330
|
+
}, [pending]);
|
|
331
|
+
const send = useCallback2(async (first) => {
|
|
332
|
+
inFlight.current = true;
|
|
333
|
+
let value = first;
|
|
334
|
+
for (; ; ) {
|
|
335
|
+
setShowSaved(false);
|
|
336
|
+
const outcome = await mutation.run(value);
|
|
337
|
+
const queued = waiting.current;
|
|
338
|
+
waiting.current = null;
|
|
339
|
+
const next = queued && !(outcome.succeeded && options.current.isEqual(queued.value, value)) ? queued : null;
|
|
340
|
+
if (outcome.succeeded && !next && options.current.isEqual(draftRef.current, value)) {
|
|
341
|
+
draftRef.current = outcome.value;
|
|
342
|
+
setDraftState(outcome.value);
|
|
343
|
+
}
|
|
344
|
+
if (next && !options.current.isEqual(next.value, confirmedRef.current)) {
|
|
345
|
+
value = next.value;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
inFlight.current = false;
|
|
349
|
+
if (!outcome.succeeded && !next) setFailedValue({ value });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
}, [mutation.run]);
|
|
353
|
+
const commit = useCallback2((next) => {
|
|
354
|
+
if (debounce.current) {
|
|
355
|
+
clearTimeout(debounce.current);
|
|
356
|
+
debounce.current = null;
|
|
357
|
+
}
|
|
358
|
+
const value = next === void 0 ? draftRef.current : next;
|
|
359
|
+
if (options.current.validate?.(value)) return;
|
|
360
|
+
if (inFlight.current) {
|
|
361
|
+
waiting.current = { value };
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (options.current.isEqual(value, confirmedRef.current)) {
|
|
365
|
+
setFailedValue(null);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
void send(value);
|
|
369
|
+
}, [send]);
|
|
370
|
+
const commitRef = useRef2(commit);
|
|
371
|
+
commitRef.current = commit;
|
|
372
|
+
useEffect2(() => () => {
|
|
373
|
+
if (savedTimer.current) clearTimeout(savedTimer.current);
|
|
374
|
+
if (!debounce.current) return;
|
|
375
|
+
clearTimeout(debounce.current);
|
|
376
|
+
debounce.current = null;
|
|
377
|
+
commitRef.current(draftRef.current);
|
|
378
|
+
}, []);
|
|
379
|
+
const setDraft = useCallback2((next) => {
|
|
380
|
+
draftRef.current = next;
|
|
381
|
+
setDraftState(next);
|
|
382
|
+
setFailedValue(null);
|
|
383
|
+
if (commitOn !== "change") return;
|
|
384
|
+
if (debounce.current) clearTimeout(debounce.current);
|
|
385
|
+
if (debounceMs > 0) debounce.current = setTimeout(() => commit(next), debounceMs);
|
|
386
|
+
else commit(next);
|
|
387
|
+
}, [commit, commitOn, debounceMs]);
|
|
388
|
+
const revert = useCallback2(() => {
|
|
389
|
+
if (debounce.current) clearTimeout(debounce.current);
|
|
390
|
+
debounce.current = null;
|
|
391
|
+
waiting.current = null;
|
|
392
|
+
mutation.reset();
|
|
393
|
+
setFailedValue(null);
|
|
394
|
+
setDraftState(confirmedRef.current);
|
|
395
|
+
}, [mutation.reset]);
|
|
396
|
+
const validation = validate && !isEqual(draft, confirmed) ? validate(draft) : null;
|
|
397
|
+
const failed = failedValue !== null && isEqual(failedValue.value, draft) && mutation.state.status === "failed";
|
|
398
|
+
const state = validation ? "invalid" : pending ? "saving" : failed ? "failed" : !isEqual(draft, confirmed) ? "unsaved" : showSaved ? "saved" : "idle";
|
|
399
|
+
const error = validation ?? (failed && mutation.state.status === "failed" ? mutation.state.message : null);
|
|
400
|
+
const retry = useCallback2(() => commit(draftRef.current), [commit]);
|
|
401
|
+
const rowProps = useMemo(() => ({
|
|
402
|
+
status: state === "saving" ? showSaving ? /* @__PURE__ */ jsx3(SavingStatus, {}) : void 0 : state === "saved" ? /* @__PURE__ */ jsx3(SavedStatus, {}) : state === "unsaved" && commitOn === "blur" ? /* @__PURE__ */ jsx3(UnsavedStatus, {}) : void 0,
|
|
403
|
+
error: error ?? void 0,
|
|
404
|
+
onRetry: state === "failed" ? retry : void 0
|
|
405
|
+
}), [state, showSaving, commitOn, error, retry]);
|
|
406
|
+
return { draft, setDraft, commit, revert, state, error, rowProps };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// src/settings-react/setting-outcome.ts
|
|
410
|
+
var FALLBACK = {
|
|
411
|
+
forbidden: "You don't have permission to change this.",
|
|
412
|
+
invalid: "That value can't be saved. Check it and try again.",
|
|
413
|
+
plan_limit: "Your plan doesn't include this.",
|
|
414
|
+
rate_limited: "You've reached the limit for now. Try again shortly.",
|
|
415
|
+
not_runnable: "This can't run right now.",
|
|
416
|
+
upstream: "A connected service didn't respond. Nothing changed; try again.",
|
|
417
|
+
network: "Couldn't reach the server. Check your connection, then retry."
|
|
418
|
+
};
|
|
419
|
+
function codeForStatus(status) {
|
|
420
|
+
if (status === 401 || status === 403) return "forbidden";
|
|
421
|
+
if (status === 402) return "plan_limit";
|
|
422
|
+
if (status === 429) return "rate_limited";
|
|
423
|
+
if (status === 400 || status === 409 || status === 422) return "invalid";
|
|
424
|
+
return "upstream";
|
|
425
|
+
}
|
|
426
|
+
function retryWording(seconds) {
|
|
427
|
+
if (seconds < 90) return `Try again in ${Math.max(1, Math.round(seconds))} seconds.`;
|
|
428
|
+
return `Try again in ${Math.round(seconds / 60)} minutes.`;
|
|
429
|
+
}
|
|
430
|
+
function readFailure(body, status) {
|
|
431
|
+
const raw = typeof body === "object" && body !== null ? body.error : void 0;
|
|
432
|
+
const error = typeof raw === "string" ? { message: raw } : typeof raw === "object" && raw !== null ? raw : {};
|
|
433
|
+
const code = typeof error.code === "string" && error.code ? error.code : codeForStatus(status);
|
|
434
|
+
const retryAfterSeconds = typeof error.retryAfterSeconds === "number" ? error.retryAfterSeconds : void 0;
|
|
435
|
+
const given = typeof error.message === "string" && error.message.trim() ? error.message.trim() : null;
|
|
436
|
+
let message = given ?? FALLBACK[code] ?? FALLBACK.upstream;
|
|
437
|
+
if (retryAfterSeconds !== void 0 && code === "rate_limited" && !given) message = `You've reached the limit for now. ${retryWording(retryAfterSeconds)}`;
|
|
438
|
+
return {
|
|
439
|
+
code,
|
|
440
|
+
message,
|
|
441
|
+
...typeof error.field === "string" ? { field: error.field } : {},
|
|
442
|
+
...retryAfterSeconds !== void 0 ? { retryAfterSeconds } : {}
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
async function confirmSettingResponse(request, parse) {
|
|
446
|
+
let response;
|
|
447
|
+
try {
|
|
448
|
+
response = await request;
|
|
449
|
+
} catch (error) {
|
|
450
|
+
return rejectWrite(FALLBACK.network, { code: "network", message: FALLBACK.network, cause: error });
|
|
451
|
+
}
|
|
452
|
+
const text = await response.text().catch(() => "");
|
|
453
|
+
let body;
|
|
454
|
+
try {
|
|
455
|
+
body = text ? JSON.parse(text) : void 0;
|
|
456
|
+
} catch {
|
|
457
|
+
body = void 0;
|
|
458
|
+
}
|
|
459
|
+
if (!response.ok) {
|
|
460
|
+
const failure = readFailure(body, response.status);
|
|
461
|
+
return rejectWrite(failure.message, failure);
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
return confirmWrite(parse ? parse(body) : body);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
return rejectWrite("The server answered, but not with the saved value. Reload to see what was kept.", error);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function settingFailure(outcome) {
|
|
470
|
+
const error = outcome.error;
|
|
471
|
+
return typeof error === "object" && error !== null && typeof error.code === "string" ? { ...error, code: error.code, message: outcome.message } : { code: "upstream", message: outcome.message };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/settings-react/confirm-action.tsx
|
|
475
|
+
import {
|
|
476
|
+
Alert,
|
|
477
|
+
AlertDialog,
|
|
478
|
+
AlertDialogCancel,
|
|
479
|
+
AlertDialogContent,
|
|
480
|
+
AlertDialogDescription,
|
|
481
|
+
AlertDialogFooter,
|
|
482
|
+
AlertDialogHeader,
|
|
483
|
+
AlertDialogTitle,
|
|
484
|
+
AlertDialogTrigger,
|
|
485
|
+
Button as Button2,
|
|
486
|
+
Input,
|
|
487
|
+
Label,
|
|
488
|
+
Sheet,
|
|
489
|
+
SheetContent,
|
|
490
|
+
SheetDescription,
|
|
491
|
+
SheetFooter,
|
|
492
|
+
SheetHeader,
|
|
493
|
+
SheetTitle,
|
|
494
|
+
SheetTrigger
|
|
495
|
+
} from "@tangle-network/ui/primitives";
|
|
496
|
+
import { CircleAlert } from "lucide-react";
|
|
497
|
+
import { useCallback as useCallback3, useEffect as useEffect3, useId, useRef as useRef3, useState as useState3, useSyncExternalStore } from "react";
|
|
498
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
499
|
+
var NARROW = "(max-width: 40rem)";
|
|
500
|
+
function subscribeNarrow(onChange) {
|
|
501
|
+
if (typeof window === "undefined" || !window.matchMedia) return () => void 0;
|
|
502
|
+
const query = window.matchMedia(NARROW);
|
|
503
|
+
query.addEventListener("change", onChange);
|
|
504
|
+
return () => query.removeEventListener("change", onChange);
|
|
505
|
+
}
|
|
506
|
+
var MENU_EXIT_LIMIT_MS = 1e3;
|
|
507
|
+
function useOpenAfterMenus(open) {
|
|
508
|
+
const [clear, setClear] = useState3(false);
|
|
509
|
+
useEffect3(() => {
|
|
510
|
+
if (!open) {
|
|
511
|
+
setClear(false);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const closing = Array.from(document.querySelectorAll('[role="menu"]'));
|
|
515
|
+
const deadline = performance.now() + MENU_EXIT_LIMIT_MS;
|
|
516
|
+
let frame = 0;
|
|
517
|
+
const wait = () => {
|
|
518
|
+
if (closing.some((menu) => menu.isConnected) && performance.now() < deadline) frame = requestAnimationFrame(wait);
|
|
519
|
+
else setClear(true);
|
|
520
|
+
};
|
|
521
|
+
wait();
|
|
522
|
+
return () => cancelAnimationFrame(frame);
|
|
523
|
+
}, [open]);
|
|
524
|
+
return open && clear;
|
|
525
|
+
}
|
|
526
|
+
function useNarrowViewport() {
|
|
527
|
+
return useSyncExternalStore(
|
|
528
|
+
subscribeNarrow,
|
|
529
|
+
() => typeof window !== "undefined" && Boolean(window.matchMedia?.(NARROW).matches),
|
|
530
|
+
() => false
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
function ConfirmAction({
|
|
534
|
+
trigger,
|
|
535
|
+
open: openProp,
|
|
536
|
+
onOpenChange,
|
|
537
|
+
title,
|
|
538
|
+
consequence,
|
|
539
|
+
changes,
|
|
540
|
+
notice,
|
|
541
|
+
confirmLabel,
|
|
542
|
+
cancelLabel = "Cancel",
|
|
543
|
+
tone = "default",
|
|
544
|
+
children,
|
|
545
|
+
canConfirm = true,
|
|
546
|
+
requireText,
|
|
547
|
+
onConfirm,
|
|
548
|
+
onConfirmed,
|
|
549
|
+
refusalAction
|
|
550
|
+
}) {
|
|
551
|
+
const [innerOpen, setInnerOpen] = useState3(false);
|
|
552
|
+
const open = openProp ?? innerOpen;
|
|
553
|
+
const shown = useOpenAfterMenus(open);
|
|
554
|
+
const [typed, setTyped] = useState3("");
|
|
555
|
+
const narrow = useNarrowViewport();
|
|
556
|
+
const bodyRef = useRef3(null);
|
|
557
|
+
const cancelRef = useRef3(null);
|
|
558
|
+
const inputId = useId();
|
|
559
|
+
const errorId = useId();
|
|
560
|
+
const mutation = useConfirmedMutation({ mutate: () => onConfirm(), concurrency: "reject" });
|
|
561
|
+
const pending = mutation.state.status === "pending";
|
|
562
|
+
const failure = mutation.state.status === "failed" ? settingFailure(mutation.state) : null;
|
|
563
|
+
const failureAction = failure && refusalAction?.(failure);
|
|
564
|
+
const { reset } = mutation;
|
|
565
|
+
const setOpen = useCallback3((next) => {
|
|
566
|
+
if (!next && pending) return;
|
|
567
|
+
setInnerOpen(next);
|
|
568
|
+
onOpenChange?.(next);
|
|
569
|
+
}, [onOpenChange, pending]);
|
|
570
|
+
useEffect3(() => {
|
|
571
|
+
if (!open) return;
|
|
572
|
+
setTyped("");
|
|
573
|
+
reset();
|
|
574
|
+
}, [open, reset]);
|
|
575
|
+
const ready = canConfirm && (requireText === void 0 || typed.trim() === requireText);
|
|
576
|
+
const confirm = async () => {
|
|
577
|
+
if (!ready || pending) return;
|
|
578
|
+
const outcome = await mutation.run();
|
|
579
|
+
if (!outcome.succeeded) return;
|
|
580
|
+
setInnerOpen(false);
|
|
581
|
+
onOpenChange?.(false);
|
|
582
|
+
onConfirmed?.(outcome.value);
|
|
583
|
+
};
|
|
584
|
+
const focusFirst = (event) => {
|
|
585
|
+
event.preventDefault();
|
|
586
|
+
setTimeout(() => {
|
|
587
|
+
const target = bodyRef.current?.querySelector('input, textarea, [role="combobox"]') ?? cancelRef.current;
|
|
588
|
+
target?.focus();
|
|
589
|
+
}, 0);
|
|
590
|
+
};
|
|
591
|
+
const hasBody = Boolean(changes?.length) || notice != null || children != null || requireText !== void 0 || failure != null;
|
|
592
|
+
const body = hasBody && /* @__PURE__ */ jsxs3("div", { ref: bodyRef, className: "grid min-w-0 gap-4 text-sm", children: [
|
|
593
|
+
changes && changes.length > 0 && /* @__PURE__ */ jsx4("dl", { className: "grid min-w-0 grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-2 rounded-lg border border-border px-4 py-3", children: changes.map((change) => /* @__PURE__ */ jsxs3("div", { className: "contents", children: [
|
|
594
|
+
/* @__PURE__ */ jsx4("dt", { className: "text-muted-foreground", children: change.label }),
|
|
595
|
+
/* @__PURE__ */ jsxs3("dd", { className: "min-w-0 [overflow-wrap:anywhere]", children: [
|
|
596
|
+
/* @__PURE__ */ jsx4("span", { className: "text-muted-foreground", children: change.from }),
|
|
597
|
+
/* @__PURE__ */ jsx4("span", { "aria-hidden": "true", className: "px-1.5 text-muted-foreground", children: "\u2192" }),
|
|
598
|
+
/* @__PURE__ */ jsx4("span", { className: "sr-only", children: " to " }),
|
|
599
|
+
/* @__PURE__ */ jsx4("span", { className: "font-medium text-foreground", children: change.to })
|
|
600
|
+
] })
|
|
601
|
+
] }, change.label)) }),
|
|
602
|
+
notice != null && /* @__PURE__ */ jsx4(Alert, { tone: "info", children: notice }),
|
|
603
|
+
children,
|
|
604
|
+
requireText !== void 0 && /* @__PURE__ */ jsxs3("div", { className: "grid gap-2", children: [
|
|
605
|
+
/* @__PURE__ */ jsxs3(Label, { htmlFor: inputId, children: [
|
|
606
|
+
"Type ",
|
|
607
|
+
/* @__PURE__ */ jsx4("span", { className: "font-semibold [overflow-wrap:anywhere]", children: requireText }),
|
|
608
|
+
" to confirm"
|
|
609
|
+
] }),
|
|
610
|
+
/* @__PURE__ */ jsx4(
|
|
611
|
+
Input,
|
|
612
|
+
{
|
|
613
|
+
id: inputId,
|
|
614
|
+
value: typed,
|
|
615
|
+
autoComplete: "off",
|
|
616
|
+
spellCheck: false,
|
|
617
|
+
"aria-describedby": failure ? errorId : void 0,
|
|
618
|
+
onChange: (event) => setTyped(event.target.value),
|
|
619
|
+
onKeyDown: (event) => {
|
|
620
|
+
if (event.key === "Enter") {
|
|
621
|
+
event.preventDefault();
|
|
622
|
+
void confirm();
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
)
|
|
627
|
+
] }),
|
|
628
|
+
failure && /* @__PURE__ */ jsxs3("p", { id: errorId, role: "alert", className: "flex items-start gap-1.5 text-[var(--surface-danger-text)] [overflow-wrap:anywhere]", children: [
|
|
629
|
+
/* @__PURE__ */ jsx4(CircleAlert, { "aria-hidden": "true", className: "mt-0.5 size-4 shrink-0" }),
|
|
630
|
+
/* @__PURE__ */ jsxs3("span", { className: "min-w-0", children: [
|
|
631
|
+
failure.message,
|
|
632
|
+
failureAction != null && /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
633
|
+
" ",
|
|
634
|
+
failureAction
|
|
635
|
+
] })
|
|
636
|
+
] })
|
|
637
|
+
] })
|
|
638
|
+
] });
|
|
639
|
+
const confirmButton = /* @__PURE__ */ jsx4(
|
|
640
|
+
Button2,
|
|
641
|
+
{
|
|
642
|
+
type: "button",
|
|
643
|
+
variant: tone === "danger" ? "destructive" : "default",
|
|
644
|
+
loading: pending,
|
|
645
|
+
disabled: !ready || pending,
|
|
646
|
+
onClick: () => void confirm(),
|
|
647
|
+
"data-confirm-action": true,
|
|
648
|
+
children: confirmLabel
|
|
649
|
+
}
|
|
650
|
+
);
|
|
651
|
+
if (narrow) {
|
|
652
|
+
return /* @__PURE__ */ jsxs3(Sheet, { open: shown, onOpenChange: setOpen, children: [
|
|
653
|
+
trigger && /* @__PURE__ */ jsx4(SheetTrigger, { asChild: true, children: trigger }),
|
|
654
|
+
/* @__PURE__ */ jsxs3(SheetContent, { side: "bottom", hideCloseButton: true, onOpenAutoFocus: focusFirst, className: "max-h-[90dvh] overflow-y-auto", children: [
|
|
655
|
+
/* @__PURE__ */ jsxs3(SheetHeader, { className: "pb-0", children: [
|
|
656
|
+
/* @__PURE__ */ jsx4(SheetTitle, { children: title }),
|
|
657
|
+
/* @__PURE__ */ jsx4(SheetDescription, { children: consequence })
|
|
658
|
+
] }),
|
|
659
|
+
body && /* @__PURE__ */ jsx4("div", { className: "px-4", children: body }),
|
|
660
|
+
/* @__PURE__ */ jsxs3(SheetFooter, { className: "pt-2 pb-[max(1rem,env(safe-area-inset-bottom))]", children: [
|
|
661
|
+
confirmButton,
|
|
662
|
+
/* @__PURE__ */ jsx4(Button2, { ref: cancelRef, type: "button", variant: "outline", disabled: pending, onClick: () => setOpen(false), children: cancelLabel })
|
|
663
|
+
] })
|
|
664
|
+
] })
|
|
665
|
+
] });
|
|
666
|
+
}
|
|
667
|
+
return /* @__PURE__ */ jsxs3(AlertDialog, { open: shown, onOpenChange: setOpen, children: [
|
|
668
|
+
trigger && /* @__PURE__ */ jsx4(AlertDialogTrigger, { asChild: true, children: trigger }),
|
|
669
|
+
/* @__PURE__ */ jsxs3(AlertDialogContent, { onOpenAutoFocus: focusFirst, children: [
|
|
670
|
+
/* @__PURE__ */ jsxs3(AlertDialogHeader, { children: [
|
|
671
|
+
/* @__PURE__ */ jsx4(AlertDialogTitle, { children: title }),
|
|
672
|
+
/* @__PURE__ */ jsx4(AlertDialogDescription, { children: consequence })
|
|
673
|
+
] }),
|
|
674
|
+
body,
|
|
675
|
+
/* @__PURE__ */ jsxs3(AlertDialogFooter, { children: [
|
|
676
|
+
/* @__PURE__ */ jsx4(AlertDialogCancel, { ref: cancelRef, disabled: pending, children: cancelLabel }),
|
|
677
|
+
confirmButton
|
|
678
|
+
] })
|
|
679
|
+
] })
|
|
680
|
+
] });
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// src/settings-react/settings-save-bar.tsx
|
|
684
|
+
import { Button as Button3 } from "@tangle-network/ui/primitives";
|
|
685
|
+
import { CircleAlert as CircleAlert2, Lock } from "lucide-react";
|
|
686
|
+
import { useEffect as useEffect4, useState as useState4 } from "react";
|
|
687
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
688
|
+
var STICKY_FOOTER = "@max-[34rem]/settings-section:[:has(>&)]:sticky @max-[34rem]/settings-section:[:has(>&)]:bottom-0 @max-[34rem]/settings-section:[:has(>&)]:z-10";
|
|
689
|
+
function SettingsSaveBar({
|
|
690
|
+
changes,
|
|
691
|
+
summary,
|
|
692
|
+
onDiscard,
|
|
693
|
+
onSave,
|
|
694
|
+
state,
|
|
695
|
+
error,
|
|
696
|
+
saveLabel = "Save changes",
|
|
697
|
+
disabledReason
|
|
698
|
+
}) {
|
|
699
|
+
const saving = state === "saving";
|
|
700
|
+
const dirty = changes > 0;
|
|
701
|
+
const [showSaved, setShowSaved] = useState4(state === "saved");
|
|
702
|
+
useEffect4(() => {
|
|
703
|
+
setShowSaved(state === "saved");
|
|
704
|
+
if (state !== "saved") return;
|
|
705
|
+
const timer = setTimeout(() => setShowSaved(false), SAVED_VISIBLE_MS);
|
|
706
|
+
return () => clearTimeout(timer);
|
|
707
|
+
}, [state]);
|
|
708
|
+
const message = disabledReason != null ? /* @__PURE__ */ jsxs4("span", { className: "flex items-start gap-1.5 text-muted-foreground", children: [
|
|
709
|
+
/* @__PURE__ */ jsx5(Lock, { "aria-hidden": "true", className: "mt-0.5 size-4 shrink-0" }),
|
|
710
|
+
/* @__PURE__ */ jsx5("span", { className: "min-w-0", children: disabledReason })
|
|
711
|
+
] }) : state === "failed" && error != null ? /* @__PURE__ */ jsxs4("span", { role: "alert", className: "flex items-start gap-1.5 text-[var(--surface-danger-text)]", children: [
|
|
712
|
+
/* @__PURE__ */ jsx5(CircleAlert2, { "aria-hidden": "true", className: "mt-0.5 size-4 shrink-0" }),
|
|
713
|
+
/* @__PURE__ */ jsx5("span", { className: "min-w-0 [overflow-wrap:anywhere]", children: error })
|
|
714
|
+
] }) : summary ?? (dirty ? /* @__PURE__ */ jsx5("span", { className: "text-muted-foreground", children: changes === 1 ? "1 unsaved change" : `${changes} unsaved changes` }) : showSaved ? /* @__PURE__ */ jsx5(SavedStatus, {}) : /* @__PURE__ */ jsx5("span", { className: "text-muted-foreground", children: "No unsaved changes" }));
|
|
715
|
+
const sticky = dirty && disabledReason == null;
|
|
716
|
+
return /* @__PURE__ */ jsxs4(
|
|
717
|
+
"div",
|
|
718
|
+
{
|
|
719
|
+
className: `flex min-w-0 flex-1 flex-wrap items-center justify-between gap-x-4 gap-y-2${sticky ? ` ${STICKY_FOOTER}` : ""}`,
|
|
720
|
+
"data-settings-save-bar": true,
|
|
721
|
+
"data-dirty": sticky || void 0,
|
|
722
|
+
children: [
|
|
723
|
+
/* @__PURE__ */ jsx5("div", { "aria-live": "polite", className: "min-w-0 text-sm", children: message }),
|
|
724
|
+
disabledReason == null && /* @__PURE__ */ jsxs4("div", { className: "ml-auto flex shrink-0 gap-2", children: [
|
|
725
|
+
dirty && /* @__PURE__ */ jsx5(Button3, { type: "button", variant: "ghost", disabled: saving, onClick: onDiscard, children: "Discard" }),
|
|
726
|
+
/* @__PURE__ */ jsx5(Button3, { type: "button", variant: dirty ? "default" : "outline", disabled: !dirty || saving, loading: saving, onClick: onSave, children: saveLabel })
|
|
727
|
+
] })
|
|
728
|
+
]
|
|
729
|
+
}
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// src/settings-react/members-section.tsx
|
|
734
|
+
import {
|
|
735
|
+
Avatar,
|
|
736
|
+
AvatarFallback,
|
|
737
|
+
Button as Button4,
|
|
738
|
+
Collapsible,
|
|
739
|
+
CollapsibleContent,
|
|
740
|
+
CollapsibleTrigger,
|
|
741
|
+
Input as Input2,
|
|
742
|
+
Label as Label2,
|
|
743
|
+
SettingsList,
|
|
744
|
+
SettingsListItem,
|
|
745
|
+
SettingsRow,
|
|
746
|
+
SettingsSection
|
|
747
|
+
} from "@tangle-network/ui/primitives";
|
|
748
|
+
import { ChevronDown } from "lucide-react";
|
|
749
|
+
import { Fragment as Fragment3, useEffect as useEffect5, useId as useId2, useState as useState5 } from "react";
|
|
750
|
+
import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
751
|
+
function breakable(address) {
|
|
752
|
+
const parts = address.split(/(?<=[@.\-_])/);
|
|
753
|
+
return parts.map((part, index) => /* @__PURE__ */ jsxs5(Fragment3, { children: [
|
|
754
|
+
part,
|
|
755
|
+
index < parts.length - 1 && /* @__PURE__ */ jsx6("wbr", {})
|
|
756
|
+
] }, index));
|
|
757
|
+
}
|
|
758
|
+
function displayName(member) {
|
|
759
|
+
return member.name ?? member.email ?? "Unknown";
|
|
760
|
+
}
|
|
761
|
+
function initials(member) {
|
|
762
|
+
const name = displayName(member);
|
|
763
|
+
const parts = name.split(/[\s@._-]+/).filter(Boolean);
|
|
764
|
+
return ((parts[0]?.[0] ?? "?") + (member.name ? parts[1]?.[0] ?? "" : "")).toUpperCase();
|
|
765
|
+
}
|
|
766
|
+
var MEMBER_COLUMNS = { status: "18rem", menu: "36px" };
|
|
767
|
+
function useMemberRole(member, onChangeRole) {
|
|
768
|
+
const field = useSettingField({
|
|
769
|
+
saved: member.role,
|
|
770
|
+
save: async (role) => {
|
|
771
|
+
const outcome = await onChangeRole({ memberId: member.id, role });
|
|
772
|
+
return outcome.succeeded ? confirmWrite(role) : outcome;
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
return { field, select: /* @__PURE__ */ jsx6(RoleSelect, { id: `role-${member.id}`, value: field.draft, ariaLabel: `Role for ${displayName(member)}`, onChange: field.setDraft }) };
|
|
776
|
+
}
|
|
777
|
+
function MemberItem({ member, editable, self, noun, onChangeRole, onRemove, extraMenu }) {
|
|
778
|
+
const [removing, setRemoving] = useState5(false);
|
|
779
|
+
const { field, select } = useMemberRole(member, onChangeRole);
|
|
780
|
+
const name = displayName(member);
|
|
781
|
+
const pending = member.acceptedAt == null;
|
|
782
|
+
const email = member.name && member.email ? member.email : null;
|
|
783
|
+
const status = field.rowProps.status;
|
|
784
|
+
return /* @__PURE__ */ jsxs5(Fragment4, { children: [
|
|
785
|
+
/* @__PURE__ */ jsx6(
|
|
786
|
+
SettingsListItem,
|
|
787
|
+
{
|
|
788
|
+
title: /* @__PURE__ */ jsxs5("span", { className: "flex min-w-0 items-center gap-3", children: [
|
|
789
|
+
/* @__PURE__ */ jsx6(Avatar, { size: "sm", children: /* @__PURE__ */ jsx6(AvatarFallback, { children: initials(member) }) }),
|
|
790
|
+
/* @__PURE__ */ jsxs5("span", { className: "min-w-0", children: [
|
|
791
|
+
/* @__PURE__ */ jsxs5("span", { className: "block", children: [
|
|
792
|
+
name,
|
|
793
|
+
self && /* @__PURE__ */ jsx6("span", { className: "font-normal text-muted-foreground", children: " (you)" })
|
|
794
|
+
] }),
|
|
795
|
+
(email || pending || status) && /* @__PURE__ */ jsxs5("span", { className: "flex flex-wrap items-center gap-x-2 font-normal text-muted-foreground", children: [
|
|
796
|
+
/* @__PURE__ */ jsxs5("span", { children: [
|
|
797
|
+
email && breakable(email),
|
|
798
|
+
email && pending && " \xB7 ",
|
|
799
|
+
pending && "Hasn\u2019t joined yet"
|
|
800
|
+
] }),
|
|
801
|
+
status
|
|
802
|
+
] })
|
|
803
|
+
] })
|
|
804
|
+
] }),
|
|
805
|
+
status: editable ? select : /* @__PURE__ */ jsx6("span", { className: "text-foreground", children: memberRoleLabel(member) }),
|
|
806
|
+
error: field.rowProps.error,
|
|
807
|
+
menu: editable ? [...extraMenu, { label: `Remove from ${noun}\u2026`, tone: "danger", onSelect: () => setRemoving(true) }] : void 0
|
|
808
|
+
}
|
|
809
|
+
),
|
|
810
|
+
editable && /* @__PURE__ */ jsx6(
|
|
811
|
+
ConfirmAction,
|
|
812
|
+
{
|
|
813
|
+
open: removing,
|
|
814
|
+
onOpenChange: setRemoving,
|
|
815
|
+
title: `Remove ${name}?`,
|
|
816
|
+
consequence: `${name} loses access to this ${noun} at once. Their chats and files stay.`,
|
|
817
|
+
confirmLabel: "Remove",
|
|
818
|
+
tone: "danger",
|
|
819
|
+
onConfirm: () => onRemove({ memberId: member.id })
|
|
820
|
+
}
|
|
821
|
+
)
|
|
822
|
+
] });
|
|
823
|
+
}
|
|
824
|
+
function plansAction(href, Link) {
|
|
825
|
+
if (!href) return void 0;
|
|
826
|
+
return (failure) => failure.code === "plan_limit" ? /* @__PURE__ */ jsx6(Link, { href, className: "font-medium text-[var(--accent-text)] underline-offset-4 hover:underline", children: "View plans" }) : null;
|
|
827
|
+
}
|
|
828
|
+
function InviteDialog({ onInvite, plansHref, LinkComponent, open, onOpenChange }) {
|
|
829
|
+
const id = useId2();
|
|
830
|
+
const [email, setEmail] = useState5("");
|
|
831
|
+
const [role, setRole] = useState5("editor");
|
|
832
|
+
const [checked, setChecked] = useState5(false);
|
|
833
|
+
useEffect5(() => {
|
|
834
|
+
if (!open) return;
|
|
835
|
+
setEmail("");
|
|
836
|
+
setRole("editor");
|
|
837
|
+
setChecked(false);
|
|
838
|
+
}, [open]);
|
|
839
|
+
const valid = isInvitationEmail(email);
|
|
840
|
+
const showHint = checked && email.trim() !== "" && !valid;
|
|
841
|
+
return /* @__PURE__ */ jsxs5(
|
|
842
|
+
ConfirmAction,
|
|
843
|
+
{
|
|
844
|
+
open,
|
|
845
|
+
onOpenChange,
|
|
846
|
+
title: "Invite people",
|
|
847
|
+
consequence: `They get an email with a link to join. The link works for ${INVITATION_EXPIRY_DAYS} days.`,
|
|
848
|
+
confirmLabel: "Send invite",
|
|
849
|
+
canConfirm: valid,
|
|
850
|
+
onConfirm: () => onInvite({ email: email.trim(), role }),
|
|
851
|
+
refusalAction: plansAction(plansHref, LinkComponent),
|
|
852
|
+
children: [
|
|
853
|
+
/* @__PURE__ */ jsxs5("div", { className: "grid gap-2", children: [
|
|
854
|
+
/* @__PURE__ */ jsx6(Label2, { htmlFor: `${id}-email`, children: "Email" }),
|
|
855
|
+
/* @__PURE__ */ jsx6(
|
|
856
|
+
Input2,
|
|
857
|
+
{
|
|
858
|
+
id: `${id}-email`,
|
|
859
|
+
type: "email",
|
|
860
|
+
inputMode: "email",
|
|
861
|
+
autoComplete: "off",
|
|
862
|
+
value: email,
|
|
863
|
+
variant: showHint ? "error" : void 0,
|
|
864
|
+
"aria-describedby": showHint ? `${id}-hint` : void 0,
|
|
865
|
+
onChange: (event) => setEmail(event.target.value),
|
|
866
|
+
onBlur: () => setChecked(true)
|
|
867
|
+
}
|
|
868
|
+
),
|
|
869
|
+
showHint && /* @__PURE__ */ jsx6("p", { id: `${id}-hint`, role: "alert", className: "text-sm text-[var(--surface-danger-text)]", children: INVITATION_EMAIL_HINT })
|
|
870
|
+
] }),
|
|
871
|
+
/* @__PURE__ */ jsxs5("div", { className: "grid gap-2", children: [
|
|
872
|
+
/* @__PURE__ */ jsx6(Label2, { htmlFor: `${id}-role`, children: "Role" }),
|
|
873
|
+
/* @__PURE__ */ jsx6(RoleSelect, { id: `${id}-role`, value: role, ariaLabel: "Role", onChange: setRole })
|
|
874
|
+
] })
|
|
875
|
+
]
|
|
876
|
+
}
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
function MembersSection({
|
|
880
|
+
id = "members",
|
|
881
|
+
title = "Members",
|
|
882
|
+
description,
|
|
883
|
+
members,
|
|
884
|
+
currentRole,
|
|
885
|
+
currentUserId,
|
|
886
|
+
noun = "project",
|
|
887
|
+
onChangeRole,
|
|
888
|
+
onRemove,
|
|
889
|
+
onInvite,
|
|
890
|
+
plansHref,
|
|
891
|
+
LinkComponent = PlainLink,
|
|
892
|
+
menuItems
|
|
893
|
+
}) {
|
|
894
|
+
const [inviting, setInviting] = useState5(false);
|
|
895
|
+
const canManage = hasWorkspaceRole(currentRole, "admin");
|
|
896
|
+
return /* @__PURE__ */ jsxs5(
|
|
897
|
+
SettingsSection,
|
|
898
|
+
{
|
|
899
|
+
id,
|
|
900
|
+
title,
|
|
901
|
+
description,
|
|
902
|
+
actions: canManage && onInvite ? /* @__PURE__ */ jsx6(Button4, { type: "button", onClick: () => setInviting(true), children: "Invite" }) : void 0,
|
|
903
|
+
children: [
|
|
904
|
+
members.length === 0 ? /* @__PURE__ */ jsx6(SettingsRow, { label: "No members yet", description: "Invite someone to work on this with you.", layout: "compact" }) : /* @__PURE__ */ jsx6(SettingsList, { "aria-label": title, columns: MEMBER_COLUMNS, children: members.map((member) => {
|
|
905
|
+
const editable = canManageMember(member, currentRole, currentUserId);
|
|
906
|
+
return /* @__PURE__ */ jsx6(
|
|
907
|
+
MemberItem,
|
|
908
|
+
{
|
|
909
|
+
member,
|
|
910
|
+
editable,
|
|
911
|
+
self: isCurrentMember(member, currentUserId),
|
|
912
|
+
noun,
|
|
913
|
+
onChangeRole,
|
|
914
|
+
onRemove,
|
|
915
|
+
extraMenu: editable && menuItems ? menuItems(member) : []
|
|
916
|
+
},
|
|
917
|
+
member.id
|
|
918
|
+
);
|
|
919
|
+
}) }),
|
|
920
|
+
onInvite && /* @__PURE__ */ jsx6(InviteDialog, { onInvite, plansHref, LinkComponent, open: inviting, onOpenChange: setInviting })
|
|
921
|
+
]
|
|
922
|
+
}
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
var INVITATION_COLUMNS = { action: "8rem", menu: "36px" };
|
|
926
|
+
function useFlash() {
|
|
927
|
+
const [message, setMessage] = useState5(null);
|
|
928
|
+
useEffect5(() => {
|
|
929
|
+
if (!message) return;
|
|
930
|
+
const timer = setTimeout(() => setMessage(null), SAVED_VISIBLE_MS);
|
|
931
|
+
return () => clearTimeout(timer);
|
|
932
|
+
}, [message]);
|
|
933
|
+
return [message, setMessage];
|
|
934
|
+
}
|
|
935
|
+
function PendingInvitation({ invitation, canManage, onResend, onRevoke, onCopy }) {
|
|
936
|
+
const [revoking, setRevoking] = useState5(false);
|
|
937
|
+
const [note, flash] = useFlash();
|
|
938
|
+
const [error, setError] = useState5(null);
|
|
939
|
+
const copy = async () => {
|
|
940
|
+
try {
|
|
941
|
+
if (onCopy) await onCopy({ inviteUrl: invitation.inviteUrl });
|
|
942
|
+
else await navigator.clipboard.writeText(invitation.inviteUrl);
|
|
943
|
+
setError(null);
|
|
944
|
+
flash("Link copied");
|
|
945
|
+
} catch {
|
|
946
|
+
setError("Couldn't copy the link. Select it from the invitation email instead.");
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
const resend = async () => {
|
|
950
|
+
const outcome = await onResend({ invitationId: invitation.id });
|
|
951
|
+
if (outcome.succeeded) {
|
|
952
|
+
setError(null);
|
|
953
|
+
flash("Sent again");
|
|
954
|
+
} else setError(outcome.message);
|
|
955
|
+
};
|
|
956
|
+
const undelivered = invitation.emailStatus === "failed" ? "The email wasn\u2019t delivered. Copy the link and send it yourself." : null;
|
|
957
|
+
return /* @__PURE__ */ jsxs5(Fragment4, { children: [
|
|
958
|
+
/* @__PURE__ */ jsx6(
|
|
959
|
+
SettingsListItem,
|
|
960
|
+
{
|
|
961
|
+
title: breakable(invitation.email),
|
|
962
|
+
meta: /* @__PURE__ */ jsxs5("span", { className: "inline-flex flex-wrap items-center gap-x-2", children: [
|
|
963
|
+
invitationDetail(invitation),
|
|
964
|
+
note && /* @__PURE__ */ jsx6("span", { className: "text-[var(--surface-success-text)]", children: note })
|
|
965
|
+
] }),
|
|
966
|
+
error: error ?? undelivered ?? void 0,
|
|
967
|
+
action: canManage ? { label: "Copy link", onSelect: () => void copy() } : void 0,
|
|
968
|
+
menu: canManage ? [
|
|
969
|
+
{ label: "Resend email", onSelect: () => void resend() },
|
|
970
|
+
{ label: "Revoke\u2026", tone: "danger", onSelect: () => setRevoking(true) }
|
|
971
|
+
] : void 0
|
|
972
|
+
}
|
|
973
|
+
),
|
|
974
|
+
canManage && /* @__PURE__ */ jsx6(
|
|
975
|
+
ConfirmAction,
|
|
976
|
+
{
|
|
977
|
+
open: revoking,
|
|
978
|
+
onOpenChange: setRevoking,
|
|
979
|
+
title: `Revoke the invitation to ${invitation.email}?`,
|
|
980
|
+
consequence: "The link stops working at once. You can invite them again later.",
|
|
981
|
+
confirmLabel: "Revoke",
|
|
982
|
+
tone: "danger",
|
|
983
|
+
onConfirm: () => onRevoke({ invitationId: invitation.id })
|
|
984
|
+
}
|
|
985
|
+
)
|
|
986
|
+
] });
|
|
987
|
+
}
|
|
988
|
+
function InvitationsSection({
|
|
989
|
+
id = "invitations",
|
|
990
|
+
title = "Invitations",
|
|
991
|
+
invitations,
|
|
992
|
+
currentRole,
|
|
993
|
+
onResend,
|
|
994
|
+
onRevoke,
|
|
995
|
+
onCopy
|
|
996
|
+
}) {
|
|
997
|
+
const [showPast, setShowPast] = useState5(false);
|
|
998
|
+
const canManage = hasWorkspaceRole(currentRole, "admin");
|
|
999
|
+
const pending = invitations.filter((invitation) => invitation.status === "pending");
|
|
1000
|
+
const past = invitations.filter((invitation) => invitation.status !== "pending");
|
|
1001
|
+
return /* @__PURE__ */ jsxs5(SettingsSection, { id, title, children: [
|
|
1002
|
+
pending.length === 0 ? /* @__PURE__ */ jsx6(SettingsRow, { label: "No pending invitations", layout: "compact" }) : /* @__PURE__ */ jsx6(SettingsList, { "aria-label": "Pending invitations", columns: INVITATION_COLUMNS, children: pending.map((invitation) => /* @__PURE__ */ jsx6(PendingInvitation, { invitation, canManage, onResend, onRevoke, onCopy }, invitation.id)) }),
|
|
1003
|
+
past.length > 0 && /* @__PURE__ */ jsxs5(Collapsible, { open: showPast, onOpenChange: setShowPast, children: [
|
|
1004
|
+
/* @__PURE__ */ jsx6("div", { className: "border-t border-[color:var(--border-subtle)] px-5 py-3 @max-[34rem]/settings-section:px-4", children: /* @__PURE__ */ jsx6(CollapsibleTrigger, { asChild: true, children: /* @__PURE__ */ jsxs5(Button4, { type: "button", variant: "ghost", className: "-ml-4", children: [
|
|
1005
|
+
showPast ? "Hide past invitations" : `Show ${past.length} past ${past.length === 1 ? "invitation" : "invitations"}`,
|
|
1006
|
+
/* @__PURE__ */ jsx6(ChevronDown, { "aria-hidden": "true", className: showPast ? "rotate-180" : void 0 })
|
|
1007
|
+
] }) }) }),
|
|
1008
|
+
/* @__PURE__ */ jsx6(CollapsibleContent, { children: /* @__PURE__ */ jsx6(SettingsList, { "aria-label": "Past invitations", columns: {}, children: past.map((invitation) => /* @__PURE__ */ jsx6(SettingsListItem, { title: breakable(invitation.email), meta: invitationDetail(invitation) }, invitation.id)) }) })
|
|
1009
|
+
] })
|
|
1010
|
+
] });
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// src/settings-react/export-section.tsx
|
|
1014
|
+
import {
|
|
1015
|
+
Button as Button5,
|
|
1016
|
+
Collapsible as Collapsible2,
|
|
1017
|
+
CollapsibleContent as CollapsibleContent2,
|
|
1018
|
+
CollapsibleTrigger as CollapsibleTrigger2,
|
|
1019
|
+
Progress,
|
|
1020
|
+
SettingsRow as SettingsRow2,
|
|
1021
|
+
SettingsSection as SettingsSection2
|
|
1022
|
+
} from "@tangle-network/ui/primitives";
|
|
1023
|
+
import { ChevronDown as ChevronDown2 } from "lucide-react";
|
|
1024
|
+
import { useState as useState6 } from "react";
|
|
1025
|
+
import { Fragment as Fragment5, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1026
|
+
var BYTE_UNITS = ["B", "KB", "MB", "GB", "TB"];
|
|
1027
|
+
function formatBytes(bytes) {
|
|
1028
|
+
let value = bytes;
|
|
1029
|
+
let unit = 0;
|
|
1030
|
+
while (value >= 1024 && unit < BYTE_UNITS.length - 1) {
|
|
1031
|
+
value /= 1024;
|
|
1032
|
+
unit += 1;
|
|
1033
|
+
}
|
|
1034
|
+
return `${value < 10 && unit ? value.toFixed(1) : Math.round(value)} ${BYTE_UNITS[unit]}`;
|
|
1035
|
+
}
|
|
1036
|
+
function unitState(unit) {
|
|
1037
|
+
if (unit.status === "pending") return "Waiting";
|
|
1038
|
+
if (unit.status === "empty") return "Nothing to export";
|
|
1039
|
+
if (unit.status === "failed") return "Failed";
|
|
1040
|
+
if (unit.rows !== void 0) return `${unit.rows} rows`;
|
|
1041
|
+
if (unit.files !== void 0) return `${unit.files} files \xB7 ${formatBytes(unit.bytes)}`;
|
|
1042
|
+
return formatBytes(unit.bytes);
|
|
1043
|
+
}
|
|
1044
|
+
function progressLine(latest) {
|
|
1045
|
+
return [
|
|
1046
|
+
`${latest.unitsDone} of ${latest.unitsTotal} sources`,
|
|
1047
|
+
latest.unitsFailed ? `${latest.unitsFailed} failed` : null,
|
|
1048
|
+
formatBytes(latest.bytes),
|
|
1049
|
+
latest.status === "running" && latest.current ? `now ${latest.current}` : null
|
|
1050
|
+
].filter(Boolean).join(" \xB7 ");
|
|
1051
|
+
}
|
|
1052
|
+
function LatestExport({ latest, continuesInBackground }) {
|
|
1053
|
+
const [open, setOpen] = useState6(false);
|
|
1054
|
+
const percent = latest.unitsTotal ? Math.round(latest.unitsDone / latest.unitsTotal * 100) : 0;
|
|
1055
|
+
const label = latest.status === "running" ? "Preparing your export" : latest.status === "complete" ? "Your export is ready" : "The export failed";
|
|
1056
|
+
return /* @__PURE__ */ jsx7(
|
|
1057
|
+
SettingsRow2,
|
|
1058
|
+
{
|
|
1059
|
+
label,
|
|
1060
|
+
layout: "stacked",
|
|
1061
|
+
description: latest.status === "failed" ? void 0 : progressLine(latest),
|
|
1062
|
+
error: latest.status === "failed" ? `${latest.error ?? "A source could not be read."} Start a new export to try again.` : void 0,
|
|
1063
|
+
children: /* @__PURE__ */ jsxs6("div", { className: "grid w-full min-w-0 gap-3 text-sm", children: [
|
|
1064
|
+
latest.status === "running" && /* @__PURE__ */ jsxs6(Fragment5, { children: [
|
|
1065
|
+
/* @__PURE__ */ jsx7(Progress, { value: percent, "aria-label": "Export progress" }),
|
|
1066
|
+
continuesInBackground && /* @__PURE__ */ jsx7("p", { className: "text-muted-foreground", children: "You can close this page. The export keeps going, and the download is here when you return." })
|
|
1067
|
+
] }),
|
|
1068
|
+
latest.downloadUrl && /* @__PURE__ */ jsxs6("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2", children: [
|
|
1069
|
+
/* @__PURE__ */ jsx7(Button5, { asChild: true, variant: "outline", children: /* @__PURE__ */ jsx7("a", { href: latest.downloadUrl, download: true, children: "Download zip" }) }),
|
|
1070
|
+
/* @__PURE__ */ jsxs6("span", { className: "text-muted-foreground", children: [
|
|
1071
|
+
"Link expires ",
|
|
1072
|
+
latest.downloadExpiresAt ? new Date(latest.downloadExpiresAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "soon",
|
|
1073
|
+
"; reload for a new one."
|
|
1074
|
+
] })
|
|
1075
|
+
] }),
|
|
1076
|
+
/* @__PURE__ */ jsxs6(Collapsible2, { open, onOpenChange: setOpen, children: [
|
|
1077
|
+
/* @__PURE__ */ jsx7(CollapsibleTrigger2, { asChild: true, children: /* @__PURE__ */ jsxs6(Button5, { type: "button", variant: "ghost", className: "-ml-4", children: [
|
|
1078
|
+
open ? "Hide sources" : `Show ${latest.units.length} sources`,
|
|
1079
|
+
/* @__PURE__ */ jsx7(ChevronDown2, { "aria-hidden": "true", className: open ? "rotate-180" : void 0 })
|
|
1080
|
+
] }) }),
|
|
1081
|
+
/* @__PURE__ */ jsx7(CollapsibleContent2, { children: /* @__PURE__ */ jsx7("ul", { className: "mt-1 divide-y divide-[color:var(--border-subtle)] rounded-lg border border-border", children: latest.units.map((unit) => /* @__PURE__ */ jsxs6("li", { className: "flex flex-wrap items-center justify-between gap-x-3 gap-y-1 px-3 py-2", children: [
|
|
1082
|
+
/* @__PURE__ */ jsx7("span", { className: "min-w-0 [overflow-wrap:anywhere]", children: unit.name }),
|
|
1083
|
+
/* @__PURE__ */ jsx7("span", { className: unit.status === "failed" ? "text-[var(--surface-danger-text)]" : "text-muted-foreground", children: unitState(unit) })
|
|
1084
|
+
] }, unit.name)) }) })
|
|
1085
|
+
] })
|
|
1086
|
+
] })
|
|
1087
|
+
}
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
function ExportSection({
|
|
1091
|
+
id = "export",
|
|
1092
|
+
title = "Export",
|
|
1093
|
+
endpoint,
|
|
1094
|
+
canExport,
|
|
1095
|
+
disabledReason,
|
|
1096
|
+
description = "A zip of everything. Credentials are never included.",
|
|
1097
|
+
fetcher,
|
|
1098
|
+
continuesInBackground = false,
|
|
1099
|
+
children
|
|
1100
|
+
}) {
|
|
1101
|
+
const { latest, error, loading, starting, running, start, retry } = useWorkspaceExport({ endpoint, enabled: canExport, fetcher });
|
|
1102
|
+
const showExport = canExport || disabledReason != null;
|
|
1103
|
+
if (!showExport && children == null) return null;
|
|
1104
|
+
return /* @__PURE__ */ jsxs6(SettingsSection2, { id, title, children: [
|
|
1105
|
+
showExport && /* @__PURE__ */ jsx7(
|
|
1106
|
+
SettingsRow2,
|
|
1107
|
+
{
|
|
1108
|
+
label: "Export everything",
|
|
1109
|
+
layout: "compact",
|
|
1110
|
+
description,
|
|
1111
|
+
disabledReason: canExport ? void 0 : disabledReason,
|
|
1112
|
+
error: canExport ? error ?? void 0 : void 0,
|
|
1113
|
+
onRetry: canExport && error ? retry : void 0,
|
|
1114
|
+
children: /* @__PURE__ */ jsx7(Button5, { type: "button", variant: "outline", disabled: !canExport || loading || starting || running, loading: starting, onClick: () => void start(), children: running ? "Exporting\u2026" : "Export" })
|
|
1115
|
+
}
|
|
1116
|
+
),
|
|
1117
|
+
canExport && latest && /* @__PURE__ */ jsx7(LatestExport, { latest, continuesInBackground }),
|
|
1118
|
+
children
|
|
1119
|
+
] });
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// src/settings-react/delete-workspace-section.tsx
|
|
1123
|
+
import { Button as Button6, SettingsRow as SettingsRow3, SettingsSection as SettingsSection3 } from "@tangle-network/ui/primitives";
|
|
1124
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1125
|
+
function listCounts(counts) {
|
|
1126
|
+
const shown = counts.filter((entry) => entry.count > 0).map((entry) => `${entry.count.toLocaleString()} ${entry.label}`);
|
|
1127
|
+
if (shown.length === 0) return null;
|
|
1128
|
+
return shown.length === 1 ? shown[0] : `${shown.slice(0, -1).join(", ")} and ${shown[shown.length - 1]}`;
|
|
1129
|
+
}
|
|
1130
|
+
function DeleteWorkspaceSection({
|
|
1131
|
+
id = "delete",
|
|
1132
|
+
name,
|
|
1133
|
+
counts = [],
|
|
1134
|
+
noun = "project",
|
|
1135
|
+
disabledReason,
|
|
1136
|
+
notice,
|
|
1137
|
+
onDelete,
|
|
1138
|
+
onDeleted
|
|
1139
|
+
}) {
|
|
1140
|
+
const title = `Delete ${noun}`;
|
|
1141
|
+
const removed = listCounts(counts);
|
|
1142
|
+
const trigger = /* @__PURE__ */ jsxs7(Button6, { type: "button", variant: "destructive-outline", disabled: disabledReason != null, children: [
|
|
1143
|
+
title,
|
|
1144
|
+
"\u2026"
|
|
1145
|
+
] });
|
|
1146
|
+
return /* @__PURE__ */ jsx8(SettingsSection3, { id, title, tone: "danger", children: /* @__PURE__ */ jsx8(
|
|
1147
|
+
SettingsRow3,
|
|
1148
|
+
{
|
|
1149
|
+
label: `Delete this ${noun} for everyone`,
|
|
1150
|
+
description: "This can\u2019t be undone.",
|
|
1151
|
+
disabledReason,
|
|
1152
|
+
layout: "compact",
|
|
1153
|
+
children: disabledReason != null ? trigger : /* @__PURE__ */ jsx8(
|
|
1154
|
+
ConfirmAction,
|
|
1155
|
+
{
|
|
1156
|
+
trigger,
|
|
1157
|
+
title: `Delete ${name}?`,
|
|
1158
|
+
consequence: removed ? `This permanently deletes ${name} for everyone: ${removed}. It can\u2019t be undone.` : `This permanently deletes ${name} for everyone. It can\u2019t be undone.`,
|
|
1159
|
+
notice,
|
|
1160
|
+
confirmLabel: title,
|
|
1161
|
+
tone: "danger",
|
|
1162
|
+
requireText: name,
|
|
1163
|
+
onConfirm: onDelete,
|
|
1164
|
+
onConfirmed: onDeleted
|
|
1165
|
+
}
|
|
1166
|
+
)
|
|
1167
|
+
}
|
|
1168
|
+
) });
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// src/settings-react/timezone-picker.tsx
|
|
1172
|
+
import { Button as Button7, Input as Input3 } from "@tangle-network/ui/primitives";
|
|
1173
|
+
import { Check as Check2, ChevronDown as ChevronDown3 } from "lucide-react";
|
|
1174
|
+
import { useCallback as useCallback4, useEffect as useEffect6, useId as useId3, useMemo as useMemo2, useRef as useRef4, useState as useState7 } from "react";
|
|
1175
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1176
|
+
var REGIONS = [
|
|
1177
|
+
["America", "Americas"],
|
|
1178
|
+
["Europe", "Europe"],
|
|
1179
|
+
["Africa", "Africa"],
|
|
1180
|
+
["Asia", "Asia"],
|
|
1181
|
+
["Australia", "Australia"],
|
|
1182
|
+
["Pacific", "Pacific"],
|
|
1183
|
+
["Atlantic", "Atlantic"],
|
|
1184
|
+
["Indian", "Indian Ocean"],
|
|
1185
|
+
["Antarctica", "Antarctica"],
|
|
1186
|
+
["Arctic", "Arctic"]
|
|
1187
|
+
];
|
|
1188
|
+
var OTHER_REGION = "UTC and other";
|
|
1189
|
+
function regionOf(id) {
|
|
1190
|
+
const prefix = id.split("/")[0] ?? "";
|
|
1191
|
+
return REGIONS.find(([name]) => name === prefix)?.[1] ?? OTHER_REGION;
|
|
1192
|
+
}
|
|
1193
|
+
function cityOf(id) {
|
|
1194
|
+
if (id === "UTC" || id === "Etc/UTC") return "Coordinated Universal Time";
|
|
1195
|
+
return (id.split("/").pop() ?? id).replace(/_/g, " ");
|
|
1196
|
+
}
|
|
1197
|
+
function offsetMinutesOf(id, now) {
|
|
1198
|
+
try {
|
|
1199
|
+
const part = new Intl.DateTimeFormat("en-US", { timeZone: id, timeZoneName: "longOffset" }).formatToParts(now).find((p) => p.type === "timeZoneName")?.value ?? "";
|
|
1200
|
+
const match = /^GMT(?:([+-−])(\d{1,2})(?::(\d{2}))?)?$/.exec(part);
|
|
1201
|
+
if (!match) return null;
|
|
1202
|
+
if (!match[1]) return 0;
|
|
1203
|
+
const minutes = Number(match[2]) * 60 + Number(match[3] ?? 0);
|
|
1204
|
+
return match[1] === "+" ? minutes : -minutes;
|
|
1205
|
+
} catch {
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
function offsetLabel(minutes) {
|
|
1210
|
+
if (minutes === 0) return "UTC";
|
|
1211
|
+
const sign = minutes > 0 ? "+" : "\u2212";
|
|
1212
|
+
const abs = Math.abs(minutes);
|
|
1213
|
+
const rest = abs % 60;
|
|
1214
|
+
return `UTC${sign}${Math.floor(abs / 60)}${rest ? `:${String(rest).padStart(2, "0")}` : ""}`;
|
|
1215
|
+
}
|
|
1216
|
+
var CURRENT_NAME = {
|
|
1217
|
+
"America/Buenos_Aires": "America/Argentina/Buenos_Aires",
|
|
1218
|
+
"America/Catamarca": "America/Argentina/Catamarca",
|
|
1219
|
+
"America/Cordoba": "America/Argentina/Cordoba",
|
|
1220
|
+
"America/Godthab": "America/Nuuk",
|
|
1221
|
+
"America/Indianapolis": "America/Indiana/Indianapolis",
|
|
1222
|
+
"America/Jujuy": "America/Argentina/Jujuy",
|
|
1223
|
+
"America/Louisville": "America/Kentucky/Louisville",
|
|
1224
|
+
"America/Mendoza": "America/Argentina/Mendoza",
|
|
1225
|
+
"Asia/Calcutta": "Asia/Kolkata",
|
|
1226
|
+
"Asia/Katmandu": "Asia/Kathmandu",
|
|
1227
|
+
"Asia/Rangoon": "Asia/Yangon",
|
|
1228
|
+
"Asia/Saigon": "Asia/Ho_Chi_Minh",
|
|
1229
|
+
"Atlantic/Faeroe": "Atlantic/Faroe",
|
|
1230
|
+
"Europe/Kiev": "Europe/Kyiv",
|
|
1231
|
+
"Pacific/Enderbury": "Pacific/Kanton",
|
|
1232
|
+
"Pacific/Ponape": "Pacific/Pohnpei",
|
|
1233
|
+
"Pacific/Truk": "Pacific/Chuuk"
|
|
1234
|
+
};
|
|
1235
|
+
function zoneOf(id, now, legacy) {
|
|
1236
|
+
const offset = offsetMinutesOf(id, now);
|
|
1237
|
+
if (offset === null) return null;
|
|
1238
|
+
return { id, ...legacy ? { legacy } : {}, city: cityOf(id), region: regionOf(id), offsetMinutes: offset, offsetLabel: offsetLabel(offset) };
|
|
1239
|
+
}
|
|
1240
|
+
function allZones(now) {
|
|
1241
|
+
const listed = typeof Intl.supportedValuesOf === "function" ? Intl.supportedValuesOf("timeZone") : [];
|
|
1242
|
+
const zones = /* @__PURE__ */ new Map();
|
|
1243
|
+
for (const name of listed.includes("UTC") ? listed : [...listed, "UTC"]) {
|
|
1244
|
+
const id = CURRENT_NAME[name] ?? name;
|
|
1245
|
+
if (zones.has(id)) continue;
|
|
1246
|
+
const zone = zoneOf(id, now, id === name ? void 0 : name) ?? zoneOf(name, now);
|
|
1247
|
+
if (zone) zones.set(zone.id, zone);
|
|
1248
|
+
}
|
|
1249
|
+
return [...zones.values()];
|
|
1250
|
+
}
|
|
1251
|
+
function queryOffset(query) {
|
|
1252
|
+
const match = /^(?:utc|gmt)?\s*([+\-−])\s*(\d{1,2})(?::?(\d{2}))?$/.exec(query);
|
|
1253
|
+
if (!match) return /^(?:utc|gmt)$/.test(query) ? 0 : null;
|
|
1254
|
+
const minutes = Number(match[2]) * 60 + Number(match[3] ?? 0);
|
|
1255
|
+
return match[1] === "+" ? minutes : -minutes;
|
|
1256
|
+
}
|
|
1257
|
+
function matches(zone, query) {
|
|
1258
|
+
const offset = queryOffset(query);
|
|
1259
|
+
if (offset !== null) return zone.offsetMinutes === offset;
|
|
1260
|
+
return zone.city.toLowerCase().includes(query) || zone.id.toLowerCase().replace(/_/g, " ").includes(query) || (zone.legacy?.toLowerCase().replace(/_/g, " ").includes(query) ?? false) || zone.region.toLowerCase().includes(query);
|
|
1261
|
+
}
|
|
1262
|
+
function grouped(zones) {
|
|
1263
|
+
const order = [...REGIONS.map(([, label]) => label), OTHER_REGION];
|
|
1264
|
+
return order.map((region) => ({
|
|
1265
|
+
region,
|
|
1266
|
+
zones: zones.filter((zone) => zone.region === region).sort((a, b) => a.offsetMinutes - b.offsetMinutes || a.city.localeCompare(b.city))
|
|
1267
|
+
})).filter((group) => group.zones.length > 0);
|
|
1268
|
+
}
|
|
1269
|
+
function TimezonePicker({ id, value, onChange, disabled = false, "aria-describedby": describedBy, "aria-invalid": invalid }) {
|
|
1270
|
+
const [open, setOpen] = useState7(false);
|
|
1271
|
+
const [query, setQuery] = useState7("");
|
|
1272
|
+
const expanded = open && !disabled;
|
|
1273
|
+
const changeOpen = useCallback4((next) => {
|
|
1274
|
+
setOpen(next && !disabled);
|
|
1275
|
+
if (!next) setQuery("");
|
|
1276
|
+
}, [disabled]);
|
|
1277
|
+
const { containerRef, triggerRef, panelRef, triggerProps } = usePopover(expanded, changeOpen);
|
|
1278
|
+
const inputRef = useRef4(null);
|
|
1279
|
+
const panelId = useId3();
|
|
1280
|
+
const searchId = useId3();
|
|
1281
|
+
const now = useMemo2(() => /* @__PURE__ */ new Date(), [expanded]);
|
|
1282
|
+
const selected = useMemo2(() => zoneOf(CURRENT_NAME[value] ?? value, /* @__PURE__ */ new Date()), [value]);
|
|
1283
|
+
const zones = useMemo2(() => expanded ? allZones(now) : [], [expanded, now]);
|
|
1284
|
+
const q = query.trim().toLowerCase();
|
|
1285
|
+
const groups = useMemo2(() => grouped(q ? zones.filter((zone) => matches(zone, q)) : zones), [zones, q]);
|
|
1286
|
+
useEffect6(() => {
|
|
1287
|
+
if (!expanded) return;
|
|
1288
|
+
const focus = () => inputRef.current?.focus();
|
|
1289
|
+
focus();
|
|
1290
|
+
const frame = requestAnimationFrame(focus);
|
|
1291
|
+
return () => cancelAnimationFrame(frame);
|
|
1292
|
+
}, [expanded]);
|
|
1293
|
+
useEffect6(() => {
|
|
1294
|
+
const panel = panelRef.current;
|
|
1295
|
+
if (!expanded || !panel) return;
|
|
1296
|
+
function onKeyDown(event) {
|
|
1297
|
+
if (event.defaultPrevented || event.isComposing || event.altKey || event.ctrlKey || event.metaKey) return;
|
|
1298
|
+
const input = inputRef.current;
|
|
1299
|
+
if (!input || !panel) return;
|
|
1300
|
+
const controls = [input, ...panel.querySelectorAll("button[data-zone]")];
|
|
1301
|
+
const index = controls.indexOf(document.activeElement);
|
|
1302
|
+
if (index < 0) return;
|
|
1303
|
+
if (event.key === "Tab") {
|
|
1304
|
+
if (event.shiftKey && index === 0 || !event.shiftKey && index === controls.length - 1) {
|
|
1305
|
+
changeOpen(false);
|
|
1306
|
+
triggerRef.current?.focus();
|
|
1307
|
+
if (event.shiftKey) event.preventDefault();
|
|
1308
|
+
}
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
let next;
|
|
1312
|
+
if (event.key === "ArrowDown") next = (index + 1) % controls.length;
|
|
1313
|
+
else if (event.key === "ArrowUp") next = (index - 1 + controls.length) % controls.length;
|
|
1314
|
+
else if (event.key === "Enter" && index === 0 && controls.length > 1) {
|
|
1315
|
+
event.preventDefault();
|
|
1316
|
+
controls[1].click();
|
|
1317
|
+
return;
|
|
1318
|
+
} else return;
|
|
1319
|
+
event.preventDefault();
|
|
1320
|
+
controls[next]?.focus();
|
|
1321
|
+
}
|
|
1322
|
+
panel.addEventListener("keydown", onKeyDown);
|
|
1323
|
+
return () => panel.removeEventListener("keydown", onKeyDown);
|
|
1324
|
+
});
|
|
1325
|
+
const pick = (zone) => {
|
|
1326
|
+
changeOpen(false);
|
|
1327
|
+
triggerRef.current?.focus();
|
|
1328
|
+
if (zone !== value) onChange(zone);
|
|
1329
|
+
};
|
|
1330
|
+
return /* @__PURE__ */ jsxs8("div", { ref: containerRef, className: "relative flex w-full min-w-0", children: [
|
|
1331
|
+
/* @__PURE__ */ jsxs8(
|
|
1332
|
+
Button7,
|
|
1333
|
+
{
|
|
1334
|
+
type: "button",
|
|
1335
|
+
variant: "bare",
|
|
1336
|
+
...triggerProps,
|
|
1337
|
+
id,
|
|
1338
|
+
disabled,
|
|
1339
|
+
"aria-haspopup": "dialog",
|
|
1340
|
+
"aria-controls": expanded ? panelId : void 0,
|
|
1341
|
+
"aria-describedby": describedBy,
|
|
1342
|
+
"aria-invalid": invalid || void 0,
|
|
1343
|
+
"data-state": expanded ? "open" : "closed",
|
|
1344
|
+
onClick: () => changeOpen(!expanded),
|
|
1345
|
+
onKeyDown: (event) => {
|
|
1346
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
1347
|
+
event.preventDefault();
|
|
1348
|
+
changeOpen(true);
|
|
1349
|
+
}
|
|
1350
|
+
},
|
|
1351
|
+
className: fieldPickerTriggerClass({ interactive: !disabled }),
|
|
1352
|
+
children: [
|
|
1353
|
+
/* @__PURE__ */ jsxs8("span", { className: "flex min-w-0 flex-1 items-center gap-2", children: [
|
|
1354
|
+
/* @__PURE__ */ jsx9("span", { className: "min-w-0 truncate", children: selected?.city ?? (value || "Choose a timezone") }),
|
|
1355
|
+
selected && /* @__PURE__ */ jsx9("span", { className: "shrink-0 text-muted-foreground tabular-nums", children: selected.offsetLabel })
|
|
1356
|
+
] }),
|
|
1357
|
+
/* @__PURE__ */ jsx9(ChevronDown3, { "aria-hidden": "true", className: "size-4 shrink-0 text-muted-foreground" })
|
|
1358
|
+
]
|
|
1359
|
+
}
|
|
1360
|
+
),
|
|
1361
|
+
/* @__PURE__ */ jsxs8(
|
|
1362
|
+
PopoverSurface,
|
|
1363
|
+
{
|
|
1364
|
+
open: expanded,
|
|
1365
|
+
id: panelId,
|
|
1366
|
+
role: "dialog",
|
|
1367
|
+
"aria-label": "Choose a timezone",
|
|
1368
|
+
triggerRef,
|
|
1369
|
+
panelRef,
|
|
1370
|
+
matchTriggerWidth: true,
|
|
1371
|
+
side: "below",
|
|
1372
|
+
contentKey: q,
|
|
1373
|
+
className: `flex w-[22rem] max-w-[calc(100vw-2rem)] flex-col overflow-hidden rounded-xl border border-card-edge bg-popover text-foreground ${OVERLAY_SHADOW}`,
|
|
1374
|
+
children: [
|
|
1375
|
+
/* @__PURE__ */ jsx9("div", { className: "shrink-0 border-b border-border p-2", children: /* @__PURE__ */ jsx9(
|
|
1376
|
+
Input3,
|
|
1377
|
+
{
|
|
1378
|
+
ref: inputRef,
|
|
1379
|
+
id: searchId,
|
|
1380
|
+
value: query,
|
|
1381
|
+
onChange: (event) => setQuery(event.target.value),
|
|
1382
|
+
placeholder: "Search by city or UTC offset",
|
|
1383
|
+
"aria-label": "Search timezones",
|
|
1384
|
+
autoComplete: "off",
|
|
1385
|
+
spellCheck: false
|
|
1386
|
+
}
|
|
1387
|
+
) }),
|
|
1388
|
+
/* @__PURE__ */ jsxs8("div", { className: "min-h-0 overflow-y-auto p-1 pb-2", "data-zone-list": true, children: [
|
|
1389
|
+
groups.length === 0 && /* @__PURE__ */ jsxs8("p", { role: "status", className: "px-3 py-4 text-center text-sm text-muted-foreground", children: [
|
|
1390
|
+
"No timezone matches \u201C",
|
|
1391
|
+
query.trim(),
|
|
1392
|
+
"\u201D"
|
|
1393
|
+
] }),
|
|
1394
|
+
groups.map((group) => /* @__PURE__ */ jsxs8("div", { role: "group", "aria-label": group.region, children: [
|
|
1395
|
+
/* @__PURE__ */ jsx9("p", { className: "px-3 pb-1 pt-3 text-sm font-semibold text-muted-foreground", children: group.region }),
|
|
1396
|
+
group.zones.map((zone) => {
|
|
1397
|
+
const current = zone.id === (CURRENT_NAME[value] ?? value);
|
|
1398
|
+
return /* @__PURE__ */ jsxs8(
|
|
1399
|
+
Button7,
|
|
1400
|
+
{
|
|
1401
|
+
type: "button",
|
|
1402
|
+
variant: "bare",
|
|
1403
|
+
"data-zone": zone.id,
|
|
1404
|
+
"aria-pressed": current,
|
|
1405
|
+
title: zone.id,
|
|
1406
|
+
onClick: () => pick(zone.id),
|
|
1407
|
+
className: `flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm ${POPOVER_OPTION_FOCUS}`,
|
|
1408
|
+
children: [
|
|
1409
|
+
/* @__PURE__ */ jsx9("span", { className: "min-w-0 flex-1 truncate", children: zone.city }),
|
|
1410
|
+
/* @__PURE__ */ jsx9("span", { className: "shrink-0 text-muted-foreground tabular-nums", children: zone.offsetLabel }),
|
|
1411
|
+
/* @__PURE__ */ jsx9(Check2, { "aria-hidden": "true", className: `size-4 shrink-0 text-[var(--accent-text)] ${current ? "" : "invisible"}` })
|
|
1412
|
+
]
|
|
1413
|
+
},
|
|
1414
|
+
zone.id
|
|
1415
|
+
);
|
|
1416
|
+
})
|
|
1417
|
+
] }, group.region))
|
|
1418
|
+
] })
|
|
1419
|
+
]
|
|
1420
|
+
}
|
|
1421
|
+
)
|
|
1422
|
+
] });
|
|
1423
|
+
}
|
|
1424
|
+
export {
|
|
1425
|
+
ConfirmAction,
|
|
1426
|
+
DeleteWorkspaceSection,
|
|
1427
|
+
ExportSection,
|
|
1428
|
+
InvitationsSection,
|
|
1429
|
+
MembersSection,
|
|
1430
|
+
SettingsLayout,
|
|
1431
|
+
SettingsNav,
|
|
1432
|
+
SettingsSaveBar,
|
|
1433
|
+
TimezonePicker,
|
|
1434
|
+
confirmSettingResponse,
|
|
1435
|
+
settingFailure,
|
|
1436
|
+
useSettingField
|
|
1437
|
+
};
|
|
1438
|
+
//# sourceMappingURL=index.js.map
|