@estiva-app/ui 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +12 -9
  2. package/base.css +43 -0
  3. package/dist/AppShell.d.ts +39 -0
  4. package/dist/AppShell.d.ts.map +1 -0
  5. package/dist/Avatar.d.ts +7 -2
  6. package/dist/Avatar.d.ts.map +1 -1
  7. package/dist/AvatarGroup.d.ts +23 -0
  8. package/dist/AvatarGroup.d.ts.map +1 -0
  9. package/dist/Banner.d.ts +19 -0
  10. package/dist/Banner.d.ts.map +1 -0
  11. package/dist/Breadcrumb.d.ts +28 -0
  12. package/dist/Breadcrumb.d.ts.map +1 -0
  13. package/dist/Checkbox.d.ts +18 -0
  14. package/dist/Checkbox.d.ts.map +1 -0
  15. package/dist/ChipInput.d.ts +57 -0
  16. package/dist/ChipInput.d.ts.map +1 -0
  17. package/dist/ConfirmDialog.d.ts +22 -0
  18. package/dist/ConfirmDialog.d.ts.map +1 -0
  19. package/dist/DialogShell.d.ts +9 -2
  20. package/dist/DialogShell.d.ts.map +1 -1
  21. package/dist/EditableText.d.ts +36 -0
  22. package/dist/EditableText.d.ts.map +1 -0
  23. package/dist/IdentityMenu.d.ts +58 -0
  24. package/dist/IdentityMenu.d.ts.map +1 -0
  25. package/dist/Menu.d.ts +82 -0
  26. package/dist/Menu.d.ts.map +1 -0
  27. package/dist/NavItem.d.ts +26 -0
  28. package/dist/NavItem.d.ts.map +1 -0
  29. package/dist/Person.d.ts +26 -0
  30. package/dist/Person.d.ts.map +1 -0
  31. package/dist/PersonTrigger.d.ts +23 -0
  32. package/dist/PersonTrigger.d.ts.map +1 -0
  33. package/dist/Property.d.ts +24 -0
  34. package/dist/Property.d.ts.map +1 -0
  35. package/dist/Rail.d.ts +18 -0
  36. package/dist/Rail.d.ts.map +1 -0
  37. package/dist/RailItem.d.ts +23 -0
  38. package/dist/RailItem.d.ts.map +1 -0
  39. package/dist/SearchInput.d.ts +21 -0
  40. package/dist/SearchInput.d.ts.map +1 -0
  41. package/dist/SectionHeader.d.ts +34 -0
  42. package/dist/SectionHeader.d.ts.map +1 -0
  43. package/dist/SectionLabel.d.ts +19 -0
  44. package/dist/SectionLabel.d.ts.map +1 -0
  45. package/dist/Select.d.ts +42 -5
  46. package/dist/Select.d.ts.map +1 -1
  47. package/dist/Sidebar.d.ts +18 -0
  48. package/dist/Sidebar.d.ts.map +1 -0
  49. package/dist/Tabs.d.ts +34 -0
  50. package/dist/Tabs.d.ts.map +1 -0
  51. package/dist/Toast.d.ts +48 -0
  52. package/dist/Toast.d.ts.map +1 -0
  53. package/dist/Tooltip.d.ts +3 -1
  54. package/dist/Tooltip.d.ts.map +1 -1
  55. package/dist/TopBar.d.ts +33 -0
  56. package/dist/TopBar.d.ts.map +1 -0
  57. package/dist/cn.d.ts +14 -4
  58. package/dist/cn.d.ts.map +1 -1
  59. package/dist/index.d.ts +23 -0
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +1254 -106
  62. package/dist/index.js.map +4 -4
  63. package/package.json +5 -2
  64. package/src/AppShell.mdx +66 -0
  65. package/src/AppShell.stories.tsx +117 -0
  66. package/src/AppShell.tsx +77 -0
  67. package/src/Avatar.initials.test.ts +21 -0
  68. package/src/Avatar.mdx +49 -0
  69. package/src/Avatar.tsx +12 -9
  70. package/src/AvatarGroup.mdx +39 -0
  71. package/src/AvatarGroup.stories.tsx +56 -0
  72. package/src/AvatarGroup.tsx +42 -0
  73. package/src/Banner.mdx +45 -0
  74. package/src/Banner.stories.tsx +35 -0
  75. package/src/Banner.tsx +35 -0
  76. package/src/Breadcrumb.mdx +52 -0
  77. package/src/Breadcrumb.stories.tsx +42 -0
  78. package/src/Breadcrumb.tsx +106 -0
  79. package/src/Button.mdx +57 -0
  80. package/src/Checkbox.mdx +44 -0
  81. package/src/Checkbox.stories.tsx +49 -0
  82. package/src/Checkbox.tsx +46 -0
  83. package/src/Chip.mdx +43 -0
  84. package/src/ChipInput.mdx +68 -0
  85. package/src/ChipInput.stories.tsx +99 -0
  86. package/src/ChipInput.tsx +247 -0
  87. package/src/ConfirmDialog.mdx +51 -0
  88. package/src/ConfirmDialog.stories.tsx +63 -0
  89. package/src/ConfirmDialog.tsx +55 -0
  90. package/src/DialogShell.mdx +68 -0
  91. package/src/DialogShell.tsx +12 -5
  92. package/src/Divider.mdx +41 -0
  93. package/src/EditableText.mdx +59 -0
  94. package/src/EditableText.stories.tsx +63 -0
  95. package/src/EditableText.tsx +150 -0
  96. package/src/EmptyState.mdx +41 -0
  97. package/src/Field.mdx +39 -0
  98. package/src/IconButton.mdx +47 -0
  99. package/src/IdentityMenu.mdx +62 -0
  100. package/src/IdentityMenu.stories.tsx +52 -0
  101. package/src/IdentityMenu.tsx +159 -0
  102. package/src/Menu.mdx +111 -0
  103. package/src/Menu.stories.tsx +66 -0
  104. package/src/Menu.tsx +172 -0
  105. package/src/MenuItem.mdx +55 -0
  106. package/src/MenuItem.stories.tsx +98 -0
  107. package/src/NavItem.mdx +53 -0
  108. package/src/NavItem.stories.tsx +36 -0
  109. package/src/NavItem.tsx +53 -0
  110. package/src/Person.mdx +44 -0
  111. package/src/Person.stories.tsx +43 -0
  112. package/src/Person.tsx +39 -0
  113. package/src/PersonTrigger.mdx +51 -0
  114. package/src/PersonTrigger.stories.tsx +30 -0
  115. package/src/PersonTrigger.tsx +62 -0
  116. package/src/Property.mdx +51 -0
  117. package/src/Property.stories.tsx +91 -0
  118. package/src/Property.tsx +44 -0
  119. package/src/Rail.tsx +26 -0
  120. package/src/RailItem.mdx +50 -0
  121. package/src/RailItem.stories.tsx +37 -0
  122. package/src/RailItem.tsx +53 -0
  123. package/src/SearchInput.mdx +39 -0
  124. package/src/SearchInput.stories.tsx +24 -0
  125. package/src/SearchInput.tsx +45 -0
  126. package/src/SectionHeader.mdx +52 -0
  127. package/src/SectionHeader.stories.tsx +66 -0
  128. package/src/SectionHeader.tsx +85 -0
  129. package/src/SectionLabel.mdx +42 -0
  130. package/src/SectionLabel.stories.tsx +31 -0
  131. package/src/SectionLabel.tsx +28 -0
  132. package/src/Select.fit.test.ts +97 -0
  133. package/src/Select.mdx +71 -0
  134. package/src/Select.stories.tsx +95 -0
  135. package/src/Select.tsx +123 -15
  136. package/src/Sidebar.mdx +56 -0
  137. package/src/Sidebar.stories.tsx +50 -0
  138. package/src/Sidebar.tsx +32 -0
  139. package/src/Skeleton.mdx +39 -0
  140. package/src/Tabs.mdx +53 -0
  141. package/src/Tabs.stories.tsx +91 -0
  142. package/src/Tabs.tsx +73 -0
  143. package/src/TextInput.mdx +39 -0
  144. package/src/Textarea.mdx +36 -0
  145. package/src/Toast.mdx +54 -0
  146. package/src/Toast.stories.tsx +87 -0
  147. package/src/Toast.tsx +169 -0
  148. package/src/Tooltip.mdx +51 -0
  149. package/src/Tooltip.tsx +4 -2
  150. package/src/TopBar.mdx +62 -0
  151. package/src/TopBar.stories.tsx +96 -0
  152. package/src/TopBar.tsx +57 -0
  153. package/src/cn.test.ts +27 -0
  154. package/src/cn.ts +25 -5
  155. package/src/index.ts +23 -0
  156. package/tailwind-preset.js +4 -3
  157. 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 { twMerge } from "tailwind-merge";
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 { jsx } from "react/jsx-runtime";
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) => name.split(" ").map((w) => w[0]).slice(0, 2).join("").toUpperCase();
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__ */ jsx("div", { className: cn("rounded-sm overflow-hidden shrink-0 bg-bg-inset", className), style: { width: size, height: size }, children: picture ? /* @__PURE__ */ jsx("img", { src: picture, alt: alt || name || "", className: "w-full h-full object-cover", onError: () => setBroken(true) }) : label ? /* @__PURE__ */ jsx(
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__ */ jsx("div", { className: "w-full h-full bg-accent-muted flex items-center justify-center text-text-muted", children: /* @__PURE__ */ jsx(IconUserFilled, { size: Math.round(size * 0.5) }) }) });
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__ */ jsxs(
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 jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
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__ */ jsxs2("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: [
93
- leadingIcon && /* @__PURE__ */ jsx2("span", { className: "flex size-3 shrink-0 items-center justify-center", children: leadingIcon }),
94
- label && /* @__PURE__ */ jsx2("span", { className: "text-chip whitespace-nowrap signal:font-mono signal:text-[10px] signal:font-semibold signal:tracking-[0.02em] signal:tabular-nums", children: label }),
95
- trailingIcon && /* @__PURE__ */ jsx2("span", { className: "flex size-3 shrink-0 items-center justify-center", children: trailingIcon })
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/Tooltip.tsx
100
- import { useCallback, useLayoutEffect, useRef, useState as useState2 } from "react";
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
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
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__ */ jsx3("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__ */ jsx3("span", { className: "text-caption text-text-primary whitespace-nowrap", children: label }) });
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] = useState2(false);
110
- const ref = useRef(null);
111
- const tooltipRef = useRef(null);
112
- const [style, setStyle] = useState2({ position: "fixed", zIndex: 9999, pointerEvents: "none", visibility: "hidden" });
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
- useLayoutEffect(() => {
554
+ useLayoutEffect2(() => {
125
555
  if (show) reposition();
126
556
  }, [show, reposition]);
127
- return /* @__PURE__ */ jsxs3("div", { ref, className: "inline-flex shrink-0", onMouseEnter: () => setShow(true), onMouseLeave: () => setShow(false), children: [
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 && createPortal(
130
- /* @__PURE__ */ jsx3("div", { ref: tooltipRef, style, children: /* @__PURE__ */ jsx3(Tooltip, { label }) }),
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 jsx4 } from "react/jsx-runtime";
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__ */ jsx4(
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__ */ jsx4(WithTooltip, { label: tooltip, placement: tooltipPlacement, children: button });
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 jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
751
+ import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
176
752
  function Field({ label, required = false, children }) {
177
- return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", children: [
178
- /* @__PURE__ */ jsxs4("label", { className: `text-input-label text-text-primary${required ? " flex items-center" : ""}`, children: [
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__ */ jsx5("span", { className: "text-error-default ml-0.5", children: "*" })
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 useRef2, useState as useState3 } from "react";
189
- import { createPortal as createPortal2 } from "react-dom";
190
- import { Fragment, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
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 = useRef2(null);
194
- const menuRef = useRef2(null);
195
- const [rect, setRect] = useState3(null);
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 = useMemo(() => Math.max(0, options.findIndex((o) => o.value === value)), [options, value]);
198
- const [activeIndex, setActiveIndex] = useState3(selectedIndex);
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
- useEffect(() => {
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 onMove = () => setRect(null);
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", onMove);
217
- window.addEventListener("scroll", onMove, true);
829
+ window.addEventListener("resize", onResize);
830
+ window.addEventListener("scroll", onScroll, true);
218
831
  return () => {
219
832
  document.removeEventListener("mousedown", onDown);
220
- window.removeEventListener("resize", onMove);
221
- window.removeEventListener("scroll", onMove, true);
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__ */ jsxs5(Fragment, { children: [
271
- /* @__PURE__ */ jsxs5(
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
- "flex w-full items-center justify-between gap-2 rounded-lg border bg-bg-inset text-left",
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
- "hover:border-border-strong disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled",
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__ */ jsxs5("span", { className: cn("flex min-w-0 items-center gap-2", !selected && "text-text-muted"), children: [
294
- selected?.leading && /* @__PURE__ */ jsx6("span", { className: "flex shrink-0 items-center", children: selected.leading }),
295
- /* @__PURE__ */ jsx6("span", { className: "truncate", children: selected?.label ?? placeholder })
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__ */ jsx6(IconChevronDown, { size: size === "small" ? 14 : 16, stroke: 1.5, className: "shrink-0 text-text-secondary" })
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 && createPortal2(
302
- /* @__PURE__ */ jsx6(
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: { top: rect.bottom + 4, left: rect.left, minWidth: rect.width },
310
- className: "fixed z-50 max-h-72 overflow-y-auto rounded-lg border border-border-default bg-bg-elevated p-1 shadow-lg",
311
- children: options.map((option, index) => /* @__PURE__ */ jsxs5(
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__ */ jsxs5("span", { className: "flex min-w-0 items-center gap-2", children: [
330
- option.leading && /* @__PURE__ */ jsx6("span", { className: "flex shrink-0 items-center", children: option.leading }),
331
- /* @__PURE__ */ jsx6("span", { className: "truncate", children: option.label })
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__ */ jsx6(IconCheck, { size: 16, stroke: 1.5, className: "shrink-0 text-text-secondary" })
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 jsx7 } from "react/jsx-runtime";
976
+ import { jsx as jsx19 } from "react/jsx-runtime";
348
977
  var TextInput = forwardRef(function TextInput2({ className, type = "text", ...props }, ref) {
349
- return /* @__PURE__ */ jsx7(
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 jsx8 } from "react/jsx-runtime";
998
+ import { jsx as jsx20 } from "react/jsx-runtime";
370
999
  var Textarea = forwardRef2(function Textarea2({ className, ...props }, ref) {
371
- return /* @__PURE__ */ jsx8(
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 useEffect2 } from "react";
390
- import { createPortal as createPortal3 } from "react-dom";
391
- import { IconX } from "@tabler/icons-react";
392
- import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
393
- function DialogShell({ title, onClose, footer, children, bodyClassName, width = 502 }) {
394
- useEffect2(() => {
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 createPortal3(
402
- /* @__PURE__ */ jsxs6(Fragment2, { children: [
403
- /* @__PURE__ */ jsx9("div", { className: "fixed inset-0 z-40 bg-black/50", onClick: onClose }),
404
- /* @__PURE__ */ jsx9("div", { className: "fixed inset-0 z-50 flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsxs6(
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__ */ jsxs6("div", { className: "h-12 flex items-center justify-between pl-5 pr-4 border-b border-border-subtle shrink-0", children: [
414
- /* @__PURE__ */ jsx9("span", { className: "text-h4 text-text-primary", children: title }),
415
- /* @__PURE__ */ jsx9(IconButton, { tooltip: "Close", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx9(IconX, { size: 16, stroke: 1.5 }) })
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__ */ jsx9("div", { className: cn("pl-5 pr-4 py-4 border-b border-border-subtle", bodyClassName), children }),
418
- /* @__PURE__ */ jsx9("div", { className: "h-12 flex items-center justify-end gap-2 pl-5 pr-4 shrink-0", children: footer })
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/Divider.tsx
428
- import { jsx as jsx10 } from "react/jsx-runtime";
429
- function Divider({ orientation = "horizontal", className }) {
430
- return /* @__PURE__ */ jsx10(
431
- "div",
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
- role: "separator",
434
- "aria-orientation": orientation,
435
- className: cn("bg-border-subtle", orientation === "horizontal" ? "h-px mx-3" : "w-px self-stretch shrink-0", className)
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, 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 && "whitespace-pre-wrap", display ?? value ? "text-text-primary" : "text-text-muted", className),
1145
+ children: (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 && "whitespace-pre-wrap",
1187
+ display ?? value ? "text-text-primary" : "text-text-muted",
1188
+ className
1189
+ ),
1190
+ children: (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 jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
1197
+ import { jsx as jsx24, jsxs as jsxs15 } from "react/jsx-runtime";
443
1198
  function EmptyState({ icon, message, className }) {
444
- return /* @__PURE__ */ jsxs7("div", { className: cn("flex flex-col gap-2 items-center", className), children: [
445
- /* @__PURE__ */ jsx11("span", { className: "text-text-secondary", children: icon ?? /* @__PURE__ */ jsx11(IconMessage2, { size: 16, stroke: 1.5 }) }),
446
- /* @__PURE__ */ jsx11("p", { className: "text-body-2 text-text-secondary text-center", children: message })
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 jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
1206
+ import { jsx as jsx25, jsxs as jsxs16 } from "react/jsx-runtime";
452
1207
  function SkeletonBar({ className, style }) {
453
- return /* @__PURE__ */ jsx12("div", { className: cn("bg-bg-inset rounded-sm animate-pulse", className), style });
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__ */ jsxs8("div", { className: "flex items-center gap-2 px-2 h-[32px] rounded-lg", children: [
458
- /* @__PURE__ */ jsx12(SkeletonBar, { className: "w-4 h-4 shrink-0" }),
459
- /* @__PURE__ */ jsx12(SkeletonBar, { className: "h-3.5", style: { width: barWidth } })
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__ */ jsx12("div", { className: cn("flex flex-col gap-0.5 animate-skeleton-in", className), "aria-hidden": true, children: Array.from({ length: rows }, (_, i) => /* @__PURE__ */ jsx12(SkeletonRow, { barWidth: ROW_BAR_WIDTHS[i % ROW_BAR_WIDTHS.length] }, i)) });
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