@estiva-app/ui 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -9
- package/base.css +43 -0
- package/dist/AppShell.d.ts +39 -0
- package/dist/AppShell.d.ts.map +1 -0
- package/dist/Avatar.d.ts +7 -2
- package/dist/Avatar.d.ts.map +1 -1
- package/dist/AvatarGroup.d.ts +23 -0
- package/dist/AvatarGroup.d.ts.map +1 -0
- package/dist/Banner.d.ts +19 -0
- package/dist/Banner.d.ts.map +1 -0
- package/dist/Breadcrumb.d.ts +28 -0
- package/dist/Breadcrumb.d.ts.map +1 -0
- package/dist/Checkbox.d.ts +18 -0
- package/dist/Checkbox.d.ts.map +1 -0
- package/dist/ChipInput.d.ts +57 -0
- package/dist/ChipInput.d.ts.map +1 -0
- package/dist/ConfirmDialog.d.ts +22 -0
- package/dist/ConfirmDialog.d.ts.map +1 -0
- package/dist/DialogShell.d.ts +9 -2
- package/dist/DialogShell.d.ts.map +1 -1
- package/dist/EditableText.d.ts +56 -0
- package/dist/EditableText.d.ts.map +1 -0
- package/dist/IdentityMenu.d.ts +58 -0
- package/dist/IdentityMenu.d.ts.map +1 -0
- package/dist/Menu.d.ts +82 -0
- package/dist/Menu.d.ts.map +1 -0
- package/dist/NavItem.d.ts +26 -0
- package/dist/NavItem.d.ts.map +1 -0
- package/dist/Person.d.ts +26 -0
- package/dist/Person.d.ts.map +1 -0
- package/dist/PersonTrigger.d.ts +23 -0
- package/dist/PersonTrigger.d.ts.map +1 -0
- package/dist/Property.d.ts +24 -0
- package/dist/Property.d.ts.map +1 -0
- package/dist/Rail.d.ts +18 -0
- package/dist/Rail.d.ts.map +1 -0
- package/dist/RailItem.d.ts +23 -0
- package/dist/RailItem.d.ts.map +1 -0
- package/dist/SearchInput.d.ts +21 -0
- package/dist/SearchInput.d.ts.map +1 -0
- package/dist/SectionHeader.d.ts +34 -0
- package/dist/SectionHeader.d.ts.map +1 -0
- package/dist/SectionLabel.d.ts +19 -0
- package/dist/SectionLabel.d.ts.map +1 -0
- package/dist/Select.d.ts +42 -5
- package/dist/Select.d.ts.map +1 -1
- package/dist/Sidebar.d.ts +18 -0
- package/dist/Sidebar.d.ts.map +1 -0
- package/dist/Tabs.d.ts +34 -0
- package/dist/Tabs.d.ts.map +1 -0
- package/dist/Toast.d.ts +48 -0
- package/dist/Toast.d.ts.map +1 -0
- package/dist/Tooltip.d.ts +3 -1
- package/dist/Tooltip.d.ts.map +1 -1
- package/dist/TopBar.d.ts +33 -0
- package/dist/TopBar.d.ts.map +1 -0
- package/dist/cn.d.ts +14 -4
- package/dist/cn.d.ts.map +1 -1
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1254 -106
- package/dist/index.js.map +4 -4
- package/package.json +5 -2
- package/src/AppShell.mdx +66 -0
- package/src/AppShell.stories.tsx +117 -0
- package/src/AppShell.tsx +77 -0
- package/src/Avatar.initials.test.ts +21 -0
- package/src/Avatar.mdx +49 -0
- package/src/Avatar.tsx +12 -9
- package/src/AvatarGroup.mdx +39 -0
- package/src/AvatarGroup.stories.tsx +56 -0
- package/src/AvatarGroup.tsx +42 -0
- package/src/Banner.mdx +45 -0
- package/src/Banner.stories.tsx +35 -0
- package/src/Banner.tsx +35 -0
- package/src/Breadcrumb.mdx +52 -0
- package/src/Breadcrumb.stories.tsx +42 -0
- package/src/Breadcrumb.tsx +106 -0
- package/src/Button.mdx +57 -0
- package/src/Checkbox.mdx +44 -0
- package/src/Checkbox.stories.tsx +49 -0
- package/src/Checkbox.tsx +46 -0
- package/src/Chip.mdx +43 -0
- package/src/ChipInput.mdx +68 -0
- package/src/ChipInput.stories.tsx +99 -0
- package/src/ChipInput.tsx +247 -0
- package/src/ConfirmDialog.mdx +51 -0
- package/src/ConfirmDialog.stories.tsx +63 -0
- package/src/ConfirmDialog.tsx +55 -0
- package/src/DialogShell.mdx +68 -0
- package/src/DialogShell.tsx +12 -5
- package/src/Divider.mdx +41 -0
- package/src/EditableText.mdx +59 -0
- package/src/EditableText.stories.tsx +63 -0
- package/src/EditableText.tsx +169 -0
- package/src/EmptyState.mdx +41 -0
- package/src/Field.mdx +39 -0
- package/src/IconButton.mdx +47 -0
- package/src/IdentityMenu.mdx +62 -0
- package/src/IdentityMenu.stories.tsx +52 -0
- package/src/IdentityMenu.tsx +159 -0
- package/src/Menu.mdx +111 -0
- package/src/Menu.stories.tsx +66 -0
- package/src/Menu.tsx +172 -0
- package/src/MenuItem.mdx +55 -0
- package/src/MenuItem.stories.tsx +98 -0
- package/src/NavItem.mdx +53 -0
- package/src/NavItem.stories.tsx +36 -0
- package/src/NavItem.tsx +53 -0
- package/src/Person.mdx +44 -0
- package/src/Person.stories.tsx +43 -0
- package/src/Person.tsx +39 -0
- package/src/PersonTrigger.mdx +51 -0
- package/src/PersonTrigger.stories.tsx +30 -0
- package/src/PersonTrigger.tsx +62 -0
- package/src/Property.mdx +51 -0
- package/src/Property.stories.tsx +91 -0
- package/src/Property.tsx +44 -0
- package/src/Rail.tsx +26 -0
- package/src/RailItem.mdx +50 -0
- package/src/RailItem.stories.tsx +37 -0
- package/src/RailItem.tsx +53 -0
- package/src/SearchInput.mdx +39 -0
- package/src/SearchInput.stories.tsx +24 -0
- package/src/SearchInput.tsx +45 -0
- package/src/SectionHeader.mdx +52 -0
- package/src/SectionHeader.stories.tsx +66 -0
- package/src/SectionHeader.tsx +85 -0
- package/src/SectionLabel.mdx +42 -0
- package/src/SectionLabel.stories.tsx +31 -0
- package/src/SectionLabel.tsx +28 -0
- package/src/Select.fit.test.ts +97 -0
- package/src/Select.mdx +71 -0
- package/src/Select.stories.tsx +95 -0
- package/src/Select.tsx +123 -15
- package/src/Sidebar.mdx +56 -0
- package/src/Sidebar.stories.tsx +50 -0
- package/src/Sidebar.tsx +32 -0
- package/src/Skeleton.mdx +39 -0
- package/src/Tabs.mdx +53 -0
- package/src/Tabs.stories.tsx +91 -0
- package/src/Tabs.tsx +73 -0
- package/src/TextInput.mdx +39 -0
- package/src/Textarea.mdx +36 -0
- package/src/Toast.mdx +54 -0
- package/src/Toast.stories.tsx +87 -0
- package/src/Toast.tsx +169 -0
- package/src/Tooltip.mdx +51 -0
- package/src/Tooltip.tsx +4 -2
- package/src/TopBar.mdx +62 -0
- package/src/TopBar.stories.tsx +96 -0
- package/src/TopBar.tsx +57 -0
- package/src/cn.test.ts +27 -0
- package/src/cn.ts +25 -5
- package/src/index.ts +23 -0
- package/tailwind-preset.js +4 -3
- package/tokens.css +4 -0
package/dist/index.js
CHANGED
|
@@ -1,28 +1,112 @@
|
|
|
1
|
-
// src/Avatar.tsx
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
import { IconUserFilled } from "@tabler/icons-react";
|
|
4
|
-
|
|
5
1
|
// src/cn.ts
|
|
6
2
|
import { clsx } from "clsx";
|
|
7
|
-
import {
|
|
3
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
4
|
+
var FONT_SIZE_TOKENS = [
|
|
5
|
+
"h1",
|
|
6
|
+
"h2",
|
|
7
|
+
"h3",
|
|
8
|
+
"h4",
|
|
9
|
+
"h5",
|
|
10
|
+
"body-1",
|
|
11
|
+
"body-2",
|
|
12
|
+
"body-2-strong",
|
|
13
|
+
"caption",
|
|
14
|
+
"menu",
|
|
15
|
+
"btn-default",
|
|
16
|
+
"btn-small",
|
|
17
|
+
"input-label",
|
|
18
|
+
"input-value",
|
|
19
|
+
"input-helper",
|
|
20
|
+
"chip"
|
|
21
|
+
];
|
|
22
|
+
var twMerge = extendTailwindMerge({
|
|
23
|
+
extend: { classGroups: { "font-size": [{ text: FONT_SIZE_TOKENS }] } }
|
|
24
|
+
});
|
|
8
25
|
function cn(...inputs) {
|
|
9
26
|
return twMerge(clsx(inputs));
|
|
10
27
|
}
|
|
11
28
|
|
|
29
|
+
// src/TopBar.tsx
|
|
30
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
31
|
+
function TopBar({ variant = "solid", menu, logo, search, right, className }) {
|
|
32
|
+
const floating = variant === "floating";
|
|
33
|
+
return /* @__PURE__ */ jsxs(
|
|
34
|
+
"header",
|
|
35
|
+
{
|
|
36
|
+
className: cn(
|
|
37
|
+
"flex h-[52px] items-center pl-5 pr-[26px]",
|
|
38
|
+
floating ? "pointer-events-none absolute left-0 right-0 top-0 z-10" : "shrink-0 gap-4 border-b border-border-default bg-bg-surface",
|
|
39
|
+
className
|
|
40
|
+
),
|
|
41
|
+
children: [
|
|
42
|
+
(menu || logo) && /* @__PURE__ */ jsxs("div", { className: cn("flex shrink-0 items-center gap-2", floating && "pointer-events-auto"), children: [
|
|
43
|
+
menu,
|
|
44
|
+
logo && /* @__PURE__ */ jsx("div", { className: "flex items-center text-body-2-strong text-text-primary", children: logo })
|
|
45
|
+
] }),
|
|
46
|
+
/* @__PURE__ */ jsx("div", { className: cn("flex min-w-0 flex-1 items-center justify-center", floating && "pointer-events-auto"), children: search }),
|
|
47
|
+
/* @__PURE__ */ jsx("div", { className: cn("flex shrink-0 items-center gap-[6px]", floating && "pointer-events-auto"), children: right })
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/AppShell.tsx
|
|
54
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
55
|
+
function AppShell({ variant = "solid", menu, logo, search, identity, banner, nav, children }) {
|
|
56
|
+
const bar = /* @__PURE__ */ jsx2(TopBar, { variant, menu, logo, search, right: identity });
|
|
57
|
+
if (variant === "floating") {
|
|
58
|
+
return /* @__PURE__ */ jsxs2("div", { className: "relative h-full min-h-0 overflow-hidden bg-bg-base", children: [
|
|
59
|
+
bar,
|
|
60
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex h-full pb-4 pr-4 pt-[52px]", children: [
|
|
61
|
+
nav,
|
|
62
|
+
/* @__PURE__ */ jsxs2(
|
|
63
|
+
"div",
|
|
64
|
+
{
|
|
65
|
+
className: cn(
|
|
66
|
+
"flex min-w-0 flex-1 flex-col overflow-hidden rounded-2xl bg-bg-surface",
|
|
67
|
+
"signal:border signal:border-border-subtle signal:shadow-[inset_0_1px_0_rgba(255,255,255,0.035)]"
|
|
68
|
+
),
|
|
69
|
+
children: [
|
|
70
|
+
banner,
|
|
71
|
+
/* @__PURE__ */ jsx2("main", { className: "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden", children })
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
] })
|
|
76
|
+
] });
|
|
77
|
+
}
|
|
78
|
+
return /* @__PURE__ */ jsxs2("div", { className: "flex h-full min-h-0 flex-col bg-bg-base text-text-primary", children: [
|
|
79
|
+
bar,
|
|
80
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex min-h-0 flex-1", children: [
|
|
81
|
+
nav,
|
|
82
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", children: [
|
|
83
|
+
banner,
|
|
84
|
+
/* @__PURE__ */ jsx2("main", { className: "min-w-0 flex-1 overflow-y-auto", children })
|
|
85
|
+
] })
|
|
86
|
+
] })
|
|
87
|
+
] });
|
|
88
|
+
}
|
|
89
|
+
|
|
12
90
|
// src/Avatar.tsx
|
|
13
|
-
import {
|
|
91
|
+
import { useState } from "react";
|
|
92
|
+
import { IconUserFilled } from "@tabler/icons-react";
|
|
93
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
14
94
|
var HUES = ["#56c8ff", "#ff8f6b", "#4ade8c", "#b18cff", "#ffc94d", "#ff7eb0", "#7ea8ff", "#5fdfd6"];
|
|
15
95
|
var hueFor = (key) => {
|
|
16
96
|
let h = 0;
|
|
17
97
|
for (let i = 0; i < key.length; i++) h = h * 31 + key.charCodeAt(i) | 0;
|
|
18
98
|
return HUES[Math.abs(h) % HUES.length];
|
|
19
99
|
};
|
|
20
|
-
var initialsFor = (name) =>
|
|
100
|
+
var initialsFor = (name) => {
|
|
101
|
+
const words = name.split(/\s+/).slice(0, 2).filter((w) => /^[\p{L}\p{N}]/u.test(w));
|
|
102
|
+
const initials = words.map((w) => w[0]).join("") || name.trim().charAt(0);
|
|
103
|
+
return initials.toUpperCase();
|
|
104
|
+
};
|
|
21
105
|
function Avatar({ src, name, alt = "", size = 36, className }) {
|
|
22
106
|
const [broken, setBroken] = useState(false);
|
|
23
107
|
const label = name || alt;
|
|
24
108
|
const picture = src && !broken ? src : void 0;
|
|
25
|
-
return /* @__PURE__ */
|
|
109
|
+
return /* @__PURE__ */ jsx3("div", { className: cn("rounded-sm overflow-hidden shrink-0 bg-bg-inset", className), style: { width: size, height: size }, children: picture ? /* @__PURE__ */ jsx3("img", { src: picture, alt: alt || name || "", className: "w-full h-full object-cover", onError: () => setBroken(true) }) : label ? /* @__PURE__ */ jsx3(
|
|
26
110
|
"div",
|
|
27
111
|
{
|
|
28
112
|
className: "w-full h-full flex items-center justify-center font-semibold",
|
|
@@ -33,11 +117,40 @@ function Avatar({ src, name, alt = "", size = 36, className }) {
|
|
|
33
117
|
},
|
|
34
118
|
children: initialsFor(label)
|
|
35
119
|
}
|
|
36
|
-
) : /* @__PURE__ */
|
|
120
|
+
) : /* @__PURE__ */ jsx3("div", { className: "w-full h-full bg-accent-muted flex items-center justify-center text-text-muted", children: /* @__PURE__ */ jsx3(IconUserFilled, { size: Math.round(size * 0.5) }) }) });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/AvatarGroup.tsx
|
|
124
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
125
|
+
function AvatarGroup({ members }) {
|
|
126
|
+
const visible = members.slice(0, 3);
|
|
127
|
+
return /* @__PURE__ */ jsx4("div", { className: "flex items-center pr-2", children: visible.map((member, i) => /* @__PURE__ */ jsx4(
|
|
128
|
+
Avatar,
|
|
129
|
+
{
|
|
130
|
+
size: 24,
|
|
131
|
+
name: member.name,
|
|
132
|
+
src: member.picture,
|
|
133
|
+
alt: member.name,
|
|
134
|
+
className: "-mr-2 relative border-2 border-bg-surface"
|
|
135
|
+
},
|
|
136
|
+
i
|
|
137
|
+
)) });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/Banner.tsx
|
|
141
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
142
|
+
var TONE = {
|
|
143
|
+
ok: "bg-success-muted text-success-default",
|
|
144
|
+
error: "bg-error-muted text-error-default",
|
|
145
|
+
info: "bg-info-muted text-info-default",
|
|
146
|
+
warning: "bg-warning-muted text-warning-default"
|
|
147
|
+
};
|
|
148
|
+
function Banner({ tone, children, className }) {
|
|
149
|
+
return /* @__PURE__ */ jsx5("div", { role: tone === "error" ? "alert" : "status", className: cn("px-4 py-2 text-body-2", TONE[tone], className), children });
|
|
37
150
|
}
|
|
38
151
|
|
|
39
152
|
// src/Button.tsx
|
|
40
|
-
import { jsxs } from "react/jsx-runtime";
|
|
153
|
+
import { jsxs as jsxs3 } from "react/jsx-runtime";
|
|
41
154
|
function Button({
|
|
42
155
|
variant = "muted",
|
|
43
156
|
size = "default",
|
|
@@ -49,7 +162,7 @@ function Button({
|
|
|
49
162
|
...props
|
|
50
163
|
}) {
|
|
51
164
|
const hasLeadingIcon = !!leadingIcon;
|
|
52
|
-
return /* @__PURE__ */
|
|
165
|
+
return /* @__PURE__ */ jsxs3(
|
|
53
166
|
"button",
|
|
54
167
|
{
|
|
55
168
|
type,
|
|
@@ -78,8 +191,36 @@ function Button({
|
|
|
78
191
|
);
|
|
79
192
|
}
|
|
80
193
|
|
|
194
|
+
// src/Checkbox.tsx
|
|
195
|
+
import { IconCheck } from "@tabler/icons-react";
|
|
196
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
197
|
+
function Checkbox({ checked, onChange, disabled = false, className, ...aria }) {
|
|
198
|
+
return /* @__PURE__ */ jsx6(
|
|
199
|
+
"button",
|
|
200
|
+
{
|
|
201
|
+
type: "button",
|
|
202
|
+
role: "checkbox",
|
|
203
|
+
"aria-checked": checked,
|
|
204
|
+
"aria-label": aria["aria-label"],
|
|
205
|
+
disabled,
|
|
206
|
+
onClick: (e) => {
|
|
207
|
+
e.stopPropagation();
|
|
208
|
+
onChange?.(!checked);
|
|
209
|
+
},
|
|
210
|
+
className: cn(
|
|
211
|
+
"inline-flex items-center justify-center size-4 shrink-0 rounded-[4px] border transition-colors",
|
|
212
|
+
checked ? "bg-accent-primary border-accent-primary text-text-inverse" : "bg-transparent border-border-strong hover:border-text-muted",
|
|
213
|
+
disabled && "opacity-50 pointer-events-none",
|
|
214
|
+
onChange ? "cursor-pointer" : "pointer-events-none",
|
|
215
|
+
className
|
|
216
|
+
),
|
|
217
|
+
children: checked && /* @__PURE__ */ jsx6(IconCheck, { size: 12, stroke: 3 })
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
81
222
|
// src/Chip.tsx
|
|
82
|
-
import { jsx as
|
|
223
|
+
import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
83
224
|
var typeStyles = {
|
|
84
225
|
neutral: "bg-bg-inset text-text-primary",
|
|
85
226
|
brand: "bg-accent-muted text-accent-primary signal:border signal:border-[rgba(86,200,255,0.3)]",
|
|
@@ -89,27 +230,316 @@ var typeStyles = {
|
|
|
89
230
|
error: "bg-error-muted text-error-default signal:border signal:border-[rgba(255,107,107,0.3)]"
|
|
90
231
|
};
|
|
91
232
|
function Chip({ type = "neutral", label, leadingIcon, trailingIcon, className }) {
|
|
92
|
-
return /* @__PURE__ */
|
|
93
|
-
leadingIcon && /* @__PURE__ */
|
|
94
|
-
label && /* @__PURE__ */
|
|
95
|
-
trailingIcon && /* @__PURE__ */
|
|
233
|
+
return /* @__PURE__ */ jsxs4("div", { className: cn("inline-flex items-center justify-center gap-1.5 rounded-full max-h-[20px] min-w-[16px] px-2 py-1", typeStyles[type], className), children: [
|
|
234
|
+
leadingIcon && /* @__PURE__ */ jsx7("span", { className: "flex size-3 shrink-0 items-center justify-center", children: leadingIcon }),
|
|
235
|
+
label && /* @__PURE__ */ jsx7("span", { className: "text-chip whitespace-nowrap signal:font-mono signal:text-[10px] signal:font-semibold signal:tracking-[0.02em] signal:tabular-nums", children: label }),
|
|
236
|
+
trailingIcon && /* @__PURE__ */ jsx7("span", { className: "flex size-3 shrink-0 items-center justify-center", children: trailingIcon })
|
|
96
237
|
] });
|
|
97
238
|
}
|
|
98
239
|
|
|
99
|
-
// src/
|
|
100
|
-
import {
|
|
240
|
+
// src/ChipInput.tsx
|
|
241
|
+
import { useState as useState2, useRef as useRef2, useMemo, useEffect as useEffect2, useLayoutEffect } from "react";
|
|
242
|
+
import { createPortal as createPortal2 } from "react-dom";
|
|
243
|
+
import { IconX } from "@tabler/icons-react";
|
|
244
|
+
|
|
245
|
+
// src/Menu.tsx
|
|
246
|
+
import { useEffect, useRef } from "react";
|
|
247
|
+
import { IconChevronRight } from "@tabler/icons-react";
|
|
101
248
|
import { createPortal } from "react-dom";
|
|
102
|
-
|
|
249
|
+
|
|
250
|
+
// src/SectionLabel.tsx
|
|
251
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
252
|
+
function SectionLabel({ children, className }) {
|
|
253
|
+
return /* @__PURE__ */ jsx8(
|
|
254
|
+
"span",
|
|
255
|
+
{
|
|
256
|
+
className: cn(
|
|
257
|
+
"text-[12px] leading-[12px] font-medium text-text-primary signal:font-mono signal:text-[10px] signal:uppercase signal:tracking-[0.14em]",
|
|
258
|
+
className
|
|
259
|
+
),
|
|
260
|
+
children
|
|
261
|
+
}
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/Menu.tsx
|
|
266
|
+
import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
267
|
+
function Menu({ onClose, position, children, className }) {
|
|
268
|
+
const ref = useRef(null);
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
const onDown = (event) => {
|
|
271
|
+
if (!ref.current?.contains(event.target)) onClose();
|
|
272
|
+
};
|
|
273
|
+
const onKey = (event) => {
|
|
274
|
+
if (event.key === "Escape") onClose();
|
|
275
|
+
};
|
|
276
|
+
document.addEventListener("mousedown", onDown);
|
|
277
|
+
document.addEventListener("keydown", onKey);
|
|
278
|
+
return () => {
|
|
279
|
+
document.removeEventListener("mousedown", onDown);
|
|
280
|
+
document.removeEventListener("keydown", onKey);
|
|
281
|
+
};
|
|
282
|
+
}, [onClose]);
|
|
283
|
+
const style = position ? { top: position.top, ..."left" in position ? { left: position.left } : { right: position.right } } : void 0;
|
|
284
|
+
const node = /* @__PURE__ */ jsx9(
|
|
285
|
+
"div",
|
|
286
|
+
{
|
|
287
|
+
ref,
|
|
288
|
+
role: "menu",
|
|
289
|
+
"data-interactive": true,
|
|
290
|
+
className: cn(
|
|
291
|
+
"z-50 flex min-w-[180px] flex-col rounded-lg border border-border-default bg-bg-elevated p-2 shadow-lg",
|
|
292
|
+
position ? "fixed" : "absolute right-0 top-full mt-1",
|
|
293
|
+
className
|
|
294
|
+
),
|
|
295
|
+
style,
|
|
296
|
+
onClick: (event) => event.stopPropagation(),
|
|
297
|
+
children
|
|
298
|
+
}
|
|
299
|
+
);
|
|
300
|
+
return position ? createPortal(node, document.body) : node;
|
|
301
|
+
}
|
|
302
|
+
function MenuItem({ label, children, size = "default", description, leading, trailing, shortcut, submenu, destructive, selected, className, ...props }) {
|
|
303
|
+
const edge = trailing ?? (shortcut ? /* @__PURE__ */ jsx9("kbd", { className: "inline-flex shrink-0 items-center justify-center rounded-sm border border-border-strong bg-bg-inset px-1 py-px text-caption text-text-secondary", children: shortcut }) : submenu ? /* @__PURE__ */ jsx9(IconChevronRight, { size: 16, stroke: 1.5, className: "shrink-0 text-text-muted" }) : null);
|
|
304
|
+
return /* @__PURE__ */ jsxs5(
|
|
305
|
+
"button",
|
|
306
|
+
{
|
|
307
|
+
type: "button",
|
|
308
|
+
role: "menuitem",
|
|
309
|
+
className: cn(
|
|
310
|
+
"flex w-full cursor-pointer items-center rounded-lg text-left hover:bg-bg-hover transition-colors",
|
|
311
|
+
// tall: as tall as its content, never shorter than 40px (Katerina,
|
|
312
|
+
// 2026-09-01) — a single-line picker row sits at 40, a row with a
|
|
313
|
+
// 32px face and a role line comes out at its natural 48. One rule,
|
|
314
|
+
// not a hand-picked height per file.
|
|
315
|
+
size === "tall" ? "min-h-10 gap-3 px-3 py-1.5" : "gap-2 px-2 py-1.5",
|
|
316
|
+
selected && "bg-bg-hover",
|
|
317
|
+
className
|
|
318
|
+
),
|
|
319
|
+
...props,
|
|
320
|
+
children: [
|
|
321
|
+
leading && /* @__PURE__ */ jsx9("span", { className: "flex shrink-0 items-center", children: leading }),
|
|
322
|
+
children ?? /* @__PURE__ */ jsxs5("span", { className: cn("flex min-w-0 flex-1 flex-col", size === "tall" && "gap-[2px]"), children: [
|
|
323
|
+
/* @__PURE__ */ jsx9("span", { className: cn("truncate text-[14px] leading-[140%]", destructive ? "text-error-default" : "text-text-primary"), children: label }),
|
|
324
|
+
description && /* @__PURE__ */ jsx9("span", { className: "truncate text-[12px] leading-[120%] text-text-secondary", children: description })
|
|
325
|
+
] }),
|
|
326
|
+
edge && /* @__PURE__ */ jsx9("span", { className: "flex shrink-0 items-center", children: edge })
|
|
327
|
+
]
|
|
328
|
+
}
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
function EnterHint({ label = "Enter" }) {
|
|
332
|
+
return /* @__PURE__ */ jsxs5("span", { className: "flex shrink-0 items-center gap-2 text-text-muted", children: [
|
|
333
|
+
/* @__PURE__ */ jsx9("span", { className: "text-[12px] leading-[120%]", children: "\u21A9" }),
|
|
334
|
+
/* @__PURE__ */ jsx9("span", { className: "text-[9px] font-medium leading-[115%] signal:font-mono signal:text-[9.5px] signal:tracking-[0.04em]", children: label })
|
|
335
|
+
] });
|
|
336
|
+
}
|
|
337
|
+
function MenuSection({ label, children, className }) {
|
|
338
|
+
return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col", children: [
|
|
339
|
+
/* @__PURE__ */ jsx9("div", { className: cn("flex h-8 items-center px-2", className), children: /* @__PURE__ */ jsx9(SectionLabel, { className: "text-text-secondary", children: label }) }),
|
|
340
|
+
children
|
|
341
|
+
] });
|
|
342
|
+
}
|
|
343
|
+
function MenuRow({ children, className }) {
|
|
344
|
+
return /* @__PURE__ */ jsx9("div", { className: cn("flex items-center gap-2 px-2 py-1.5", className), children });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/ChipInput.tsx
|
|
348
|
+
import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
349
|
+
function InputChip({ label, leading, onRemove, className }) {
|
|
350
|
+
return /* @__PURE__ */ jsxs6(
|
|
351
|
+
"div",
|
|
352
|
+
{
|
|
353
|
+
className: cn(
|
|
354
|
+
// Curved, not a pill (Katerina, 2026-09-01): the Avatar keeps its own
|
|
355
|
+
// rounded-sm corners — never a forced circle — and the chip's corner
|
|
356
|
+
// follows concentrically: 4px face + 2px inset = rounded-md.
|
|
357
|
+
"inline-flex items-center gap-1.5 bg-bg-elevated border border-border-subtle rounded-md py-0.5 max-h-[24px]",
|
|
358
|
+
// The padding follows the contents (Katerina, 2026-09-01): a face
|
|
359
|
+
// sits 2px from the edge, a bare label needs 8px of air; the ✕
|
|
360
|
+
// brings its own box, so 4px behind it — 8px when there isn't one.
|
|
361
|
+
leading ? "pl-[2px]" : "pl-2",
|
|
362
|
+
onRemove ? "pr-1" : "pr-2",
|
|
363
|
+
className
|
|
364
|
+
),
|
|
365
|
+
children: [
|
|
366
|
+
leading && /* @__PURE__ */ jsx10("span", { className: "flex shrink-0 items-center", children: leading }),
|
|
367
|
+
/* @__PURE__ */ jsx10("span", { className: "text-caption font-medium text-text-primary", children: label }),
|
|
368
|
+
onRemove && /* @__PURE__ */ jsx10(
|
|
369
|
+
"button",
|
|
370
|
+
{
|
|
371
|
+
type: "button",
|
|
372
|
+
onClick: (e) => {
|
|
373
|
+
e.stopPropagation();
|
|
374
|
+
onRemove();
|
|
375
|
+
},
|
|
376
|
+
className: "size-4 flex items-center justify-center rounded-full hover:bg-bg-hover text-text-secondary",
|
|
377
|
+
"aria-label": `Remove ${label}`,
|
|
378
|
+
children: /* @__PURE__ */ jsx10(IconX, { size: 10, stroke: 1.5 })
|
|
379
|
+
}
|
|
380
|
+
)
|
|
381
|
+
]
|
|
382
|
+
}
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
function ChipInput({
|
|
386
|
+
value,
|
|
387
|
+
onChange,
|
|
388
|
+
options,
|
|
389
|
+
placeholder = "Search\u2026",
|
|
390
|
+
autoFocus,
|
|
391
|
+
excludeIds = [],
|
|
392
|
+
chipLeading,
|
|
393
|
+
rowLeading
|
|
394
|
+
}) {
|
|
395
|
+
const [query, setQuery] = useState2("");
|
|
396
|
+
const [highlight, setHighlight] = useState2(0);
|
|
397
|
+
const [isFocused, setIsFocused] = useState2(false);
|
|
398
|
+
const [anchorRect, setAnchorRect] = useState2(null);
|
|
399
|
+
const wrapperRef = useRef2(null);
|
|
400
|
+
const inputRef = useRef2(null);
|
|
401
|
+
const matches = useMemo(() => {
|
|
402
|
+
const selectedIds = new Set(value.map((o) => o.id));
|
|
403
|
+
const excludedIds = new Set(excludeIds);
|
|
404
|
+
const q = query.trim().toLowerCase();
|
|
405
|
+
return options.filter((o) => {
|
|
406
|
+
if (selectedIds.has(o.id)) return false;
|
|
407
|
+
if (excludedIds.has(o.id)) return false;
|
|
408
|
+
if (!q) return true;
|
|
409
|
+
return o.label.toLowerCase().includes(q) || (o.description ?? "").toLowerCase().includes(q);
|
|
410
|
+
});
|
|
411
|
+
}, [query, value, excludeIds, options]);
|
|
412
|
+
useEffect2(() => {
|
|
413
|
+
setHighlight(0);
|
|
414
|
+
}, [query, matches.length]);
|
|
415
|
+
const showDropdown = isFocused && query.trim().length > 0 && matches.length > 0;
|
|
416
|
+
useLayoutEffect(() => {
|
|
417
|
+
if (!showDropdown) return;
|
|
418
|
+
const update = () => {
|
|
419
|
+
if (wrapperRef.current) setAnchorRect(wrapperRef.current.getBoundingClientRect());
|
|
420
|
+
};
|
|
421
|
+
update();
|
|
422
|
+
window.addEventListener("resize", update);
|
|
423
|
+
window.addEventListener("scroll", update, true);
|
|
424
|
+
return () => {
|
|
425
|
+
window.removeEventListener("resize", update);
|
|
426
|
+
window.removeEventListener("scroll", update, true);
|
|
427
|
+
};
|
|
428
|
+
}, [showDropdown, value.length]);
|
|
429
|
+
function addOption(option) {
|
|
430
|
+
onChange([...value, option]);
|
|
431
|
+
setQuery("");
|
|
432
|
+
inputRef.current?.focus();
|
|
433
|
+
}
|
|
434
|
+
function removeOption(id) {
|
|
435
|
+
onChange(value.filter((o) => o.id !== id));
|
|
436
|
+
}
|
|
437
|
+
function handleKeyDown(e) {
|
|
438
|
+
if (e.key === "Backspace" && query === "" && value.length > 0) {
|
|
439
|
+
e.preventDefault();
|
|
440
|
+
removeOption(value[value.length - 1].id);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (e.key === "ArrowDown") {
|
|
444
|
+
e.preventDefault();
|
|
445
|
+
setHighlight((h) => Math.min(h + 1, Math.max(0, matches.length - 1)));
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (e.key === "ArrowUp") {
|
|
449
|
+
e.preventDefault();
|
|
450
|
+
setHighlight((h) => Math.max(h - 1, 0));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (e.key === "Enter") {
|
|
454
|
+
e.preventDefault();
|
|
455
|
+
const target = matches[highlight];
|
|
456
|
+
if (target) addOption(target);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (e.key === "Escape") {
|
|
460
|
+
if (query !== "") {
|
|
461
|
+
e.preventDefault();
|
|
462
|
+
setQuery("");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return /* @__PURE__ */ jsxs6("div", { className: "relative", children: [
|
|
467
|
+
/* @__PURE__ */ jsxs6(
|
|
468
|
+
"div",
|
|
469
|
+
{
|
|
470
|
+
ref: wrapperRef,
|
|
471
|
+
className: "bg-bg-inset border border-border-default hover:border-border-strong focus-within:border-border-focus focus-within:hover:border-border-focus rounded-lg px-3 py-1.5 flex flex-wrap items-center gap-1.5 transition-colors min-h-[38px] cursor-text signal:transition-shadow signal:focus-within:shadow-focus-ring",
|
|
472
|
+
onClick: () => inputRef.current?.focus(),
|
|
473
|
+
children: [
|
|
474
|
+
value.map((o) => /* @__PURE__ */ jsx10(InputChip, { label: o.label, leading: chipLeading?.(o), onRemove: () => removeOption(o.id) }, o.id)),
|
|
475
|
+
/* @__PURE__ */ jsx10(
|
|
476
|
+
"input",
|
|
477
|
+
{
|
|
478
|
+
ref: inputRef,
|
|
479
|
+
autoFocus,
|
|
480
|
+
type: "text",
|
|
481
|
+
value: query,
|
|
482
|
+
onChange: (e) => setQuery(e.target.value),
|
|
483
|
+
onKeyDown: handleKeyDown,
|
|
484
|
+
onFocus: () => setIsFocused(true),
|
|
485
|
+
onBlur: () => {
|
|
486
|
+
setTimeout(() => setIsFocused(false), 150);
|
|
487
|
+
},
|
|
488
|
+
placeholder: value.length === 0 ? placeholder : "",
|
|
489
|
+
className: "flex-1 min-w-[120px] bg-transparent text-body-2 text-text-primary placeholder:text-text-muted outline-none border-none"
|
|
490
|
+
}
|
|
491
|
+
)
|
|
492
|
+
]
|
|
493
|
+
}
|
|
494
|
+
),
|
|
495
|
+
showDropdown && anchorRect && createPortal2(
|
|
496
|
+
/* @__PURE__ */ jsx10(
|
|
497
|
+
"div",
|
|
498
|
+
{
|
|
499
|
+
className: "fixed z-[60] max-h-[240px] overflow-y-auto bg-bg-elevated border border-border-default rounded-lg shadow-lg",
|
|
500
|
+
style: {
|
|
501
|
+
top: anchorRect.bottom + 4,
|
|
502
|
+
left: anchorRect.left,
|
|
503
|
+
width: anchorRect.width
|
|
504
|
+
},
|
|
505
|
+
children: matches.map((o, i) => /* @__PURE__ */ jsx10(
|
|
506
|
+
MenuItem,
|
|
507
|
+
{
|
|
508
|
+
size: "tall",
|
|
509
|
+
className: "h-12 rounded-none",
|
|
510
|
+
leading: rowLeading?.(o),
|
|
511
|
+
label: o.label,
|
|
512
|
+
description: o.description,
|
|
513
|
+
selected: i === highlight,
|
|
514
|
+
onMouseEnter: () => setHighlight(i),
|
|
515
|
+
onMouseDown: (e) => {
|
|
516
|
+
e.preventDefault();
|
|
517
|
+
addOption(o);
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
o.id
|
|
521
|
+
))
|
|
522
|
+
}
|
|
523
|
+
),
|
|
524
|
+
document.body
|
|
525
|
+
)
|
|
526
|
+
] });
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/Tooltip.tsx
|
|
530
|
+
import { useCallback, useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState3 } from "react";
|
|
531
|
+
import { createPortal as createPortal3 } from "react-dom";
|
|
532
|
+
import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
103
533
|
function Tooltip({ label, className }) {
|
|
104
|
-
return /* @__PURE__ */
|
|
534
|
+
return /* @__PURE__ */ jsx11("div", { role: "tooltip", className: cn("bg-bg-elevated border border-border-default rounded-lg h-[30px] flex items-center justify-center px-2 shadow-lg", className), children: /* @__PURE__ */ jsx11("span", { className: "text-caption text-text-primary whitespace-nowrap", children: label }) });
|
|
105
535
|
}
|
|
106
536
|
var GAP = 6;
|
|
107
537
|
var VIEWPORT_PAD = 8;
|
|
108
|
-
function WithTooltip({ label, placement = "top", children }) {
|
|
109
|
-
const [show, setShow] =
|
|
110
|
-
const ref =
|
|
111
|
-
const tooltipRef =
|
|
112
|
-
const [style, setStyle] =
|
|
538
|
+
function WithTooltip({ label, placement = "top", wrapperClassName, children }) {
|
|
539
|
+
const [show, setShow] = useState3(false);
|
|
540
|
+
const ref = useRef3(null);
|
|
541
|
+
const tooltipRef = useRef3(null);
|
|
542
|
+
const [style, setStyle] = useState3({ position: "fixed", zIndex: 9999, pointerEvents: "none", visibility: "hidden" });
|
|
113
543
|
const reposition = useCallback(() => {
|
|
114
544
|
const trigger = ref.current;
|
|
115
545
|
const tip = tooltipRef.current;
|
|
@@ -121,20 +551,20 @@ function WithTooltip({ label, placement = "top", children }) {
|
|
|
121
551
|
left = Math.max(VIEWPORT_PAD, Math.min(left, window.innerWidth - tipRect.width - VIEWPORT_PAD));
|
|
122
552
|
setStyle({ position: "fixed", top, left, zIndex: 9999, pointerEvents: "none", visibility: "visible" });
|
|
123
553
|
}, [placement]);
|
|
124
|
-
|
|
554
|
+
useLayoutEffect2(() => {
|
|
125
555
|
if (show) reposition();
|
|
126
556
|
}, [show, reposition]);
|
|
127
|
-
return /* @__PURE__ */
|
|
557
|
+
return /* @__PURE__ */ jsxs7("div", { ref, className: cn("inline-flex shrink-0", wrapperClassName), onMouseEnter: () => setShow(true), onMouseLeave: () => setShow(false), children: [
|
|
128
558
|
children,
|
|
129
|
-
show &&
|
|
130
|
-
/* @__PURE__ */
|
|
559
|
+
show && createPortal3(
|
|
560
|
+
/* @__PURE__ */ jsx11("div", { ref: tooltipRef, style, children: /* @__PURE__ */ jsx11(Tooltip, { label }) }),
|
|
131
561
|
document.body
|
|
132
562
|
)
|
|
133
563
|
] });
|
|
134
564
|
}
|
|
135
565
|
|
|
136
566
|
// src/IconButton.tsx
|
|
137
|
-
import { jsx as
|
|
567
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
138
568
|
function IconButton({
|
|
139
569
|
variant = "muted",
|
|
140
570
|
className,
|
|
@@ -145,7 +575,7 @@ function IconButton({
|
|
|
145
575
|
type = "button",
|
|
146
576
|
...props
|
|
147
577
|
}) {
|
|
148
|
-
const button = /* @__PURE__ */
|
|
578
|
+
const button = /* @__PURE__ */ jsx12(
|
|
149
579
|
"button",
|
|
150
580
|
{
|
|
151
581
|
type,
|
|
@@ -166,36 +596,198 @@ function IconButton({
|
|
|
166
596
|
}
|
|
167
597
|
);
|
|
168
598
|
if (tooltip) {
|
|
169
|
-
return /* @__PURE__ */
|
|
599
|
+
return /* @__PURE__ */ jsx12(WithTooltip, { label: tooltip, placement: tooltipPlacement, children: button });
|
|
170
600
|
}
|
|
171
601
|
return button;
|
|
172
602
|
}
|
|
173
603
|
|
|
604
|
+
// src/IdentityMenu.tsx
|
|
605
|
+
import { useState as useState4 } from "react";
|
|
606
|
+
|
|
607
|
+
// src/Divider.tsx
|
|
608
|
+
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
609
|
+
function Divider({ orientation = "horizontal", className }) {
|
|
610
|
+
return /* @__PURE__ */ jsx13(
|
|
611
|
+
"div",
|
|
612
|
+
{
|
|
613
|
+
role: "separator",
|
|
614
|
+
"aria-orientation": orientation,
|
|
615
|
+
className: cn("bg-border-subtle", orientation === "horizontal" ? "h-px mx-3" : "w-px self-stretch shrink-0", className)
|
|
616
|
+
}
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/Person.tsx
|
|
621
|
+
import { jsx as jsx14, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
622
|
+
function Person({ name, picture, fallback = "\u2014", size = 20, className }) {
|
|
623
|
+
return (
|
|
624
|
+
// gap-2: the face-to-name distance is one rule (8px) wherever a person
|
|
625
|
+
// appears — Katerina, 2026-09-01, set on Person so PersonTrigger and every
|
|
626
|
+
// row agree by construction.
|
|
627
|
+
/* @__PURE__ */ jsxs8("span", { className: cn("flex min-w-0 items-center gap-2", className), children: [
|
|
628
|
+
/* @__PURE__ */ jsx14(Avatar, { name, src: picture, size }),
|
|
629
|
+
/* @__PURE__ */ jsx14("span", { className: cn("truncate", !name && "text-text-muted"), children: name ?? fallback })
|
|
630
|
+
] })
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/PersonTrigger.tsx
|
|
635
|
+
import { IconChevronDown } from "@tabler/icons-react";
|
|
636
|
+
import { jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
637
|
+
function PersonTrigger({ name, picture, fallback, size, open = false, compact = false, className, ...props }) {
|
|
638
|
+
if (compact) {
|
|
639
|
+
return /* @__PURE__ */ jsx15(
|
|
640
|
+
"button",
|
|
641
|
+
{
|
|
642
|
+
type: "button",
|
|
643
|
+
"aria-haspopup": "menu",
|
|
644
|
+
"aria-expanded": open,
|
|
645
|
+
className: cn("cursor-pointer rounded-full focus:outline-none", className),
|
|
646
|
+
...props,
|
|
647
|
+
children: /* @__PURE__ */ jsx15(Avatar, { name, src: picture, size: size ?? 36 })
|
|
648
|
+
}
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
return /* @__PURE__ */ jsxs9(
|
|
652
|
+
"button",
|
|
653
|
+
{
|
|
654
|
+
type: "button",
|
|
655
|
+
"aria-haspopup": "menu",
|
|
656
|
+
"aria-expanded": open,
|
|
657
|
+
className: cn(
|
|
658
|
+
// The size is an arbitrary value (the body-2 token): this list goes through
|
|
659
|
+
// cn(), and tw-merge silently drops a custom text-{size} once a
|
|
660
|
+
// text-{colour} follows it. Measured: the name rendered 16px.
|
|
661
|
+
"flex h-8 cursor-pointer items-center gap-1.5 rounded-md pl-1.5 pr-1.5 text-[14px] leading-[140%] text-text-primary transition-colors hover:bg-bg-hover",
|
|
662
|
+
open && "bg-bg-hover",
|
|
663
|
+
className
|
|
664
|
+
),
|
|
665
|
+
...props,
|
|
666
|
+
children: [
|
|
667
|
+
/* @__PURE__ */ jsx15(Person, { name, picture, fallback, size: size ?? 22 }),
|
|
668
|
+
/* @__PURE__ */ jsx15(IconChevronDown, { size: 14, stroke: 1.5, className: "shrink-0 text-text-muted" })
|
|
669
|
+
]
|
|
670
|
+
}
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/IdentityMenu.tsx
|
|
675
|
+
import { Fragment, jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
676
|
+
function IdentityPanel({ me, signedIn, relayUrl, idBase, onCopyKey, onSignOut, onClose, className, children }) {
|
|
677
|
+
const act = (action) => () => {
|
|
678
|
+
onClose();
|
|
679
|
+
action();
|
|
680
|
+
};
|
|
681
|
+
const appRows = typeof children === "function" ? children(onClose) : children;
|
|
682
|
+
return /* @__PURE__ */ jsxs10(Menu, { onClose, className: cn("w-72", className), children: [
|
|
683
|
+
/* @__PURE__ */ jsxs10(MenuSection, { label: signedIn ? "Signed in as" : "Acting as", children: [
|
|
684
|
+
/* @__PURE__ */ jsx16(MenuRow, { children: /* @__PURE__ */ jsx16(Person, { name: me.name, picture: me.picture, fallback: "Anonymous", size: 28, className: "text-body-2-strong" }) }),
|
|
685
|
+
/* @__PURE__ */ jsx16(Note, { children: signedIn ? "Your Estiva ID. The same person you are in every Estiva app." : "A key held by this browser only. Nobody else knows who you are." }),
|
|
686
|
+
me.email && /* @__PURE__ */ jsxs10(Note, { children: [
|
|
687
|
+
me.email,
|
|
688
|
+
" \xB7 known to Estiva, never published to the relay"
|
|
689
|
+
] })
|
|
690
|
+
] }),
|
|
691
|
+
relayUrl && /* @__PURE__ */ jsxs10(Fragment, { children: [
|
|
692
|
+
/* @__PURE__ */ jsx16(Divider, { className: "mx-0 my-2" }),
|
|
693
|
+
/* @__PURE__ */ jsxs10(MenuSection, { label: "Workspace", children: [
|
|
694
|
+
/* @__PURE__ */ jsx16(MenuRow, { children: /* @__PURE__ */ jsx16("span", { className: "break-all font-mono text-caption text-text-primary", children: relayUrl }) }),
|
|
695
|
+
/* @__PURE__ */ jsx16(Note, { children: "Everyone on this relay shares this workspace, in every app." })
|
|
696
|
+
] })
|
|
697
|
+
] }),
|
|
698
|
+
appRows && /* @__PURE__ */ jsxs10(Fragment, { children: [
|
|
699
|
+
/* @__PURE__ */ jsx16(Divider, { className: "mx-0 my-2" }),
|
|
700
|
+
appRows
|
|
701
|
+
] }),
|
|
702
|
+
signedIn && idBase || onCopyKey || idBase && onSignOut ? /* @__PURE__ */ jsx16(Divider, { className: "mx-0 my-2" }) : null,
|
|
703
|
+
signedIn && idBase && /* @__PURE__ */ jsx16(
|
|
704
|
+
MenuItem,
|
|
705
|
+
{
|
|
706
|
+
label: "Edit your profile in Estiva ID",
|
|
707
|
+
onClick: act(() => window.open(idBase, "_blank", "noopener,noreferrer"))
|
|
708
|
+
}
|
|
709
|
+
),
|
|
710
|
+
onCopyKey && /* @__PURE__ */ jsx16(MenuItem, { label: "Copy public key", onClick: act(onCopyKey) }),
|
|
711
|
+
idBase && onSignOut && /* @__PURE__ */ jsx16(MenuItem, { label: "Sign out", onClick: act(onSignOut) })
|
|
712
|
+
] });
|
|
713
|
+
}
|
|
714
|
+
function IdentityMenu({ me, signedIn, relayUrl, idBase, onCopyKey, onSignOut, compact = false, className, children }) {
|
|
715
|
+
const [open, setOpen] = useState4(false);
|
|
716
|
+
return /* @__PURE__ */ jsxs10("div", { className: cn("relative", className), children: [
|
|
717
|
+
/* @__PURE__ */ jsx16(
|
|
718
|
+
PersonTrigger,
|
|
719
|
+
{
|
|
720
|
+
name: me.name,
|
|
721
|
+
picture: me.picture,
|
|
722
|
+
fallback: "Anonymous",
|
|
723
|
+
compact,
|
|
724
|
+
size: compact ? 36 : void 0,
|
|
725
|
+
open,
|
|
726
|
+
onMouseDown: (event) => event.stopPropagation(),
|
|
727
|
+
onClick: () => setOpen((value) => !value),
|
|
728
|
+
"aria-label": compact ? "Account menu" : void 0
|
|
729
|
+
}
|
|
730
|
+
),
|
|
731
|
+
open && /* @__PURE__ */ jsx16(
|
|
732
|
+
IdentityPanel,
|
|
733
|
+
{
|
|
734
|
+
me,
|
|
735
|
+
signedIn,
|
|
736
|
+
relayUrl,
|
|
737
|
+
idBase,
|
|
738
|
+
onCopyKey,
|
|
739
|
+
onSignOut,
|
|
740
|
+
onClose: () => setOpen(false),
|
|
741
|
+
children
|
|
742
|
+
}
|
|
743
|
+
)
|
|
744
|
+
] });
|
|
745
|
+
}
|
|
746
|
+
function Note({ children }) {
|
|
747
|
+
return /* @__PURE__ */ jsx16("span", { className: "px-2 pb-1.5 text-caption text-text-secondary", children });
|
|
748
|
+
}
|
|
749
|
+
|
|
174
750
|
// src/Field.tsx
|
|
175
|
-
import { jsx as
|
|
751
|
+
import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
176
752
|
function Field({ label, required = false, children }) {
|
|
177
|
-
return /* @__PURE__ */
|
|
178
|
-
/* @__PURE__ */
|
|
753
|
+
return /* @__PURE__ */ jsxs11("div", { className: "flex flex-col gap-2", children: [
|
|
754
|
+
/* @__PURE__ */ jsxs11("label", { className: `text-input-label text-text-primary${required ? " flex items-center" : ""}`, children: [
|
|
179
755
|
label,
|
|
180
|
-
required && /* @__PURE__ */
|
|
756
|
+
required && /* @__PURE__ */ jsx17("span", { className: "text-error-default ml-0.5", children: "*" })
|
|
181
757
|
] }),
|
|
182
758
|
children
|
|
183
759
|
] });
|
|
184
760
|
}
|
|
185
761
|
|
|
186
762
|
// src/Select.tsx
|
|
187
|
-
import { IconCheck, IconChevronDown } from "@tabler/icons-react";
|
|
188
|
-
import { useCallback as useCallback2, useEffect, useId, useMemo, useRef as
|
|
189
|
-
import { createPortal as
|
|
190
|
-
import { Fragment, jsx as
|
|
763
|
+
import { IconCheck as IconCheck2, IconChevronDown as IconChevronDown2 } from "@tabler/icons-react";
|
|
764
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useId, useLayoutEffect as useLayoutEffect3, useMemo as useMemo2, useRef as useRef4, useState as useState5 } from "react";
|
|
765
|
+
import { createPortal as createPortal4 } from "react-dom";
|
|
766
|
+
import { Fragment as Fragment2, jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
767
|
+
function fitMenu({
|
|
768
|
+
anchor,
|
|
769
|
+
menu,
|
|
770
|
+
viewport
|
|
771
|
+
}) {
|
|
772
|
+
const MARGIN = 8;
|
|
773
|
+
const GAP2 = 4;
|
|
774
|
+
const CAP = 288;
|
|
775
|
+
const left = Math.max(MARGIN, Math.min(anchor.left, viewport.width - menu.width - MARGIN));
|
|
776
|
+
const below = viewport.height - anchor.bottom - GAP2 - MARGIN;
|
|
777
|
+
const above = anchor.top - GAP2 - MARGIN;
|
|
778
|
+
const openUp = below < Math.min(menu.contentHeight, CAP) && above > below;
|
|
779
|
+
const maxHeight = Math.max(Math.min(CAP, openUp ? above : below), 120);
|
|
780
|
+
return openUp ? { left, bottom: viewport.height - anchor.top + GAP2, maxHeight } : { left, top: anchor.bottom + GAP2, maxHeight };
|
|
781
|
+
}
|
|
191
782
|
function Select({ value, onChange, options, size = "default", ariaLabel, placeholder = "Select\u2026", disabled, className }) {
|
|
192
783
|
const id = useId();
|
|
193
|
-
const triggerRef =
|
|
194
|
-
const menuRef =
|
|
195
|
-
const [rect, setRect] =
|
|
784
|
+
const triggerRef = useRef4(null);
|
|
785
|
+
const menuRef = useRef4(null);
|
|
786
|
+
const [rect, setRect] = useState5(null);
|
|
787
|
+
const [placement, setPlacement] = useState5(null);
|
|
196
788
|
const open = rect !== null;
|
|
197
|
-
const selectedIndex =
|
|
198
|
-
const [activeIndex, setActiveIndex] =
|
|
789
|
+
const selectedIndex = useMemo2(() => Math.max(0, options.findIndex((o) => o.value === value)), [options, value]);
|
|
790
|
+
const [activeIndex, setActiveIndex] = useState5(selectedIndex);
|
|
199
791
|
const close = useCallback2((refocus) => {
|
|
200
792
|
setRect(null);
|
|
201
793
|
if (refocus) triggerRef.current?.focus();
|
|
@@ -204,21 +796,42 @@ function Select({ value, onChange, options, size = "default", ariaLabel, placeho
|
|
|
204
796
|
setActiveIndex(selectedIndex);
|
|
205
797
|
setRect(triggerRef.current?.getBoundingClientRect() ?? null);
|
|
206
798
|
}, [selectedIndex]);
|
|
207
|
-
|
|
799
|
+
useLayoutEffect3(() => {
|
|
800
|
+
if (!rect || !menuRef.current) {
|
|
801
|
+
setPlacement(null);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
setPlacement(
|
|
805
|
+
fitMenu({
|
|
806
|
+
anchor: { left: rect.left, top: rect.top, bottom: rect.bottom },
|
|
807
|
+
menu: { width: menuRef.current.offsetWidth, contentHeight: menuRef.current.scrollHeight },
|
|
808
|
+
viewport: { width: window.innerWidth, height: window.innerHeight }
|
|
809
|
+
})
|
|
810
|
+
);
|
|
811
|
+
}, [rect, options.length]);
|
|
812
|
+
useEffect3(() => {
|
|
813
|
+
if (!open) return;
|
|
814
|
+
document.getElementById(`${id}-${activeIndex}`)?.scrollIntoView?.({ block: "nearest" });
|
|
815
|
+
}, [open, activeIndex, id]);
|
|
816
|
+
useEffect3(() => {
|
|
208
817
|
if (!open) return;
|
|
209
818
|
const onDown = (e) => {
|
|
210
819
|
if (menuRef.current?.contains(e.target)) return;
|
|
211
820
|
if (triggerRef.current?.contains(e.target)) return;
|
|
212
821
|
setRect(null);
|
|
213
822
|
};
|
|
214
|
-
const
|
|
823
|
+
const onResize = () => setRect(null);
|
|
824
|
+
const onScroll = (e) => {
|
|
825
|
+
if (e.target instanceof Node && menuRef.current?.contains(e.target)) return;
|
|
826
|
+
setRect(null);
|
|
827
|
+
};
|
|
215
828
|
document.addEventListener("mousedown", onDown);
|
|
216
|
-
window.addEventListener("resize",
|
|
217
|
-
window.addEventListener("scroll",
|
|
829
|
+
window.addEventListener("resize", onResize);
|
|
830
|
+
window.addEventListener("scroll", onScroll, true);
|
|
218
831
|
return () => {
|
|
219
832
|
document.removeEventListener("mousedown", onDown);
|
|
220
|
-
window.removeEventListener("resize",
|
|
221
|
-
window.removeEventListener("scroll",
|
|
833
|
+
window.removeEventListener("resize", onResize);
|
|
834
|
+
window.removeEventListener("scroll", onScroll, true);
|
|
222
835
|
};
|
|
223
836
|
}, [open]);
|
|
224
837
|
const pick = (index) => {
|
|
@@ -267,8 +880,8 @@ function Select({ value, onChange, options, size = "default", ariaLabel, placeho
|
|
|
267
880
|
}
|
|
268
881
|
};
|
|
269
882
|
const selected = options.find((o) => o.value === value);
|
|
270
|
-
return /* @__PURE__ */
|
|
271
|
-
/* @__PURE__ */
|
|
883
|
+
return /* @__PURE__ */ jsxs12(Fragment2, { children: [
|
|
884
|
+
/* @__PURE__ */ jsxs12(
|
|
272
885
|
"button",
|
|
273
886
|
{
|
|
274
887
|
ref: triggerRef,
|
|
@@ -280,9 +893,20 @@ function Select({ value, onChange, options, size = "default", ariaLabel, placeho
|
|
|
280
893
|
onClick: () => open ? close(false) : openMenu(),
|
|
281
894
|
onKeyDown,
|
|
282
895
|
className: cn(
|
|
283
|
-
|
|
896
|
+
/*
|
|
897
|
+
* `min-w-0 max-w-full`: a trigger must never outgrow its container
|
|
898
|
+
* (Katerina, 2026-09-01 — a long label stretched the files panel's
|
|
899
|
+
* Lead row past its card). In a flex row the default min-width:auto
|
|
900
|
+
* forbids shrinking below the label's width, which is what kept
|
|
901
|
+
* `truncate` from ever engaging; in a block container max-w-full is
|
|
902
|
+
* the cap. Full-width callers are unaffected.
|
|
903
|
+
*/
|
|
904
|
+
"flex w-full min-w-0 max-w-full items-center justify-between gap-2 rounded-lg border bg-bg-inset text-left",
|
|
284
905
|
"border-border-default text-text-primary outline-none transition-colors",
|
|
285
|
-
|
|
906
|
+
// The focused border survives a hover: hover alone strengthens the
|
|
907
|
+
// hairline, but hover while focused must not grey the focus colour —
|
|
908
|
+
// the stacked variant outranks plain hover by specificity.
|
|
909
|
+
"hover:border-border-strong focus-visible:hover:border-border-focus aria-expanded:hover:border-border-focus disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled",
|
|
286
910
|
"focus-visible:border-border-focus aria-expanded:border-border-focus",
|
|
287
911
|
"signal:transition-shadow signal:focus-visible:shadow-focus-ring",
|
|
288
912
|
size === "default" && "px-3 py-2 text-[14px] leading-[1.4] font-normal",
|
|
@@ -290,25 +914,30 @@ function Select({ value, onChange, options, size = "default", ariaLabel, placeho
|
|
|
290
914
|
className
|
|
291
915
|
),
|
|
292
916
|
children: [
|
|
293
|
-
/* @__PURE__ */
|
|
294
|
-
selected?.leading && /* @__PURE__ */
|
|
295
|
-
/* @__PURE__ */
|
|
917
|
+
/* @__PURE__ */ jsxs12("span", { className: cn("flex min-w-0 items-center gap-2", !selected && "text-text-muted"), children: [
|
|
918
|
+
selected?.leading && /* @__PURE__ */ jsx18("span", { className: "flex shrink-0 items-center", children: selected.leading }),
|
|
919
|
+
/* @__PURE__ */ jsx18("span", { className: "truncate", children: selected?.label ?? placeholder })
|
|
296
920
|
] }),
|
|
297
|
-
/* @__PURE__ */
|
|
921
|
+
/* @__PURE__ */ jsx18(IconChevronDown2, { size: size === "small" ? 14 : 16, stroke: 1.5, className: "shrink-0 text-text-secondary" })
|
|
298
922
|
]
|
|
299
923
|
}
|
|
300
924
|
),
|
|
301
|
-
open && rect &&
|
|
302
|
-
/* @__PURE__ */
|
|
925
|
+
open && rect && createPortal4(
|
|
926
|
+
/* @__PURE__ */ jsx18(
|
|
303
927
|
"div",
|
|
304
928
|
{
|
|
305
929
|
ref: menuRef,
|
|
306
930
|
role: "listbox",
|
|
307
931
|
"aria-activedescendant": `${id}-${activeIndex}`,
|
|
308
932
|
tabIndex: -1,
|
|
309
|
-
style:
|
|
310
|
-
|
|
311
|
-
|
|
933
|
+
style: placement ? { ...placement, minWidth: rect.width } : (
|
|
934
|
+
// Provisional frame: measured by the layout effect above,
|
|
935
|
+
// replaced before paint. Hidden so a cut-off position is
|
|
936
|
+
// never visible, not even for a frame.
|
|
937
|
+
{ top: rect.bottom + 4, left: rect.left, minWidth: rect.width, visibility: "hidden" }
|
|
938
|
+
),
|
|
939
|
+
className: "fixed z-50 overflow-y-auto rounded-lg border border-border-default bg-bg-elevated p-1 shadow-lg",
|
|
940
|
+
children: options.map((option, index) => /* @__PURE__ */ jsxs12(
|
|
312
941
|
"div",
|
|
313
942
|
{
|
|
314
943
|
id: `${id}-${index}`,
|
|
@@ -326,11 +955,11 @@ function Select({ value, onChange, options, size = "default", ariaLabel, placeho
|
|
|
326
955
|
option.value === value && "font-medium"
|
|
327
956
|
),
|
|
328
957
|
children: [
|
|
329
|
-
/* @__PURE__ */
|
|
330
|
-
option.leading && /* @__PURE__ */
|
|
331
|
-
/* @__PURE__ */
|
|
958
|
+
/* @__PURE__ */ jsxs12("span", { className: "flex min-w-0 items-center gap-2", children: [
|
|
959
|
+
option.leading && /* @__PURE__ */ jsx18("span", { className: "flex shrink-0 items-center", children: option.leading }),
|
|
960
|
+
/* @__PURE__ */ jsx18("span", { className: "truncate", children: option.label })
|
|
332
961
|
] }),
|
|
333
|
-
option.value === value && /* @__PURE__ */
|
|
962
|
+
option.value === value && /* @__PURE__ */ jsx18(IconCheck2, { size: 16, stroke: 1.5, className: "shrink-0 text-text-secondary" })
|
|
334
963
|
]
|
|
335
964
|
},
|
|
336
965
|
option.value
|
|
@@ -344,9 +973,9 @@ function Select({ value, onChange, options, size = "default", ariaLabel, placeho
|
|
|
344
973
|
|
|
345
974
|
// src/TextInput.tsx
|
|
346
975
|
import { forwardRef } from "react";
|
|
347
|
-
import { jsx as
|
|
976
|
+
import { jsx as jsx19 } from "react/jsx-runtime";
|
|
348
977
|
var TextInput = forwardRef(function TextInput2({ className, type = "text", ...props }, ref) {
|
|
349
|
-
return /* @__PURE__ */
|
|
978
|
+
return /* @__PURE__ */ jsx19(
|
|
350
979
|
"input",
|
|
351
980
|
{
|
|
352
981
|
ref,
|
|
@@ -366,9 +995,9 @@ var TextInput = forwardRef(function TextInput2({ className, type = "text", ...pr
|
|
|
366
995
|
|
|
367
996
|
// src/Textarea.tsx
|
|
368
997
|
import { forwardRef as forwardRef2 } from "react";
|
|
369
|
-
import { jsx as
|
|
998
|
+
import { jsx as jsx20 } from "react/jsx-runtime";
|
|
370
999
|
var Textarea = forwardRef2(function Textarea2({ className, ...props }, ref) {
|
|
371
|
-
return /* @__PURE__ */
|
|
1000
|
+
return /* @__PURE__ */ jsx20(
|
|
372
1001
|
"textarea",
|
|
373
1002
|
{
|
|
374
1003
|
ref,
|
|
@@ -385,23 +1014,26 @@ var Textarea = forwardRef2(function Textarea2({ className, ...props }, ref) {
|
|
|
385
1014
|
);
|
|
386
1015
|
});
|
|
387
1016
|
|
|
1017
|
+
// src/ConfirmDialog.tsx
|
|
1018
|
+
import { useState as useState6 } from "react";
|
|
1019
|
+
|
|
388
1020
|
// src/DialogShell.tsx
|
|
389
|
-
import { useEffect as
|
|
390
|
-
import { createPortal as
|
|
391
|
-
import { IconX } from "@tabler/icons-react";
|
|
392
|
-
import { Fragment as
|
|
393
|
-
function DialogShell({ title, onClose, footer, children, bodyClassName, width = 502 }) {
|
|
394
|
-
|
|
1021
|
+
import { useEffect as useEffect4 } from "react";
|
|
1022
|
+
import { createPortal as createPortal5 } from "react-dom";
|
|
1023
|
+
import { IconX as IconX2 } from "@tabler/icons-react";
|
|
1024
|
+
import { Fragment as Fragment3, jsx as jsx21, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
1025
|
+
function DialogShell({ title, onClose, headerContent, footer, children, bodyClassName, width = 502 }) {
|
|
1026
|
+
useEffect4(() => {
|
|
395
1027
|
const onKey = (event) => {
|
|
396
1028
|
if (event.key === "Escape") onClose();
|
|
397
1029
|
};
|
|
398
1030
|
document.addEventListener("keydown", onKey);
|
|
399
1031
|
return () => document.removeEventListener("keydown", onKey);
|
|
400
1032
|
}, [onClose]);
|
|
401
|
-
return
|
|
402
|
-
/* @__PURE__ */
|
|
403
|
-
/* @__PURE__ */
|
|
404
|
-
/* @__PURE__ */
|
|
1033
|
+
return createPortal5(
|
|
1034
|
+
/* @__PURE__ */ jsxs13(Fragment3, { children: [
|
|
1035
|
+
/* @__PURE__ */ jsx21("div", { className: "fixed inset-0 z-40 bg-black/50", onClick: onClose }),
|
|
1036
|
+
/* @__PURE__ */ jsx21("div", { className: "fixed inset-0 z-50 flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsxs13(
|
|
405
1037
|
"div",
|
|
406
1038
|
{
|
|
407
1039
|
role: "dialog",
|
|
@@ -410,12 +1042,12 @@ function DialogShell({ title, onClose, footer, children, bodyClassName, width =
|
|
|
410
1042
|
className: "bg-bg-elevated border border-border-subtle rounded-lg shadow-lg pointer-events-auto flex flex-col overflow-hidden",
|
|
411
1043
|
style: { width },
|
|
412
1044
|
children: [
|
|
413
|
-
/* @__PURE__ */
|
|
414
|
-
/* @__PURE__ */
|
|
415
|
-
/* @__PURE__ */
|
|
1045
|
+
/* @__PURE__ */ jsxs13("div", { className: "h-12 flex items-center justify-between pl-5 pr-4 border-b border-border-subtle shrink-0", children: [
|
|
1046
|
+
headerContent ?? /* @__PURE__ */ jsx21("span", { className: "text-h4 text-text-primary", children: title }),
|
|
1047
|
+
/* @__PURE__ */ jsx21(IconButton, { tooltip: "Close", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx21(IconX2, { size: 16, stroke: 1.5 }) })
|
|
416
1048
|
] }),
|
|
417
|
-
/* @__PURE__ */
|
|
418
|
-
/* @__PURE__ */
|
|
1049
|
+
/* @__PURE__ */ jsx21("div", { className: cn("pl-5 pr-4 py-4", footer != null && "border-b border-border-subtle", bodyClassName), children }),
|
|
1050
|
+
footer != null && /* @__PURE__ */ jsx21("div", { className: "h-12 flex items-center justify-end gap-2 pl-5 pr-4 shrink-0", children: footer })
|
|
419
1051
|
]
|
|
420
1052
|
}
|
|
421
1053
|
) })
|
|
@@ -424,63 +1056,579 @@ function DialogShell({ title, onClose, footer, children, bodyClassName, width =
|
|
|
424
1056
|
);
|
|
425
1057
|
}
|
|
426
1058
|
|
|
427
|
-
// src/
|
|
428
|
-
import { jsx as
|
|
429
|
-
function
|
|
430
|
-
|
|
431
|
-
|
|
1059
|
+
// src/ConfirmDialog.tsx
|
|
1060
|
+
import { Fragment as Fragment4, jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
1061
|
+
function ConfirmDialog({ title, children, confirmLabel, destructive = false, onConfirm, onClose }) {
|
|
1062
|
+
const [busy, setBusy] = useState6(false);
|
|
1063
|
+
const confirm = async () => {
|
|
1064
|
+
setBusy(true);
|
|
1065
|
+
try {
|
|
1066
|
+
const ok = await onConfirm();
|
|
1067
|
+
if (ok !== false) onClose();
|
|
1068
|
+
} finally {
|
|
1069
|
+
setBusy(false);
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
return /* @__PURE__ */ jsx22(
|
|
1073
|
+
DialogShell,
|
|
432
1074
|
{
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
1075
|
+
title,
|
|
1076
|
+
onClose,
|
|
1077
|
+
bodyClassName: "flex flex-col gap-3 text-body-2 text-text-primary",
|
|
1078
|
+
footer: /* @__PURE__ */ jsxs14(Fragment4, { children: [
|
|
1079
|
+
/* @__PURE__ */ jsx22(Button, { variant: "muted", onClick: onClose, disabled: busy, children: "Cancel" }),
|
|
1080
|
+
/* @__PURE__ */ jsx22(Button, { variant: destructive ? "destructive" : "primary", onClick: () => void confirm(), disabled: busy, children: confirmLabel })
|
|
1081
|
+
] }),
|
|
1082
|
+
children
|
|
1083
|
+
}
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// src/EditableText.tsx
|
|
1088
|
+
import { useEffect as useEffect5, useRef as useRef5, useState as useState7 } from "react";
|
|
1089
|
+
import { jsx as jsx23 } from "react/jsx-runtime";
|
|
1090
|
+
function EditableText({ value, display, displayNode, placeholder, onCommit, multiline = false, className, label, readOnly = false }) {
|
|
1091
|
+
const [editing, setEditing] = useState7(false);
|
|
1092
|
+
const [draft, setDraft] = useState7(value);
|
|
1093
|
+
const [busy, setBusy] = useState7(false);
|
|
1094
|
+
const fieldRef = useRef5(null);
|
|
1095
|
+
useEffect5(() => {
|
|
1096
|
+
if (editing) {
|
|
1097
|
+
fieldRef.current?.focus();
|
|
1098
|
+
fieldRef.current?.select();
|
|
1099
|
+
}
|
|
1100
|
+
}, [editing]);
|
|
1101
|
+
const open = () => {
|
|
1102
|
+
setDraft(value);
|
|
1103
|
+
setEditing(true);
|
|
1104
|
+
};
|
|
1105
|
+
const cancel = () => {
|
|
1106
|
+
setEditing(false);
|
|
1107
|
+
setDraft(value);
|
|
1108
|
+
};
|
|
1109
|
+
const commit = async () => {
|
|
1110
|
+
if (busy) return;
|
|
1111
|
+
const next = draft.trim();
|
|
1112
|
+
if (next === value) {
|
|
1113
|
+
setEditing(false);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
setBusy(true);
|
|
1117
|
+
try {
|
|
1118
|
+
const ok = await onCommit(next);
|
|
1119
|
+
if (ok) setEditing(false);
|
|
1120
|
+
} catch {
|
|
1121
|
+
} finally {
|
|
1122
|
+
setBusy(false);
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
const onKeyDown = (event) => {
|
|
1126
|
+
if (event.key === "Escape") {
|
|
1127
|
+
event.preventDefault();
|
|
1128
|
+
cancel();
|
|
1129
|
+
} else if (event.key === "Enter" && !(multiline && event.shiftKey)) {
|
|
1130
|
+
event.preventDefault();
|
|
1131
|
+
void commit();
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
const fieldClass = cn(
|
|
1135
|
+
"w-full rounded-md border border-border-strong bg-bg-inset px-2 py-1 text-text-primary outline-none",
|
|
1136
|
+
multiline && "resize-none",
|
|
1137
|
+
className
|
|
1138
|
+
);
|
|
1139
|
+
if (readOnly) {
|
|
1140
|
+
return /* @__PURE__ */ jsx23(
|
|
1141
|
+
"div",
|
|
1142
|
+
{
|
|
1143
|
+
"aria-label": label,
|
|
1144
|
+
className: cn("w-full px-2 py-1", multiline && !displayNode && "whitespace-pre-wrap", display ?? value ? "text-text-primary" : "text-text-muted", className),
|
|
1145
|
+
children: display ?? value ? displayNode ?? (display ?? value) : placeholder
|
|
1146
|
+
}
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
if (editing) {
|
|
1150
|
+
return multiline ? /* @__PURE__ */ jsx23(
|
|
1151
|
+
"textarea",
|
|
1152
|
+
{
|
|
1153
|
+
ref: fieldRef,
|
|
1154
|
+
"aria-label": label,
|
|
1155
|
+
value: draft,
|
|
1156
|
+
rows: 4,
|
|
1157
|
+
disabled: busy,
|
|
1158
|
+
onChange: (event) => setDraft(event.target.value),
|
|
1159
|
+
onKeyDown,
|
|
1160
|
+
onBlur: () => void commit(),
|
|
1161
|
+
className: fieldClass
|
|
1162
|
+
}
|
|
1163
|
+
) : /* @__PURE__ */ jsx23(
|
|
1164
|
+
"input",
|
|
1165
|
+
{
|
|
1166
|
+
ref: fieldRef,
|
|
1167
|
+
"aria-label": label,
|
|
1168
|
+
value: draft,
|
|
1169
|
+
disabled: busy,
|
|
1170
|
+
onChange: (event) => setDraft(event.target.value),
|
|
1171
|
+
onKeyDown,
|
|
1172
|
+
onBlur: () => void commit(),
|
|
1173
|
+
className: fieldClass
|
|
1174
|
+
}
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
return /* @__PURE__ */ jsx23(
|
|
1178
|
+
"button",
|
|
1179
|
+
{
|
|
1180
|
+
type: "button",
|
|
1181
|
+
onClick: open,
|
|
1182
|
+
title: "Click to edit",
|
|
1183
|
+
"aria-label": `Edit ${label.toLowerCase()}`,
|
|
1184
|
+
className: cn(
|
|
1185
|
+
"w-full rounded-md border border-transparent px-2 py-1 text-left transition-colors hover:border-border-default",
|
|
1186
|
+
multiline && !displayNode && "whitespace-pre-wrap",
|
|
1187
|
+
display ?? value ? "text-text-primary" : "text-text-muted",
|
|
1188
|
+
className
|
|
1189
|
+
),
|
|
1190
|
+
children: display ?? value ? displayNode ?? (display ?? value) : placeholder
|
|
436
1191
|
}
|
|
437
1192
|
);
|
|
438
1193
|
}
|
|
439
1194
|
|
|
440
1195
|
// src/EmptyState.tsx
|
|
441
1196
|
import { IconMessage2 } from "@tabler/icons-react";
|
|
442
|
-
import { jsx as
|
|
1197
|
+
import { jsx as jsx24, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
443
1198
|
function EmptyState({ icon, message, className }) {
|
|
444
|
-
return /* @__PURE__ */
|
|
445
|
-
/* @__PURE__ */
|
|
446
|
-
/* @__PURE__ */
|
|
1199
|
+
return /* @__PURE__ */ jsxs15("div", { className: cn("flex flex-col gap-2 items-center", className), children: [
|
|
1200
|
+
/* @__PURE__ */ jsx24("span", { className: "text-text-secondary", children: icon ?? /* @__PURE__ */ jsx24(IconMessage2, { size: 16, stroke: 1.5 }) }),
|
|
1201
|
+
/* @__PURE__ */ jsx24("p", { className: "text-body-2 text-text-secondary text-center", children: message })
|
|
447
1202
|
] });
|
|
448
1203
|
}
|
|
449
1204
|
|
|
450
1205
|
// src/Skeleton.tsx
|
|
451
|
-
import { jsx as
|
|
1206
|
+
import { jsx as jsx25, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
452
1207
|
function SkeletonBar({ className, style }) {
|
|
453
|
-
return /* @__PURE__ */
|
|
1208
|
+
return /* @__PURE__ */ jsx25("div", { className: cn("bg-bg-inset rounded-sm animate-pulse", className), style });
|
|
454
1209
|
}
|
|
455
1210
|
var ROW_BAR_WIDTHS = [150, 100, 170, 120, 90, 140, 110, 160];
|
|
456
1211
|
function SkeletonRow({ barWidth = 130 }) {
|
|
457
|
-
return /* @__PURE__ */
|
|
458
|
-
/* @__PURE__ */
|
|
459
|
-
/* @__PURE__ */
|
|
1212
|
+
return /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-2 px-2 h-[32px] rounded-lg", children: [
|
|
1213
|
+
/* @__PURE__ */ jsx25(SkeletonBar, { className: "w-4 h-4 shrink-0" }),
|
|
1214
|
+
/* @__PURE__ */ jsx25(SkeletonBar, { className: "h-3.5", style: { width: barWidth } })
|
|
460
1215
|
] });
|
|
461
1216
|
}
|
|
462
1217
|
function SkeletonList({ rows = 8, className }) {
|
|
463
|
-
return /* @__PURE__ */
|
|
1218
|
+
return /* @__PURE__ */ jsx25("div", { className: cn("flex flex-col gap-0.5 animate-skeleton-in", className), "aria-hidden": true, children: Array.from({ length: rows }, (_, i) => /* @__PURE__ */ jsx25(SkeletonRow, { barWidth: ROW_BAR_WIDTHS[i % ROW_BAR_WIDTHS.length] }, i)) });
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// src/Breadcrumb.tsx
|
|
1222
|
+
import { Fragment as Fragment5, useCallback as useCallback3, useLayoutEffect as useLayoutEffect4, useRef as useRef6, useState as useState8 } from "react";
|
|
1223
|
+
import { jsx as jsx26, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
1224
|
+
function Breadcrumb({ items, className }) {
|
|
1225
|
+
const navRef = useRef6(null);
|
|
1226
|
+
const labelRefs = useRef6([]);
|
|
1227
|
+
const [truncated, setTruncated] = useState8(/* @__PURE__ */ new Set());
|
|
1228
|
+
const measure = useCallback3(() => {
|
|
1229
|
+
const next = /* @__PURE__ */ new Set();
|
|
1230
|
+
labelRefs.current.forEach((el, index) => {
|
|
1231
|
+
if (el && el.scrollWidth > el.clientWidth) next.add(index);
|
|
1232
|
+
});
|
|
1233
|
+
setTruncated((prev) => prev.size === next.size && [...next].every((i) => prev.has(i)) ? prev : next);
|
|
1234
|
+
}, []);
|
|
1235
|
+
useLayoutEffect4(() => {
|
|
1236
|
+
measure();
|
|
1237
|
+
const nav = navRef.current;
|
|
1238
|
+
if (!nav || typeof ResizeObserver === "undefined") return;
|
|
1239
|
+
const observer = new ResizeObserver(measure);
|
|
1240
|
+
observer.observe(nav);
|
|
1241
|
+
return () => observer.disconnect();
|
|
1242
|
+
}, [measure, items]);
|
|
1243
|
+
return /* @__PURE__ */ jsx26("nav", { ref: navRef, "aria-label": "Breadcrumb", className: cn("flex min-w-0 items-center gap-1.5 text-[14px] leading-[140%]", className), children: items.map((item, index) => {
|
|
1244
|
+
const last = index === items.length - 1;
|
|
1245
|
+
const text = cn(
|
|
1246
|
+
"truncate",
|
|
1247
|
+
// A mono crumb is a ref — the identity. It never gives up width to a
|
|
1248
|
+
// long name beside it (the LongName story always claimed "the ref
|
|
1249
|
+
// stays"; flexbox was squeezing it anyway until this line).
|
|
1250
|
+
item.mono && "shrink-0 font-mono text-[12px] leading-[120%]",
|
|
1251
|
+
item.muted || last && item.mono ? "text-text-muted" : last ? "text-text-primary" : "text-text-secondary"
|
|
1252
|
+
);
|
|
1253
|
+
const setLabelRef = (el) => {
|
|
1254
|
+
labelRefs.current[index] = el;
|
|
1255
|
+
};
|
|
1256
|
+
const crumb = item.href ? /* @__PURE__ */ jsx26("a", { ref: setLabelRef, href: item.href, className: cn(text, "hover:text-text-primary"), children: item.label }) : /* @__PURE__ */ jsx26("span", { ref: setLabelRef, className: text, "aria-current": last ? "page" : void 0, children: item.label });
|
|
1257
|
+
return /* @__PURE__ */ jsxs17(Fragment5, { children: [
|
|
1258
|
+
index > 0 && /* @__PURE__ */ jsx26("span", { "aria-hidden": "true", className: "shrink-0 text-text-muted", children: "/" }),
|
|
1259
|
+
truncated.has(index) ? (
|
|
1260
|
+
// `min-w-0 shrink` undoes the wrapper's own shrink-0 — the crumb
|
|
1261
|
+
// must keep truncating inside it, or wrapping it would widen the
|
|
1262
|
+
// trail and the tooltip would never be needed again.
|
|
1263
|
+
/* @__PURE__ */ jsx26(WithTooltip, { label: item.label, wrapperClassName: "min-w-0 shrink", children: crumb })
|
|
1264
|
+
) : crumb
|
|
1265
|
+
] }, `${item.label}-${index}`);
|
|
1266
|
+
}) });
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// src/NavItem.tsx
|
|
1270
|
+
import { jsx as jsx27, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
1271
|
+
function NavItem({ label, href, count, countLabel, active = false, icon, className, ...props }) {
|
|
1272
|
+
return /* @__PURE__ */ jsxs18(
|
|
1273
|
+
"a",
|
|
1274
|
+
{
|
|
1275
|
+
href,
|
|
1276
|
+
"aria-current": active ? "page" : void 0,
|
|
1277
|
+
className: cn(
|
|
1278
|
+
// shrink-0: the row keeps its 32px inside an overflowing flex column
|
|
1279
|
+
// — without it the list compresses instead of scrolling (the missing-
|
|
1280
|
+
// shrink-0 family; Katerina, 2026-09-02).
|
|
1281
|
+
"flex h-8 min-w-0 shrink-0 items-center gap-2 rounded-md px-2 text-body-2 transition-colors",
|
|
1282
|
+
active ? "bg-bg-active text-text-primary" : "text-text-secondary hover:bg-bg-hover hover:text-text-primary",
|
|
1283
|
+
className
|
|
1284
|
+
),
|
|
1285
|
+
...props,
|
|
1286
|
+
children: [
|
|
1287
|
+
icon && /* @__PURE__ */ jsx27("span", { className: "flex shrink-0 items-center", children: icon }),
|
|
1288
|
+
/* @__PURE__ */ jsx27("span", { className: "flex-1 truncate", children: label }),
|
|
1289
|
+
count ? /* @__PURE__ */ jsx27(WithTooltip, { label: countLabel ?? `${count} open`, children: /* @__PURE__ */ jsx27("span", { className: "shrink-0 font-mono text-caption tabular-nums text-text-muted", children: count }) }) : null
|
|
1290
|
+
]
|
|
1291
|
+
}
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// src/Rail.tsx
|
|
1296
|
+
import { jsx as jsx28 } from "react/jsx-runtime";
|
|
1297
|
+
function Rail({ "aria-label": ariaLabel = "Navigation", children, className }) {
|
|
1298
|
+
return /* @__PURE__ */ jsx28("nav", { "aria-label": ariaLabel, className: cn("flex w-16 shrink-0 flex-col items-start gap-2 px-2 py-3", className), children });
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// src/RailItem.tsx
|
|
1302
|
+
import { jsx as jsx29, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
1303
|
+
function RailItem({ href, icon, label, active = false, className, ...props }) {
|
|
1304
|
+
return /* @__PURE__ */ jsxs19(
|
|
1305
|
+
"a",
|
|
1306
|
+
{
|
|
1307
|
+
href,
|
|
1308
|
+
"aria-current": active ? "page" : void 0,
|
|
1309
|
+
className: cn("group flex w-full shrink-0 flex-col items-center gap-0.5 px-2 py-0.5", className),
|
|
1310
|
+
...props,
|
|
1311
|
+
children: [
|
|
1312
|
+
/* @__PURE__ */ jsx29(
|
|
1313
|
+
"div",
|
|
1314
|
+
{
|
|
1315
|
+
className: cn(
|
|
1316
|
+
"flex items-center justify-center rounded-lg p-2 transition-colors",
|
|
1317
|
+
active ? "bg-bg-selected" : "group-hover:bg-bg-hover"
|
|
1318
|
+
),
|
|
1319
|
+
children: /* @__PURE__ */ jsx29(
|
|
1320
|
+
"span",
|
|
1321
|
+
{
|
|
1322
|
+
className: cn(
|
|
1323
|
+
"flex items-center transition-colors",
|
|
1324
|
+
active ? "text-text-primary signal:text-[color:var(--text-interactive)]" : "text-text-secondary"
|
|
1325
|
+
),
|
|
1326
|
+
children: icon
|
|
1327
|
+
}
|
|
1328
|
+
)
|
|
1329
|
+
}
|
|
1330
|
+
),
|
|
1331
|
+
/* @__PURE__ */ jsx29("span", { className: cn("text-center text-[9px] font-medium leading-[115%]", active ? "text-text-primary" : "text-text-secondary"), children: label })
|
|
1332
|
+
]
|
|
1333
|
+
}
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// src/Sidebar.tsx
|
|
1338
|
+
import { jsx as jsx30 } from "react/jsx-runtime";
|
|
1339
|
+
function Sidebar({ "aria-label": ariaLabel = "Workspace", children, className }) {
|
|
1340
|
+
return /* @__PURE__ */ jsx30(
|
|
1341
|
+
"nav",
|
|
1342
|
+
{
|
|
1343
|
+
"aria-label": ariaLabel,
|
|
1344
|
+
className: cn(
|
|
1345
|
+
"flex w-60 shrink-0 flex-col gap-px overflow-y-auto border-r border-border-default bg-bg-surface px-2.5 py-3",
|
|
1346
|
+
className
|
|
1347
|
+
),
|
|
1348
|
+
children
|
|
1349
|
+
}
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// src/Property.tsx
|
|
1354
|
+
import { jsx as jsx31, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
1355
|
+
function Property({ label, layout = "row", children, className }) {
|
|
1356
|
+
if (layout === "stacked") {
|
|
1357
|
+
return (
|
|
1358
|
+
// gap-3 — the one label-above-content distance (Katerina, 2026-09-01):
|
|
1359
|
+
// Peek's topic-details sections already sit at 12px, Ship's rail was
|
|
1360
|
+
// 6px, and unifying means the bigger, calmer one.
|
|
1361
|
+
/* @__PURE__ */ jsxs20("div", { className: cn("flex flex-col gap-3", className), children: [
|
|
1362
|
+
/* @__PURE__ */ jsx31("span", { className: "text-menu uppercase tracking-[0.08em] text-text-secondary", children: label }),
|
|
1363
|
+
children
|
|
1364
|
+
] })
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
return /* @__PURE__ */ jsxs20("div", { className: cn("flex items-center gap-2", className), children: [
|
|
1368
|
+
/* @__PURE__ */ jsx31("span", { className: "w-[68px] shrink-0 text-caption text-text-secondary", children: label }),
|
|
1369
|
+
children
|
|
1370
|
+
] });
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// src/SearchInput.tsx
|
|
1374
|
+
import "react";
|
|
1375
|
+
import { jsx as jsx32, jsxs as jsxs21 } from "react/jsx-runtime";
|
|
1376
|
+
function SearchInput({ shortcut, className, placeholder = "Search\u2026", ...props }) {
|
|
1377
|
+
return /* @__PURE__ */ jsxs21(
|
|
1378
|
+
"div",
|
|
1379
|
+
{
|
|
1380
|
+
className: cn(
|
|
1381
|
+
"flex gap-2 items-center px-3 py-2 rounded-lg",
|
|
1382
|
+
"bg-bg-inset border border-border-default",
|
|
1383
|
+
"focus-within:border-border-strong transition-colors",
|
|
1384
|
+
className
|
|
1385
|
+
),
|
|
1386
|
+
children: [
|
|
1387
|
+
/* @__PURE__ */ jsx32(
|
|
1388
|
+
"input",
|
|
1389
|
+
{
|
|
1390
|
+
className: "flex-1 min-w-0 bg-transparent text-input-value text-text-primary placeholder:text-text-muted outline-none",
|
|
1391
|
+
placeholder,
|
|
1392
|
+
...props
|
|
1393
|
+
}
|
|
1394
|
+
),
|
|
1395
|
+
shortcut && /* @__PURE__ */ jsx32("div", { className: "flex items-center justify-center px-1 py-px rounded-sm bg-bg-inset border border-border-strong shrink-0 signal:bg-[rgba(255,255,255,.05)] signal:border-b-2", children: /* @__PURE__ */ jsx32("span", { className: "text-caption text-text-secondary whitespace-nowrap signal:font-mono signal:text-[10px]", children: shortcut }) })
|
|
1396
|
+
]
|
|
1397
|
+
}
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// src/SectionHeader.tsx
|
|
1402
|
+
import { useState as useState9 } from "react";
|
|
1403
|
+
import { IconChevronRight as IconChevronRight2 } from "@tabler/icons-react";
|
|
1404
|
+
import { jsx as jsx33, jsxs as jsxs22 } from "react/jsx-runtime";
|
|
1405
|
+
function SectionHeader({ title, chevron = false, isExpanded = true, onToggle, actions, showActions = "hover", className }) {
|
|
1406
|
+
const [isHovered, setIsHovered] = useState9(false);
|
|
1407
|
+
return /* @__PURE__ */ jsxs22(
|
|
1408
|
+
"div",
|
|
1409
|
+
{
|
|
1410
|
+
className: cn(
|
|
1411
|
+
"flex h-[32px] items-center justify-between rounded-lg px-2 transition-colors",
|
|
1412
|
+
isHovered && "bg-bg-hover",
|
|
1413
|
+
chevron && "cursor-pointer",
|
|
1414
|
+
className
|
|
1415
|
+
),
|
|
1416
|
+
onClick: chevron ? onToggle : void 0,
|
|
1417
|
+
onMouseEnter: () => setIsHovered(true),
|
|
1418
|
+
onMouseLeave: () => setIsHovered(false),
|
|
1419
|
+
children: [
|
|
1420
|
+
/* @__PURE__ */ jsxs22("div", { className: "flex shrink-0 items-center gap-1", children: [
|
|
1421
|
+
chevron && /* @__PURE__ */ jsx33(
|
|
1422
|
+
IconChevronRight2,
|
|
1423
|
+
{
|
|
1424
|
+
size: 12,
|
|
1425
|
+
stroke: 1.5,
|
|
1426
|
+
className: cn("text-text-secondary transition-transform duration-150", isExpanded && "rotate-90")
|
|
1427
|
+
}
|
|
1428
|
+
),
|
|
1429
|
+
/* @__PURE__ */ jsx33(SectionLabel, { children: title })
|
|
1430
|
+
] }),
|
|
1431
|
+
(showActions === "always" || isHovered) && actions && actions.length > 0 && /* @__PURE__ */ jsx33("div", { className: "flex items-center gap-1", children: actions.map((action) => /* @__PURE__ */ jsx33(
|
|
1432
|
+
IconButton,
|
|
1433
|
+
{
|
|
1434
|
+
tooltip: action.tooltip,
|
|
1435
|
+
"aria-label": action.tooltip,
|
|
1436
|
+
onClick: (event) => {
|
|
1437
|
+
event.stopPropagation();
|
|
1438
|
+
action.onClick();
|
|
1439
|
+
},
|
|
1440
|
+
children: action.icon
|
|
1441
|
+
},
|
|
1442
|
+
action.tooltip
|
|
1443
|
+
)) })
|
|
1444
|
+
]
|
|
1445
|
+
}
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// src/Tabs.tsx
|
|
1450
|
+
import { jsx as jsx34, jsxs as jsxs23 } from "react/jsx-runtime";
|
|
1451
|
+
function Tabs({ tabs, active, onChange, size = "default", className }) {
|
|
1452
|
+
return /* @__PURE__ */ jsx34("div", { role: "tablist", className: cn("flex items-center gap-2", className), children: tabs.map((tab) => /* @__PURE__ */ jsxs23(
|
|
1453
|
+
"button",
|
|
1454
|
+
{
|
|
1455
|
+
type: "button",
|
|
1456
|
+
role: "tab",
|
|
1457
|
+
"aria-selected": active === tab.id,
|
|
1458
|
+
onClick: () => onChange(tab.id),
|
|
1459
|
+
className: cn(
|
|
1460
|
+
"flex cursor-pointer items-center transition-colors",
|
|
1461
|
+
// Arbitrary sizes (the body-2 and caption tokens): the colour branch below
|
|
1462
|
+
// follows them through cn(), and tw-merge drops a custom text-{size}
|
|
1463
|
+
// once a text-{colour} lands after it. Measured: tabs rendered 16px.
|
|
1464
|
+
// gap: default is Ship's 6px; small keeps Peek's original 4px, or
|
|
1465
|
+
// "small is Peek's geometry" stops being true.
|
|
1466
|
+
size === "default" ? "gap-1.5 rounded-md px-2 py-1 text-[14px] leading-[140%]" : "gap-1 rounded px-1.5 py-0.5 text-[12px] leading-[120%]",
|
|
1467
|
+
active === tab.id ? "bg-bg-active text-text-primary" : "text-text-secondary hover:bg-bg-hover"
|
|
1468
|
+
),
|
|
1469
|
+
children: [
|
|
1470
|
+
tab.icon,
|
|
1471
|
+
/* @__PURE__ */ jsxs23("span", { className: "flex items-baseline", children: [
|
|
1472
|
+
tab.label,
|
|
1473
|
+
tab.count !== void 0 ? /* @__PURE__ */ jsx34("span", { className: "ml-2.5 font-mono text-caption tabular-nums text-text-secondary", children: tab.count }) : null
|
|
1474
|
+
] })
|
|
1475
|
+
]
|
|
1476
|
+
},
|
|
1477
|
+
tab.id
|
|
1478
|
+
)) });
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// src/Toast.tsx
|
|
1482
|
+
import { createContext, useCallback as useCallback4, useContext, useEffect as useEffect6, useMemo as useMemo3, useState as useState10 } from "react";
|
|
1483
|
+
import { createPortal as createPortal6 } from "react-dom";
|
|
1484
|
+
import { IconCircleCheck } from "@tabler/icons-react";
|
|
1485
|
+
import { jsx as jsx35, jsxs as jsxs24 } from "react/jsx-runtime";
|
|
1486
|
+
var SURFACE_BY_TYPE = {
|
|
1487
|
+
success: "bg-success-muted signal:bg-bg-inset signal:border signal:border-border-default signal:shadow-[shadow:var(--shadow-md)]",
|
|
1488
|
+
brand: "bg-accent-muted signal:bg-bg-inset signal:border signal:border-border-default signal:shadow-[shadow:var(--shadow-md)]",
|
|
1489
|
+
neutral: "bg-bg-inset border border-border-subtle signal:border-border-default signal:shadow-[shadow:var(--shadow-md)]"
|
|
1490
|
+
};
|
|
1491
|
+
var ICON_BY_TYPE = {
|
|
1492
|
+
success: "signal:text-success-default signal:drop-shadow-[0_0_5px_rgba(63,222,140,0.7)]",
|
|
1493
|
+
brand: "signal:text-text-interactive signal:drop-shadow-[0_0_5px_rgba(86,200,255,0.6)]",
|
|
1494
|
+
neutral: "signal:text-text-secondary"
|
|
1495
|
+
};
|
|
1496
|
+
var ACTION_BORDER_BY_TYPE = {
|
|
1497
|
+
success: "signal:border signal:border-border-default signal:hover:border-border-strong",
|
|
1498
|
+
brand: "signal:border signal:border-border-default signal:hover:border-border-strong",
|
|
1499
|
+
neutral: "border border-border-default"
|
|
1500
|
+
};
|
|
1501
|
+
function Toast({ label, type = "neutral", leadingIcon = true, actionLabel, onAction, className }) {
|
|
1502
|
+
const hasAction = !!(actionLabel && onAction);
|
|
1503
|
+
return /* @__PURE__ */ jsxs24(
|
|
1504
|
+
"div",
|
|
1505
|
+
{
|
|
1506
|
+
className: cn(
|
|
1507
|
+
"inline-flex items-center min-h-[32px] pl-2 py-1 rounded-lg shadow-lg",
|
|
1508
|
+
// Without an action the label needs real right padding; the action
|
|
1509
|
+
// button brings its own edge, so the tight pr-1 only applies there.
|
|
1510
|
+
hasAction ? "pr-1 gap-[46px]" : "pr-3",
|
|
1511
|
+
SURFACE_BY_TYPE[type],
|
|
1512
|
+
className
|
|
1513
|
+
),
|
|
1514
|
+
children: [
|
|
1515
|
+
/* @__PURE__ */ jsxs24("div", { className: "flex items-center gap-2 shrink-0", children: [
|
|
1516
|
+
leadingIcon && /* @__PURE__ */ jsx35(IconCircleCheck, { size: 16, stroke: 1.5, className: cn("text-text-primary shrink-0", ICON_BY_TYPE[type]) }),
|
|
1517
|
+
/* @__PURE__ */ jsx35("span", { className: "font-normal text-[14px] leading-[1.4] text-text-primary whitespace-nowrap", children: label })
|
|
1518
|
+
] }),
|
|
1519
|
+
hasAction && /* @__PURE__ */ jsx35(
|
|
1520
|
+
"button",
|
|
1521
|
+
{
|
|
1522
|
+
type: "button",
|
|
1523
|
+
onClick: onAction,
|
|
1524
|
+
className: cn(
|
|
1525
|
+
"h-6 flex items-center justify-center gap-1 px-1 py-1 rounded-md shrink-0 transition-colors",
|
|
1526
|
+
ACTION_BORDER_BY_TYPE[type],
|
|
1527
|
+
type === "neutral" ? "hover:border-border-strong" : "hover:opacity-80"
|
|
1528
|
+
),
|
|
1529
|
+
children: /* @__PURE__ */ jsx35("span", { className: "font-medium text-[12px] leading-[12px] text-text-primary whitespace-nowrap", children: actionLabel })
|
|
1530
|
+
}
|
|
1531
|
+
)
|
|
1532
|
+
]
|
|
1533
|
+
}
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
var ToastContext = createContext(null);
|
|
1537
|
+
var toastSeq = 0;
|
|
1538
|
+
function ToastProvider({ children }) {
|
|
1539
|
+
const [toast, setToast] = useState10(null);
|
|
1540
|
+
const showToast = useCallback4((opts) => {
|
|
1541
|
+
setToast({ ...opts, id: ++toastSeq });
|
|
1542
|
+
}, []);
|
|
1543
|
+
const dismissToast = useCallback4(() => {
|
|
1544
|
+
setToast(null);
|
|
1545
|
+
}, []);
|
|
1546
|
+
useEffect6(() => {
|
|
1547
|
+
if (!toast) return;
|
|
1548
|
+
const duration = toast.durationMs ?? 5e3;
|
|
1549
|
+
if (duration <= 0) return;
|
|
1550
|
+
const timer = setTimeout(() => {
|
|
1551
|
+
setToast((current) => current?.id === toast.id ? null : current);
|
|
1552
|
+
}, duration);
|
|
1553
|
+
return () => clearTimeout(timer);
|
|
1554
|
+
}, [toast]);
|
|
1555
|
+
const value = useMemo3(() => ({ showToast, dismissToast }), [showToast, dismissToast]);
|
|
1556
|
+
return /* @__PURE__ */ jsxs24(ToastContext.Provider, { value, children: [
|
|
1557
|
+
children,
|
|
1558
|
+
toast && createPortal6(
|
|
1559
|
+
/* @__PURE__ */ jsx35("div", { className: "fixed bottom-4 left-4 z-[100] pointer-events-none", children: /* @__PURE__ */ jsx35(
|
|
1560
|
+
Toast,
|
|
1561
|
+
{
|
|
1562
|
+
className: "pointer-events-auto",
|
|
1563
|
+
label: toast.label,
|
|
1564
|
+
type: toast.type ?? "neutral",
|
|
1565
|
+
leadingIcon: toast.leadingIcon ?? true,
|
|
1566
|
+
actionLabel: toast.actionLabel,
|
|
1567
|
+
onAction: toast.onAction ? () => {
|
|
1568
|
+
toast.onAction?.();
|
|
1569
|
+
dismissToast();
|
|
1570
|
+
} : void 0
|
|
1571
|
+
}
|
|
1572
|
+
) }),
|
|
1573
|
+
document.body
|
|
1574
|
+
)
|
|
1575
|
+
] });
|
|
1576
|
+
}
|
|
1577
|
+
function useToast() {
|
|
1578
|
+
const ctx = useContext(ToastContext);
|
|
1579
|
+
if (!ctx) throw new Error("useToast must be used within ToastProvider");
|
|
1580
|
+
return ctx;
|
|
464
1581
|
}
|
|
465
1582
|
export {
|
|
1583
|
+
AppShell,
|
|
466
1584
|
Avatar,
|
|
1585
|
+
AvatarGroup,
|
|
1586
|
+
Banner,
|
|
1587
|
+
Breadcrumb,
|
|
467
1588
|
Button,
|
|
1589
|
+
Checkbox,
|
|
468
1590
|
Chip,
|
|
1591
|
+
ChipInput,
|
|
1592
|
+
ConfirmDialog,
|
|
469
1593
|
DialogShell,
|
|
470
1594
|
Divider,
|
|
1595
|
+
EditableText,
|
|
471
1596
|
EmptyState,
|
|
1597
|
+
EnterHint,
|
|
472
1598
|
Field,
|
|
473
1599
|
IconButton,
|
|
1600
|
+
IdentityMenu,
|
|
1601
|
+
IdentityPanel,
|
|
1602
|
+
InputChip,
|
|
1603
|
+
Menu,
|
|
1604
|
+
MenuItem,
|
|
1605
|
+
MenuRow,
|
|
1606
|
+
MenuSection,
|
|
1607
|
+
NavItem,
|
|
1608
|
+
Person,
|
|
1609
|
+
PersonTrigger,
|
|
1610
|
+
Property,
|
|
1611
|
+
Rail,
|
|
1612
|
+
RailItem,
|
|
1613
|
+
SearchInput,
|
|
1614
|
+
SectionHeader,
|
|
1615
|
+
SectionLabel,
|
|
474
1616
|
Select,
|
|
1617
|
+
Sidebar,
|
|
475
1618
|
SkeletonBar,
|
|
476
1619
|
SkeletonList,
|
|
477
1620
|
SkeletonRow,
|
|
1621
|
+
Tabs,
|
|
478
1622
|
TextInput,
|
|
479
1623
|
Textarea,
|
|
1624
|
+
Toast,
|
|
1625
|
+
ToastProvider,
|
|
480
1626
|
Tooltip,
|
|
1627
|
+
TopBar,
|
|
481
1628
|
WithTooltip,
|
|
482
1629
|
cn,
|
|
483
1630
|
hueFor,
|
|
484
|
-
initialsFor
|
|
1631
|
+
initialsFor,
|
|
1632
|
+
useToast
|
|
485
1633
|
};
|
|
486
1634
|
//# sourceMappingURL=index.js.map
|