@tangle-network/sandbox-ui 0.113.4 → 0.113.6

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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Button,
3
3
  Logo
4
- } from "./chunk-2XPJ2XF7.js";
4
+ } from "./chunk-ANSC5AFC.js";
5
5
  import {
6
6
  moonshot_default
7
7
  } from "./chunk-RQZRXKMR.js";
@@ -227,7 +227,8 @@ function RailTooltip({ label, children, disabled, className }) {
227
227
  }
228
228
 
229
229
  // src/dashboard/app-sidebar.tsx
230
- import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
230
+ import { focusRing } from "@tangle-network/ui/utils";
231
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
231
232
  function DefaultLink({
232
233
  href,
233
234
  to,
@@ -286,7 +287,7 @@ function RailThemeToggle({ className }) {
286
287
  "flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors",
287
288
  MOTION_CONTROL,
288
289
  "hover:bg-[var(--accent-surface-soft)] hover:text-foreground",
289
- "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
290
+ focusRing,
290
291
  className
291
292
  ),
292
293
  children: isDark ? /* @__PURE__ */ jsx3(SunIcon, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx3(MoonIcon, { className: "h-4 w-4" })
@@ -320,7 +321,7 @@ function RailCollapseToggle({ collapsed, showLabel, onToggle, className }) {
320
321
  MOTION_CONTROL,
321
322
  showLabel ? "h-9 w-full justify-start gap-2.5 px-2.5" : "h-9 w-9 justify-center",
322
323
  "hover:bg-[var(--accent-surface-soft)] hover:text-foreground",
323
- "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
324
+ focusRing,
324
325
  className
325
326
  ),
326
327
  children: [
@@ -344,15 +345,22 @@ function PanelToggleButton({ collapsed, onToggle, className }) {
344
345
  "flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors",
345
346
  MOTION_CONTROL,
346
347
  "hover:bg-[var(--accent-surface-soft)] hover:text-foreground",
347
- "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
348
+ focusRing,
348
349
  className
349
350
  ),
350
351
  children: /* @__PURE__ */ jsx3(PanelLeft, { className: "h-[18px] w-[18px] shrink-0" })
351
352
  }
352
353
  );
353
354
  }
355
+ function isRenderable(node) {
356
+ return React3.Children.toArray(node).some(
357
+ (child) => React3.isValidElement(child) && child.type === React3.Fragment ? isRenderable(child.props.children) : true
358
+ );
359
+ }
354
360
  function RailHeader({ brand, brandHref, children, collapsed, onToggle, collapsible = true, LinkComponent, className }) {
355
361
  const Link = LinkComponent ?? DefaultLink;
362
+ const hasBrand = isRenderable(brand);
363
+ const hasChildren = isRenderable(children);
356
364
  const brandNode = brandHref !== void 0 ? /* @__PURE__ */ jsx3(
357
365
  Link,
358
366
  {
@@ -371,21 +379,50 @@ function RailHeader({ brand, brandHref, children, collapsed, onToggle, collapsib
371
379
  collapsed ? "justify-center px-2" : "gap-2 px-3",
372
380
  className
373
381
  ),
374
- children: collapsed ? collapsible ? /* @__PURE__ */ jsx3(RailTooltip, { label: "Expand sidebar", children: /* @__PURE__ */ jsxs2(
382
+ children: collapsed ? !hasBrand && hasChildren ? (
383
+ // No brand mark: `children` (SidebarLayout's `railHeaderContent`) is
384
+ // the only rail-top content the consumer gave us, so render it here
385
+ // too. Dropping it left an EMPTY header — the collapsed rail showed
386
+ // nothing at all where the product's mark belongs. With neither a
387
+ // brand nor children, the plain expand button below still owns the
388
+ // rail top so the rail stays expandable.
389
+ //
390
+ // Rendered outside the expand button on purpose: content that owns
391
+ // the rail top is routinely interactive itself (a mark that expands
392
+ // the rail on click), and a <button> inside a <button> is invalid
393
+ // HTML that React refuses to hydrate.
394
+ //
395
+ // The explicit "Expand sidebar" control stays in the tree for
396
+ // keyboard and assistive-tech users (the content may be a passive
397
+ // switcher, and the empty-rail click fallback is mouse-only). It sits
398
+ // over the content, invisible and non-interactive until it receives
399
+ // keyboard focus, so it costs no rail width.
400
+ /* @__PURE__ */ jsxs2("div", { className: "relative flex min-w-0 items-center justify-center", children: [
401
+ children,
402
+ collapsible && /* @__PURE__ */ jsx3(
403
+ PanelToggleButton,
404
+ {
405
+ collapsed,
406
+ onToggle,
407
+ className: "pointer-events-none absolute inset-0 m-auto bg-background opacity-0 focus-visible:pointer-events-auto focus-visible:opacity-100"
408
+ }
409
+ )
410
+ ] })
411
+ ) : collapsible ? /* @__PURE__ */ jsx3(RailTooltip, { label: "Expand sidebar", children: /* @__PURE__ */ jsxs2(
375
412
  "button",
376
413
  {
377
414
  type: "button",
378
415
  onClick: onToggle,
379
416
  "aria-label": "Expand sidebar",
380
417
  "aria-pressed": true,
381
- className: cn("group/brand relative flex h-9 w-9 items-center justify-center rounded-md transition-colors hover:bg-[var(--accent-surface-soft)] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", MOTION_CONTROL),
418
+ className: cn("group/brand relative flex h-9 w-9 items-center justify-center rounded-md transition-colors hover:bg-[var(--accent-surface-soft)]", focusRing, MOTION_CONTROL),
382
419
  children: [
383
420
  /* @__PURE__ */ jsx3("span", { className: cn("flex items-center justify-center transition-opacity group-hover/brand:opacity-0", MOTION_CONTROL), children: brand }),
384
421
  /* @__PURE__ */ jsx3("span", { className: cn("pointer-events-none absolute inset-0 flex items-center justify-center text-foreground opacity-0 transition-opacity group-hover/brand:opacity-100", MOTION_CONTROL), children: /* @__PURE__ */ jsx3(PanelLeft, { className: "h-[18px] w-[18px]" }) })
385
422
  ]
386
423
  }
387
- ) }) : brandNode : /* @__PURE__ */ jsxs2(Fragment, { children: [
388
- brand !== void 0 && brandNode,
424
+ ) }) : brandNode : /* @__PURE__ */ jsxs2(Fragment2, { children: [
425
+ hasBrand && brandNode,
389
426
  /* @__PURE__ */ jsx3("div", { className: "min-w-0 flex-1", children }),
390
427
  collapsible && /* @__PURE__ */ jsx3(PanelToggleButton, { collapsed, onToggle })
391
428
  ] })
@@ -445,7 +482,7 @@ function RailButton({ icon: Icon2, label, isActive, badge, onClick, className, s
445
482
  MOTION_CONTROL,
446
483
  showLabel ? "w-full justify-start px-2.5 h-9 gap-2.5" : "w-9 h-9 justify-center",
447
484
  // Quiet press feedback via color only — no scale on the full-width row.
448
- "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
485
+ focusRing,
449
486
  // The active item (route matches) and any explicit primary share one
450
487
  // emphasized look — a filled accent pill with an accent ring — so the
451
488
  // current destination clearly stands out from the default rows.
@@ -456,7 +493,7 @@ function RailButton({ icon: Icon2, label, isActive, badge, onClick, className, s
456
493
  /* @__PURE__ */ jsx3(Icon2, { className: "h-[17px] w-[17px] shrink-0" }),
457
494
  badge !== void 0 && badge > 0 && /* @__PURE__ */ jsx3("span", { className: "absolute -top-1.5 -right-1.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-primary text-[9px] font-bold text-[var(--md3-on-primary)] px-1", children: badge > 99 ? "99+" : badge })
458
495
  ] });
459
- const content = /* @__PURE__ */ jsxs2(Fragment, { children: [
496
+ const content = /* @__PURE__ */ jsxs2(Fragment2, { children: [
460
497
  iconWithBadge,
461
498
  showLabel && /* @__PURE__ */ jsx3("span", { className: "text-[13.5px] font-medium", children: label })
462
499
  ] });
@@ -512,7 +549,7 @@ function RailFlyout({
512
549
  "relative flex shrink-0 items-center rounded-md transition-colors",
513
550
  MOTION_CONTROL,
514
551
  showLabel ? "w-full justify-start px-2.5 h-9 gap-2.5" : "w-9 h-9 justify-center",
515
- "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
552
+ focusRing,
516
553
  active ? "bg-[var(--accent-surface-strong)] text-[var(--accent-text)]" : "text-muted-foreground hover:bg-[var(--accent-surface-soft)] hover:text-foreground",
517
554
  className
518
555
  );
@@ -724,7 +761,7 @@ function RailExpandable({
724
761
  const triggerClasses = cn(
725
762
  "relative flex h-9 w-9 items-center justify-center rounded-md transition-colors",
726
763
  MOTION_CONTROL,
727
- "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
764
+ focusRing,
728
765
  isActive || open ? "bg-[var(--accent-surface-strong)] text-[var(--accent-text)] font-medium ring-1 ring-inset ring-[var(--border-accent)]" : "text-muted-foreground hover:bg-[var(--accent-surface-soft)] hover:text-foreground"
729
766
  );
730
767
  const openFlyout = () => {
@@ -828,7 +865,7 @@ function RailExpandable({
828
865
  "aria-expanded": open,
829
866
  "aria-controls": discloseId,
830
867
  "aria-label": open ? `Collapse ${label}` : `Expand ${label}`,
831
- className: "relative flex h-9 w-9 shrink-0 items-center justify-center rounded-md focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
868
+ className: `relative flex h-9 w-9 shrink-0 items-center justify-center rounded-md ${focusRing}`,
832
869
  children: [
833
870
  /* @__PURE__ */ jsx3(
834
871
  Icon2,
@@ -945,7 +982,7 @@ function AppearanceMenuSection({ appearance, divider = true }) {
945
982
  const resolved = appearance.value === "system" ? systemDark ? "dark" : "light" : appearance.value;
946
983
  const TriggerIcon = resolved === "dark" ? MoonIcon : SunIcon;
947
984
  const caption = appearance.value === "system" ? `System (${resolved === "dark" ? "Dark" : "Light"})` : APPEARANCE_META[appearance.value].label;
948
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
985
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
949
986
  divider && /* @__PURE__ */ jsx3(DropdownMenuSeparator, {}),
950
987
  /* @__PURE__ */ jsxs2(DropdownMenuSub, { children: [
951
988
  /* @__PURE__ */ jsxs2(DropdownMenuSubTrigger, { children: [
@@ -999,7 +1036,7 @@ function ProfileAvatar({
999
1036
  className: cn(
1000
1037
  "flex items-center rounded-lg transition-colors hover:bg-[var(--accent-surface-soft)]",
1001
1038
  MOTION_CONTROL,
1002
- "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
1039
+ focusRing,
1003
1040
  showDetails ? "w-full gap-2.5 px-3 py-2 text-left" : "justify-center w-12 h-12",
1004
1041
  className
1005
1042
  ),
@@ -1019,10 +1056,10 @@ function ProfileAvatar({
1019
1056
  user?.avatarUrl && /* @__PURE__ */ jsx3(AvatarImage, { src: user.avatarUrl, alt: "" }),
1020
1057
  /* @__PURE__ */ jsx3(AvatarFallback, { className: "text-sm bg-[var(--surface-violet-bg)] text-[var(--surface-violet-text)]", children: getInitials(user?.name, user?.email) })
1021
1058
  ] }),
1022
- /* @__PURE__ */ jsx3("div", { className: "min-w-0 flex-1", children: isLoading ? /* @__PURE__ */ jsxs2(Fragment, { children: [
1059
+ /* @__PURE__ */ jsx3("div", { className: "min-w-0 flex-1", children: isLoading ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
1023
1060
  /* @__PURE__ */ jsx3(Skeleton, { className: "h-4 w-24 mb-1" }),
1024
1061
  /* @__PURE__ */ jsx3(Skeleton, { className: "h-3 w-32" })
1025
- ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
1062
+ ] }) : /* @__PURE__ */ jsxs2(Fragment2, { children: [
1026
1063
  /* @__PURE__ */ jsx3("p", { className: "text-sm font-semibold truncate", children: user?.name ?? user?.email ?? "Not logged in" }),
1027
1064
  user?.email && user?.name && /* @__PURE__ */ jsx3("p", { className: "text-xs text-muted-foreground truncate", children: user.email }),
1028
1065
  user?.tier && /* @__PURE__ */ jsxs2("p", { className: "text-xs text-muted-foreground capitalize", children: [
@@ -1041,7 +1078,7 @@ function ProfileAvatar({
1041
1078
  /* @__PURE__ */ jsx3(SettingsIcon, { className: "mr-2 h-4 w-4", "aria-hidden": "true" }),
1042
1079
  "Settings"
1043
1080
  ] }) }),
1044
- onLogout && /* @__PURE__ */ jsxs2(Fragment, { children: [
1081
+ onLogout && /* @__PURE__ */ jsxs2(Fragment2, { children: [
1045
1082
  /* @__PURE__ */ jsx3(DropdownMenuSeparator, {}),
1046
1083
  /* @__PURE__ */ jsxs2(DropdownMenuItem, { className: "text-[var(--surface-danger-text)]", onClick: onLogout, children: [
1047
1084
  /* @__PURE__ */ jsx3(LogOutIcon, { className: "mr-2 h-4 w-4", "aria-hidden": "true" }),
@@ -1054,6 +1091,7 @@ function ProfileAvatar({
1054
1091
 
1055
1092
  // src/dashboard/credit-balance.tsx
1056
1093
  import * as React4 from "react";
1094
+ import { focusFieldWithin } from "@tangle-network/ui/utils";
1057
1095
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1058
1096
  function CreditBalance({
1059
1097
  amount,
@@ -1063,6 +1101,7 @@ function CreditBalance({
1063
1101
  className
1064
1102
  }) {
1065
1103
  const [topUpValue, setTopUpValue] = React4.useState("50.00");
1104
+ const topUpId = React4.useId();
1066
1105
  return /* @__PURE__ */ jsxs3(
1067
1106
  "div",
1068
1107
  {
@@ -1080,11 +1119,14 @@ function CreditBalance({
1080
1119
  /* @__PURE__ */ jsx4("p", { className: "text-sm text-muted-foreground leading-relaxed", children: description })
1081
1120
  ] }),
1082
1121
  onTopUp && /* @__PURE__ */ jsxs3("div", { className: "space-y-2.5 mt-5", children: [
1083
- /* @__PURE__ */ jsxs3("div", { className: "bg-surface-container-low border border-[var(--md3-outline-variant)] p-1 rounded-lg flex items-center", children: [
1122
+ /* @__PURE__ */ jsx4("label", { htmlFor: topUpId, className: "block text-xs font-medium text-muted-foreground", children: "Top-up amount (USD)" }),
1123
+ /* @__PURE__ */ jsxs3("div", { className: `bg-surface-container-low border p-1 rounded-lg flex items-center ${focusFieldWithin}`, children: [
1084
1124
  /* @__PURE__ */ jsx4(
1085
1125
  "input",
1086
1126
  {
1127
+ id: topUpId,
1087
1128
  type: "text",
1129
+ inputMode: "decimal",
1088
1130
  value: `$${topUpValue}`,
1089
1131
  onChange: (e) => {
1090
1132
  const raw = e.target.value.replace(/[^0-9.]/g, "");
@@ -1193,7 +1235,7 @@ function OutOfCreditsModal({
1193
1235
 
1194
1236
  // src/dashboard/cluster-status-bar.tsx
1195
1237
  import * as React5 from "react";
1196
- import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1238
+ import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1197
1239
  function ClusterStatusBar({ items, latency, className }) {
1198
1240
  if (!items?.length) return null;
1199
1241
  return /* @__PURE__ */ jsx6("div", { className: cn("fixed bottom-6 left-1/2 -translate-x-1/2 z-40 w-max max-w-[90vw] animate-in slide-in-from-bottom flex justify-center", className), children: /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-4 overflow-x-auto rounded-full border border-[var(--md3-outline-variant)] bg-surface-container px-5 py-2.5 shadow-sm", children: [
@@ -1207,7 +1249,7 @@ function ClusterStatusBar({ items, latency, className }) {
1207
1249
  ] })
1208
1250
  ] })
1209
1251
  ] }, i)),
1210
- latency && /* @__PURE__ */ jsxs5(Fragment3, { children: [
1252
+ latency && /* @__PURE__ */ jsxs5(Fragment4, { children: [
1211
1253
  /* @__PURE__ */ jsx6("span", { "aria-hidden": true, className: "h-5 w-px shrink-0 bg-border" }),
1212
1254
  /* @__PURE__ */ jsxs5("div", { className: "flex shrink-0 items-center gap-2", children: [
1213
1255
  /* @__PURE__ */ jsxs5("span", { className: "relative flex h-2.5 w-2.5 items-center justify-center", children: [
@@ -1357,7 +1399,8 @@ import {
1357
1399
  HardDrive,
1358
1400
  Box
1359
1401
  } from "lucide-react";
1360
- import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1402
+ import { focusRing as focusRing2 } from "@tangle-network/ui/utils";
1403
+ import { Fragment as Fragment5, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1361
1404
  function canAdminSandbox(sandbox) {
1362
1405
  if (!sandbox.team) return true;
1363
1406
  return sandbox.team.role === "owner" || sandbox.team.role === "admin";
@@ -1451,13 +1494,13 @@ function SandboxCard({
1451
1494
  "button",
1452
1495
  {
1453
1496
  type: "button",
1454
- className: "rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-surface-container-high hover:text-foreground outline-none",
1497
+ className: `rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-surface-container-high hover:text-foreground ${focusRing2}`,
1455
1498
  "aria-label": "Sandbox options",
1456
1499
  children: /* @__PURE__ */ jsx10(MoreVertical, { className: "h-4 w-4" })
1457
1500
  }
1458
1501
  ) }),
1459
1502
  /* @__PURE__ */ jsxs9(DropdownMenuContent2, { align: "end", className: "min-w-[180px]", children: [
1460
- isRunning && /* @__PURE__ */ jsxs9(Fragment4, { children: [
1503
+ isRunning && /* @__PURE__ */ jsxs9(Fragment5, { children: [
1461
1504
  onStop && /* @__PURE__ */ jsxs9(DropdownMenuItem2, { onClick: () => onStop(sandbox.id), children: [
1462
1505
  /* @__PURE__ */ jsx10(PowerOff, { className: "mr-2 h-4 w-4" }),
1463
1506
  " Stop Sandbox"
@@ -1476,7 +1519,7 @@ function SandboxCard({
1476
1519
  " Health Check"
1477
1520
  ] }),
1478
1521
  (onUsage || onHealth) && /* @__PURE__ */ jsx10(DropdownMenuSeparator2, {}),
1479
- onFork && /* @__PURE__ */ jsxs9(Fragment4, { children: [
1522
+ onFork && /* @__PURE__ */ jsxs9(Fragment5, { children: [
1480
1523
  /* @__PURE__ */ jsxs9(DropdownMenuItem2, { onClick: () => onFork(sandbox.id), children: [
1481
1524
  /* @__PURE__ */ jsx10(Copy, { className: "mr-2 h-4 w-4" }),
1482
1525
  " Fork Sandbox"
@@ -1484,7 +1527,7 @@ function SandboxCard({
1484
1527
  /* @__PURE__ */ jsx10(DropdownMenuSeparator2, {})
1485
1528
  ] })
1486
1529
  ] }),
1487
- isStopped && /* @__PURE__ */ jsxs9(Fragment4, { children: [
1530
+ isStopped && /* @__PURE__ */ jsxs9(Fragment5, { children: [
1488
1531
  resumeHandler && /* @__PURE__ */ jsxs9(DropdownMenuItem2, { onClick: () => resumeHandler(sandbox.id), children: [
1489
1532
  /* @__PURE__ */ jsx10(Power, { className: "mr-2 h-4 w-4" }),
1490
1533
  " ",
@@ -1647,7 +1690,8 @@ import {
1647
1690
  DropdownMenuSeparator as DropdownMenuSeparator3,
1648
1691
  DropdownMenuTrigger as DropdownMenuTrigger3
1649
1692
  } from "@tangle-network/ui/primitives";
1650
- import { Fragment as Fragment6, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1693
+ import { focusRing as focusRing3 } from "@tangle-network/ui/utils";
1694
+ import { Fragment as Fragment7, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1651
1695
  var statusColors = {
1652
1696
  running: { dot: "bg-[var(--code-success)] animate-pulse", text: "text-[var(--code-success)]", bar: "bg-[var(--code-success)]" },
1653
1697
  hibernating: { dot: "bg-muted-foreground", text: "text-muted-foreground", bar: "bg-muted-foreground" },
@@ -1803,7 +1847,7 @@ function SandboxTable({
1803
1847
  /* @__PURE__ */ jsx11(MiniMeter, { label: "RAM", percent: 0 })
1804
1848
  ] }) : null }),
1805
1849
  /* @__PURE__ */ jsx11("td", { className: "px-6 py-5 text-right", children: /* @__PURE__ */ jsxs10("div", { className: "flex items-center justify-end gap-1", children: [
1806
- isActive && /* @__PURE__ */ jsxs10(Fragment6, { children: [
1850
+ isActive && /* @__PURE__ */ jsxs10(Fragment7, { children: [
1807
1851
  /* @__PURE__ */ jsx11("button", { type: "button", onClick: (e) => {
1808
1852
  stopRowClick(e);
1809
1853
  onOpenIDE?.(sb.id);
@@ -1895,8 +1939,8 @@ function SandboxTable({
1895
1939
  {
1896
1940
  type: "button",
1897
1941
  onClick: stopRowClick,
1898
- className: "p-2 rounded-lg hover:bg-surface-container-high text-muted-foreground hover:text-foreground transition-all active:scale-90 outline-none",
1899
- "aria-label": "More actions",
1942
+ className: `p-2 rounded-lg hover:bg-surface-container-high text-muted-foreground hover:text-foreground transition-all active:scale-90 ${focusRing3}`,
1943
+ "aria-label": `More actions for ${sb.name}`,
1900
1944
  title: "More actions",
1901
1945
  children: /* @__PURE__ */ jsx11(MoreVertical2, { className: "h-4 w-4" })
1902
1946
  }
@@ -1907,10 +1951,10 @@ function SandboxTable({
1907
1951
  ] }, sectionIdx)) })
1908
1952
  ] });
1909
1953
  })(),
1910
- onDelete && canAdminSandbox(sb) && /* @__PURE__ */ jsx11("button", { type: "button", onClick: (e) => {
1954
+ onDelete && canAdminSandbox(sb) && /* @__PURE__ */ jsx11("button", { type: "button", "aria-label": `Delete ${sb.name}`, onClick: (e) => {
1911
1955
  stopRowClick(e);
1912
1956
  onDelete(sb.id);
1913
- }, className: "p-2 rounded-lg hover:bg-[var(--surface-danger-bg)] text-muted-foreground hover:text-[var(--surface-danger-text)] transition-all active:scale-90", title: "Delete", children: /* @__PURE__ */ jsx11(Trash22, { className: "h-4 w-4" }) })
1957
+ }, className: `p-2 rounded-lg hover:bg-[var(--surface-danger-bg)] text-muted-foreground hover:text-[var(--surface-danger-text)] transition-all active:scale-90 ${focusRing3}`, title: "Delete", children: /* @__PURE__ */ jsx11(Trash22, { className: "h-4 w-4" }) })
1914
1958
  ] }) })
1915
1959
  ]
1916
1960
  },
@@ -2040,6 +2084,7 @@ function PlanCards({ plans, className }) {
2040
2084
  // src/dashboard/backend-selector.tsx
2041
2085
  import { ChevronDown } from "lucide-react";
2042
2086
  import * as Select from "@radix-ui/react-select";
2087
+ import { focusRing as focusRing4 } from "@tangle-network/ui/utils";
2043
2088
  import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
2044
2089
  function BackendSelector({
2045
2090
  backends,
@@ -2063,7 +2108,7 @@ function BackendSelector({
2063
2108
  "px-2.5 text-xs font-medium text-foreground shadow-sm",
2064
2109
  "transition-colors duration-[var(--transition-fast)]",
2065
2110
  "hover:border-[var(--md3-outline-variant)] hover:bg-surface-container-high",
2066
- "focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
2111
+ focusRing4,
2067
2112
  "data-[state=open]:border-[var(--md3-outline)] data-[state=open]:bg-surface-container-high",
2068
2113
  "disabled:opacity-50 disabled:cursor-not-allowed",
2069
2114
  triggerClassName
@@ -2084,7 +2129,7 @@ function BackendSelector({
2084
2129
  "border border-[var(--md3-outline-variant)] bg-surface-container px-3 py-2.5 text-sm text-left",
2085
2130
  "transition-colors duration-[var(--transition-fast)]",
2086
2131
  "hover:border-[var(--md3-outline-variant)] hover:bg-surface-container-high",
2087
- "focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
2132
+ focusRing4,
2088
2133
  "data-[state=open]:border-[var(--md3-outline)] data-[state=open]:bg-surface-container-high",
2089
2134
  "disabled:opacity-50 disabled:cursor-not-allowed",
2090
2135
  triggerClassName
@@ -2338,7 +2383,7 @@ function useBrandThemeSync() {
2338
2383
  }
2339
2384
 
2340
2385
  // src/dashboard/dashboard-layout.tsx
2341
- import { Fragment as Fragment8, jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
2386
+ import { Fragment as Fragment9, jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
2342
2387
  function MenuIcon({ className }) {
2343
2388
  return /* @__PURE__ */ jsxs14("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className, children: [
2344
2389
  /* @__PURE__ */ jsx17("title", { children: "Menu icon" }),
@@ -2425,7 +2470,7 @@ function DashboardLayoutInner({
2425
2470
  );
2426
2471
  const activePanel = panels.find((p) => p.mode === mode);
2427
2472
  const buildSidebarContent = React9.useCallback(
2428
- (showLabels, allowCollapse) => /* @__PURE__ */ jsxs14(Fragment8, { children: [
2473
+ (showLabels, allowCollapse) => /* @__PURE__ */ jsxs14(Fragment9, { children: [
2429
2474
  /* @__PURE__ */ jsxs14(SidebarRail, { wide: showLabels, children: [
2430
2475
  /* @__PURE__ */ jsx17(
2431
2476
  RailHeader,
@@ -2706,7 +2751,7 @@ function DashboardLayout({ defaultPanelOpen, defaultMode, labeledRail, defaultRa
2706
2751
 
2707
2752
  // src/dashboard/sidebar-layout.tsx
2708
2753
  import * as React10 from "react";
2709
- import { Fragment as Fragment9, jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
2754
+ import { Fragment as Fragment10, jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
2710
2755
  var HIDE_BELOW_CLASS = {
2711
2756
  md: "max-md:hidden",
2712
2757
  lg: "max-lg:hidden"
@@ -2939,7 +2984,7 @@ function SidebarLayoutInner({
2939
2984
  breakpoint: hideBelow,
2940
2985
  onClose: closeMobileNav,
2941
2986
  header: railHeaderContent ?? logo,
2942
- footer: railFooter !== void 0 || hasProfile ? /* @__PURE__ */ jsxs15(Fragment9, { children: [
2987
+ footer: railFooter !== void 0 || hasProfile ? /* @__PURE__ */ jsxs15(Fragment10, { children: [
2943
2988
  railFooter,
2944
2989
  hasProfile && /* @__PURE__ */ jsx18(
2945
2990
  ProfileAvatar,
@@ -3096,7 +3141,7 @@ import {
3096
3141
  DropdownMenuSeparator as DropdownMenuSeparator4,
3097
3142
  DropdownMenuTrigger as DropdownMenuTrigger4
3098
3143
  } from "@tangle-network/ui/primitives";
3099
- import { Fragment as Fragment10, jsx as jsx19, jsxs as jsxs16 } from "react/jsx-runtime";
3144
+ import { Fragment as Fragment11, jsx as jsx19, jsxs as jsxs16 } from "react/jsx-runtime";
3100
3145
  function ProfileSelector({
3101
3146
  profiles,
3102
3147
  selectedId,
@@ -3130,7 +3175,7 @@ function ProfileSelector({
3130
3175
  ]
3131
3176
  }
3132
3177
  ),
3133
- builtinProfiles.length > 0 && /* @__PURE__ */ jsxs16(Fragment10, { children: [
3178
+ builtinProfiles.length > 0 && /* @__PURE__ */ jsxs16(Fragment11, { children: [
3134
3179
  /* @__PURE__ */ jsx19(DropdownMenuSeparator4, {}),
3135
3180
  /* @__PURE__ */ jsx19(DropdownMenuLabel2, { children: "Built-in Profiles" }),
3136
3181
  builtinProfiles.map((profile) => /* @__PURE__ */ jsxs16(
@@ -3155,7 +3200,7 @@ function ProfileSelector({
3155
3200
  profile.id
3156
3201
  ))
3157
3202
  ] }),
3158
- customProfiles.length > 0 && /* @__PURE__ */ jsxs16(Fragment10, { children: [
3203
+ customProfiles.length > 0 && /* @__PURE__ */ jsxs16(Fragment11, { children: [
3159
3204
  /* @__PURE__ */ jsx19(DropdownMenuSeparator4, {}),
3160
3205
  /* @__PURE__ */ jsx19(DropdownMenuLabel2, { children: "Custom Profiles" }),
3161
3206
  customProfiles.map((profile) => /* @__PURE__ */ jsxs16(
@@ -3192,7 +3237,7 @@ function ProfileSelector({
3192
3237
  profile.id
3193
3238
  ))
3194
3239
  ] }),
3195
- (onCreateClick || onManageClick) && /* @__PURE__ */ jsxs16(Fragment10, { children: [
3240
+ (onCreateClick || onManageClick) && /* @__PURE__ */ jsxs16(Fragment11, { children: [
3196
3241
  /* @__PURE__ */ jsx19(DropdownMenuSeparator4, {}),
3197
3242
  onCreateClick && /* @__PURE__ */ jsxs16(
3198
3243
  DropdownMenuItem4,
@@ -3434,7 +3479,7 @@ import {
3434
3479
  } from "lucide-react";
3435
3480
  import { Button as Button3 } from "@tangle-network/ui/primitives";
3436
3481
  import { Badge as Badge2 } from "@tangle-network/ui/primitives";
3437
- import { Fragment as Fragment12, jsx as jsx21, jsxs as jsxs18 } from "react/jsx-runtime";
3482
+ import { Fragment as Fragment13, jsx as jsx21, jsxs as jsxs18 } from "react/jsx-runtime";
3438
3483
  var statusConfig = {
3439
3484
  pending: {
3440
3485
  icon: Clock3,
@@ -3558,7 +3603,7 @@ function VariantList({
3558
3603
  children: outcomeConfig[variant.outcome].label
3559
3604
  }
3560
3605
  ),
3561
- variant.status === "completed" && variant.outcome === "pending_review" && onAccept && onReject && /* @__PURE__ */ jsxs18(Fragment12, { children: [
3606
+ variant.status === "completed" && variant.outcome === "pending_review" && onAccept && onReject && /* @__PURE__ */ jsxs18(Fragment13, { children: [
3562
3607
  /* @__PURE__ */ jsxs18(
3563
3608
  Button3,
3564
3609
  {
@@ -3872,16 +3917,38 @@ function GitPanel({ status, log, loading = false, onRefresh, className }) {
3872
3917
  // src/dashboard/ports-list.tsx
3873
3918
  import * as React13 from "react";
3874
3919
  import { Copy as Copy3, Check as Check5, Globe, Plus as Plus4, Trash2 as Trash23 } from "lucide-react";
3920
+ import { focusField, focusRing as focusRing5 } from "@tangle-network/ui/utils";
3875
3921
  import { jsx as jsx25, jsxs as jsxs22 } from "react/jsx-runtime";
3876
3922
  function PortsList({ ports, onExposePort, onRemovePort, isExposing = false, className }) {
3877
3923
  const [newPort, setNewPort] = React13.useState("");
3878
3924
  const [copiedPort, setCopiedPort] = React13.useState(null);
3925
+ const [statusMessage, setStatusMessage] = React13.useState("");
3879
3926
  const copyTimerRef = React13.useRef(null);
3927
+ const pendingRemovalRef = React13.useRef(null);
3928
+ const removeButtonsRef = React13.useRef(/* @__PURE__ */ new Map());
3929
+ const portInputRef = React13.useRef(null);
3930
+ const portInputId = React13.useId();
3931
+ const portErrorId = React13.useId();
3932
+ const portNumber = Number(newPort);
3933
+ const validPort = /^\d+$/.test(newPort) && portNumber >= 1 && portNumber <= 65535;
3934
+ const duplicatePort = validPort && ports.some((port) => port.port === portNumber);
3935
+ const portError = newPort === "" ? null : !validPort ? "Enter a whole port number from 1 to 65535." : duplicatePort ? `Port ${portNumber} is already exposed.` : null;
3880
3936
  React13.useEffect(() => {
3881
3937
  return () => {
3882
3938
  if (copyTimerRef.current) clearTimeout(copyTimerRef.current);
3883
3939
  };
3884
3940
  }, []);
3941
+ React13.useEffect(() => {
3942
+ const pending = pendingRemovalRef.current;
3943
+ if (!pending || ports.some((port) => port.port === pending.removed)) return;
3944
+ setStatusMessage(`Port ${pending.removed} removed.`);
3945
+ if (pending.restoreFocus) {
3946
+ const nextButton = pending.next == null ? null : removeButtonsRef.current.get(pending.next);
3947
+ const focusTarget = nextButton ?? removeButtonsRef.current.values().next().value ?? portInputRef.current;
3948
+ focusTarget?.focus();
3949
+ }
3950
+ pendingRemovalRef.current = null;
3951
+ }, [ports]);
3885
3952
  const handleCopy = async (url, port) => {
3886
3953
  try {
3887
3954
  await navigator.clipboard.writeText(url);
@@ -3893,86 +3960,114 @@ function PortsList({ ports, onExposePort, onRemovePort, isExposing = false, clas
3893
3960
  }
3894
3961
  };
3895
3962
  const handleExpose = () => {
3896
- const port = parseInt(newPort, 10);
3897
- if (port > 0 && port <= 65535) {
3898
- onExposePort(port);
3963
+ if (validPort && !duplicatePort && !isExposing) {
3964
+ onExposePort(portNumber);
3899
3965
  setNewPort("");
3900
3966
  }
3901
3967
  };
3902
3968
  return /* @__PURE__ */ jsxs22("div", { className: cn("space-y-4", className), children: [
3903
- ports.length > 0 ? /* @__PURE__ */ jsx25("div", { className: "rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container overflow-hidden", children: /* @__PURE__ */ jsxs22("table", { className: "w-full text-sm", children: [
3904
- /* @__PURE__ */ jsx25("thead", { className: "bg-surface-container-high border-b border-[var(--md3-outline-variant)]", children: /* @__PURE__ */ jsxs22("tr", { children: [
3905
- /* @__PURE__ */ jsx25("th", { className: "px-4 py-2.5 text-left text-xs font-medium text-muted-foreground", children: "Port" }),
3906
- /* @__PURE__ */ jsx25("th", { className: "px-4 py-2.5 text-left text-xs font-medium text-muted-foreground", children: "Public URL" }),
3907
- /* @__PURE__ */ jsx25("th", { className: "px-4 py-2.5 text-left text-xs font-medium text-muted-foreground", children: "Status" }),
3908
- /* @__PURE__ */ jsx25("th", { className: "px-4 py-2.5 text-right text-xs font-medium text-muted-foreground w-20" })
3909
- ] }) }),
3910
- /* @__PURE__ */ jsx25("tbody", { className: "divide-y divide-border", children: ports.map((p) => /* @__PURE__ */ jsxs22("tr", { children: [
3911
- /* @__PURE__ */ jsx25("td", { className: "px-4 py-3 font-mono text-xs text-foreground", children: p.port }),
3912
- /* @__PURE__ */ jsx25("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxs22(
3969
+ /* @__PURE__ */ jsx25("span", { role: "status", className: "sr-only", children: statusMessage }),
3970
+ ports.length > 0 ? /* @__PURE__ */ jsxs22("div", { className: "overflow-hidden rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container text-sm", children: [
3971
+ /* @__PURE__ */ jsxs22("div", { "aria-hidden": "true", className: "hidden border-b border-[var(--md3-outline-variant)] bg-surface-container-high px-3 py-2.5 text-xs font-medium text-muted-foreground sm:grid sm:grid-cols-[4rem_minmax(0,1fr)_5.5rem_3rem] sm:gap-x-2", children: [
3972
+ /* @__PURE__ */ jsx25("span", { children: "Port" }),
3973
+ /* @__PURE__ */ jsx25("span", { children: "Public URL" }),
3974
+ /* @__PURE__ */ jsx25("span", { children: "Status" })
3975
+ ] }),
3976
+ /* @__PURE__ */ jsx25("ul", { className: "divide-y divide-border", children: ports.map((p, index) => /* @__PURE__ */ jsxs22("li", { className: "grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-x-2 gap-y-1 px-3 py-2 sm:grid-cols-[4rem_minmax(0,1fr)_5.5rem_3rem] sm:py-1", children: [
3977
+ /* @__PURE__ */ jsxs22("span", { className: "col-start-1 row-start-1 font-mono text-xs text-foreground", children: [
3978
+ /* @__PURE__ */ jsx25("span", { className: "sr-only", children: "Port " }),
3979
+ p.port
3980
+ ] }),
3981
+ /* @__PURE__ */ jsxs22(
3913
3982
  "button",
3914
3983
  {
3915
3984
  type: "button",
3985
+ "aria-label": copiedPort === p.port ? `Copied public URL for port ${p.port}` : `Copy public URL for port ${p.port}: ${p.url}`,
3916
3986
  onClick: () => handleCopy(p.url, p.port),
3917
- className: "flex items-center gap-2 font-mono text-xs text-primary hover:underline cursor-pointer group",
3987
+ className: `col-span-2 row-start-3 flex min-h-10 min-w-0 items-center gap-2 rounded text-left font-mono text-xs text-[var(--accent-text)] hover:underline sm:col-span-1 sm:col-start-2 sm:row-start-1 ${focusRing5}`,
3918
3988
  children: [
3919
- /* @__PURE__ */ jsx25("span", { className: "truncate max-w-[300px]", children: p.url }),
3920
- copiedPort === p.port ? /* @__PURE__ */ jsx25(Check5, { className: "h-3 w-3 text-[var(--surface-success-text)] shrink-0" }) : /* @__PURE__ */ jsx25(Copy3, { className: "h-3 w-3 opacity-0 group-hover:opacity-100 transition-opacity shrink-0" })
3989
+ /* @__PURE__ */ jsx25("span", { "aria-hidden": "true", className: "min-w-0 flex-1 break-all sm:truncate", children: p.url }),
3990
+ copiedPort === p.port ? /* @__PURE__ */ jsx25(Check5, { className: "h-4 w-4 shrink-0 text-[var(--surface-success-text)]", "aria-hidden": "true" }) : /* @__PURE__ */ jsx25(Copy3, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" })
3921
3991
  ]
3922
3992
  }
3923
- ) }),
3924
- /* @__PURE__ */ jsx25("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsx25("span", { className: cn(
3925
- "inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider",
3993
+ ),
3994
+ /* @__PURE__ */ jsxs22("span", { className: cn(
3995
+ "col-start-1 row-start-2 inline-flex items-center justify-self-start rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider sm:col-start-3 sm:row-start-1",
3926
3996
  p.status === "active" ? "bg-[var(--surface-success-bg)] text-[var(--surface-success-text)]" : "bg-[var(--surface-warning-bg)] text-[var(--surface-warning-text)]"
3927
- ), children: p.status }) }),
3928
- /* @__PURE__ */ jsx25("td", { className: "px-4 py-3 text-right", children: onRemovePort && /* @__PURE__ */ jsx25(
3997
+ ), children: [
3998
+ /* @__PURE__ */ jsx25("span", { className: "sr-only", children: "Status: " }),
3999
+ p.status
4000
+ ] }),
4001
+ onRemovePort && /* @__PURE__ */ jsx25(
3929
4002
  "button",
3930
4003
  {
3931
4004
  type: "button",
3932
- onClick: () => onRemovePort(p.port),
3933
- className: "p-1 text-muted-foreground hover:text-destructive transition-colors rounded",
3934
- children: /* @__PURE__ */ jsx25(Trash23, { className: "h-3.5 w-3.5" })
4005
+ "aria-label": `Remove port ${p.port}`,
4006
+ ref: (button) => {
4007
+ if (button) removeButtonsRef.current.set(p.port, button);
4008
+ else removeButtonsRef.current.delete(p.port);
4009
+ },
4010
+ onClick: (event) => {
4011
+ pendingRemovalRef.current = {
4012
+ removed: p.port,
4013
+ next: ports[index + 1]?.port ?? ports[index - 1]?.port ?? null,
4014
+ restoreFocus: event.detail === 0
4015
+ };
4016
+ onRemovePort(p.port);
4017
+ },
4018
+ className: `col-start-2 row-start-1 inline-flex min-h-10 min-w-10 items-center justify-center rounded text-muted-foreground transition-colors hover:text-destructive sm:col-start-4 ${focusRing5}`,
4019
+ children: /* @__PURE__ */ jsx25(Trash23, { className: "h-4 w-4", "aria-hidden": "true" })
3935
4020
  }
3936
- ) })
4021
+ )
3937
4022
  ] }, p.port)) })
3938
- ] }) }) : /* @__PURE__ */ jsxs22("div", { className: "rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container p-6 text-center", children: [
4023
+ ] }) : /* @__PURE__ */ jsxs22("div", { className: "rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container p-6 text-center", children: [
3939
4024
  /* @__PURE__ */ jsx25(Globe, { className: "mx-auto h-8 w-8 text-muted-foreground mb-2" }),
3940
4025
  /* @__PURE__ */ jsx25("p", { className: "text-sm text-muted-foreground", children: "No ports exposed yet" })
3941
4026
  ] }),
3942
- /* @__PURE__ */ jsxs22("div", { className: "flex items-center gap-3", children: [
3943
- /* @__PURE__ */ jsx25(
3944
- "input",
3945
- {
3946
- type: "number",
3947
- min: 1,
3948
- max: 65535,
3949
- placeholder: "Port (e.g. 3000)",
3950
- value: newPort,
3951
- onChange: (e) => setNewPort(e.target.value),
3952
- onKeyDown: (e) => e.key === "Enter" && handleExpose(),
3953
- className: "flex-1 rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container-low px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-ring"
3954
- }
3955
- ),
4027
+ /* @__PURE__ */ jsxs22("div", { className: "flex flex-wrap items-end gap-3", children: [
4028
+ /* @__PURE__ */ jsxs22("div", { className: "min-w-0 flex-[1_1_10rem]", children: [
4029
+ /* @__PURE__ */ jsx25("label", { htmlFor: portInputId, className: "mb-1 block text-xs font-medium text-muted-foreground", children: "Port number" }),
4030
+ /* @__PURE__ */ jsx25(
4031
+ "input",
4032
+ {
4033
+ ref: portInputRef,
4034
+ id: portInputId,
4035
+ type: "number",
4036
+ min: 1,
4037
+ max: 65535,
4038
+ step: 1,
4039
+ "aria-invalid": Boolean(portError),
4040
+ "aria-describedby": portError ? portErrorId : void 0,
4041
+ placeholder: "3000",
4042
+ value: newPort,
4043
+ onChange: (e) => setNewPort(e.target.value),
4044
+ onKeyDown: (e) => e.key === "Enter" && handleExpose(),
4045
+ className: `w-full min-w-0 rounded-lg border bg-surface-container-low px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground ${focusField}`
4046
+ }
4047
+ )
4048
+ ] }),
3956
4049
  /* @__PURE__ */ jsxs22(
3957
4050
  "button",
3958
4051
  {
3959
4052
  type: "button",
3960
4053
  onClick: handleExpose,
3961
- disabled: !newPort || isExposing,
3962
- className: "inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground shadow-sm transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:bg-primary/55 disabled:text-primary-foreground/90 disabled:shadow-none",
4054
+ disabled: !validPort || duplicatePort || isExposing,
4055
+ className: `inline-flex shrink-0 items-center gap-2 rounded-lg bg-[var(--btn-primary-bg)] px-4 py-2 text-sm font-semibold text-[var(--btn-primary-text)] shadow-sm transition-colors hover:bg-[var(--btn-primary-hover)] disabled:cursor-not-allowed disabled:opacity-55 disabled:shadow-none ${focusRing5}`,
3963
4056
  children: [
3964
4057
  /* @__PURE__ */ jsx25(Plus4, { className: "h-4 w-4" }),
3965
4058
  "Expose"
3966
4059
  ]
3967
4060
  }
3968
4061
  )
3969
- ] })
4062
+ ] }),
4063
+ portError && /* @__PURE__ */ jsx25("p", { id: portErrorId, className: "text-xs text-[var(--surface-danger-text)]", children: portError })
3970
4064
  ] });
3971
4065
  }
3972
4066
 
3973
4067
  // src/dashboard/process-list.tsx
3974
4068
  import * as React14 from "react";
3975
4069
  import { Activity as Activity3, Plus as Plus5, Skull, Terminal as Terminal4 } from "lucide-react";
4070
+ import { focusField as focusField2, focusRing as focusRing6 } from "@tangle-network/ui/utils";
3976
4071
  import { jsx as jsx26, jsxs as jsxs23 } from "react/jsx-runtime";
3977
4072
  function formatUptime(startedAt) {
3978
4073
  if (!startedAt) return "-";
@@ -3984,6 +4079,18 @@ function formatUptime(startedAt) {
3984
4079
  }
3985
4080
  function ProcessList({ processes, onSpawn, onKill, loading = false, className }) {
3986
4081
  const [newCommand, setNewCommand] = React14.useState("");
4082
+ const [statusMessage, setStatusMessage] = React14.useState("");
4083
+ const commandId = React14.useId();
4084
+ const commandInputRef = React14.useRef(null);
4085
+ const pendingKillRef = React14.useRef(null);
4086
+ React14.useEffect(() => {
4087
+ const pending = pendingKillRef.current;
4088
+ if (!pending || processes.some((process) => process.pid === pending.pid && process.running)) return;
4089
+ const stopped = processes.find((process) => process.pid === pending.pid);
4090
+ setStatusMessage(stopped?.exitCode == null ? `Process ${pending.pid} stopped.` : `Process ${pending.pid} exited with code ${stopped.exitCode}.`);
4091
+ if (pending.restoreFocus) commandInputRef.current?.focus();
4092
+ pendingKillRef.current = null;
4093
+ }, [processes]);
3987
4094
  const handleSpawn = () => {
3988
4095
  const cmd = newCommand.trim();
3989
4096
  if (cmd) {
@@ -3992,61 +4099,73 @@ function ProcessList({ processes, onSpawn, onKill, loading = false, className })
3992
4099
  }
3993
4100
  };
3994
4101
  return /* @__PURE__ */ jsxs23("div", { className: cn("space-y-4", className), children: [
4102
+ /* @__PURE__ */ jsx26("span", { role: "status", className: "sr-only", children: statusMessage }),
3995
4103
  loading ? /* @__PURE__ */ jsxs23("div", { className: "rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container p-6 text-center", children: [
3996
4104
  /* @__PURE__ */ jsx26(Activity3, { className: "mx-auto h-6 w-6 text-muted-foreground animate-spin mb-2", "data-motion": "essential" }),
3997
4105
  /* @__PURE__ */ jsx26("p", { className: "text-sm text-muted-foreground", children: "Loading processes..." })
3998
- ] }) : processes.length > 0 ? /* @__PURE__ */ jsx26("div", { className: "rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container overflow-hidden", children: /* @__PURE__ */ jsxs23("table", { className: "w-full text-sm", children: [
3999
- /* @__PURE__ */ jsx26("thead", { className: "bg-surface-container-high border-b border-[var(--md3-outline-variant)]", children: /* @__PURE__ */ jsxs23("tr", { children: [
4000
- /* @__PURE__ */ jsx26("th", { className: "px-4 py-2.5 text-left text-xs font-medium text-muted-foreground", children: "PID" }),
4001
- /* @__PURE__ */ jsx26("th", { className: "px-4 py-2.5 text-left text-xs font-medium text-muted-foreground", children: "Command" }),
4002
- /* @__PURE__ */ jsx26("th", { className: "px-4 py-2.5 text-left text-xs font-medium text-muted-foreground", children: "Status" }),
4003
- /* @__PURE__ */ jsx26("th", { className: "px-4 py-2.5 text-left text-xs font-medium text-muted-foreground", children: "Uptime" }),
4004
- /* @__PURE__ */ jsx26("th", { className: "px-4 py-2.5 text-right text-xs font-medium text-muted-foreground w-20" })
4005
- ] }) }),
4006
- /* @__PURE__ */ jsx26("tbody", { className: "divide-y divide-border", children: processes.map((p) => /* @__PURE__ */ jsxs23("tr", { children: [
4007
- /* @__PURE__ */ jsx26("td", { className: "px-4 py-3 font-mono text-xs text-foreground", children: p.pid }),
4008
- /* @__PURE__ */ jsx26("td", { className: "px-4 py-3 font-mono text-xs text-foreground truncate max-w-[250px]", children: p.command }),
4009
- /* @__PURE__ */ jsx26("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsx26("span", { className: cn(
4010
- "inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider",
4011
- p.running ? "bg-[var(--surface-success-bg)] text-[var(--surface-success-text)]" : "bg-surface-container-high text-muted-foreground"
4012
- ), children: p.running ? "running" : `exited (${p.exitCode ?? "?"})` }) }),
4013
- /* @__PURE__ */ jsx26("td", { className: "px-4 py-3 font-mono text-xs text-muted-foreground", children: formatUptime(p.startedAt) }),
4014
- /* @__PURE__ */ jsx26("td", { className: "px-4 py-3 text-right", children: p.running && /* @__PURE__ */ jsx26(
4106
+ ] }) : processes.length > 0 ? /* @__PURE__ */ jsx26("div", { className: "rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container overflow-hidden", children: /* @__PURE__ */ jsx26("ul", { "aria-label": "Processes", className: "divide-y divide-border", children: processes.map((p) => {
4107
+ const age = formatUptime(p.startedAt);
4108
+ return /* @__PURE__ */ jsxs23("li", { className: "flex min-w-0 items-start gap-3 px-3 py-3", children: [
4109
+ /* @__PURE__ */ jsxs23("div", { className: "min-w-0 flex-1", children: [
4110
+ /* @__PURE__ */ jsx26("code", { className: "block break-all font-mono text-xs leading-5 text-foreground", children: p.command }),
4111
+ /* @__PURE__ */ jsxs23("div", { className: "mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [
4112
+ /* @__PURE__ */ jsxs23("span", { className: "font-mono tabular-nums", children: [
4113
+ "PID ",
4114
+ p.pid
4115
+ ] }),
4116
+ /* @__PURE__ */ jsx26("span", { className: cn(
4117
+ "inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider",
4118
+ p.running ? "bg-[var(--surface-success-bg)] text-[var(--surface-success-text)]" : "bg-surface-container-high text-muted-foreground"
4119
+ ), children: p.running ? "running" : `exited (${p.exitCode ?? "?"})` }),
4120
+ age !== "-" && /* @__PURE__ */ jsx26("span", { className: "font-mono tabular-nums", children: p.running ? `Uptime ${age}` : `Started ${age} ago` })
4121
+ ] })
4122
+ ] }),
4123
+ p.running && /* @__PURE__ */ jsx26(
4015
4124
  "button",
4016
4125
  {
4017
4126
  type: "button",
4018
- onClick: () => onKill(p.pid),
4019
- className: "p-1 text-muted-foreground hover:text-destructive transition-colors rounded",
4020
- title: "Kill process",
4021
- children: /* @__PURE__ */ jsx26(Skull, { className: "h-3.5 w-3.5" })
4127
+ onClick: (event) => {
4128
+ pendingKillRef.current = { pid: p.pid, restoreFocus: event.detail === 0 };
4129
+ onKill(p.pid);
4130
+ },
4131
+ className: `flex size-10 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive ${focusRing6}`,
4132
+ "aria-label": `Kill process ${p.pid}`,
4133
+ title: `Kill process ${p.pid}`,
4134
+ children: /* @__PURE__ */ jsx26(Skull, { "aria-hidden": "true", className: "h-4 w-4" })
4022
4135
  }
4023
- ) })
4024
- ] }, `${p.pid}-${p.startedAt ?? p.command}`)) })
4025
- ] }) }) : /* @__PURE__ */ jsxs23("div", { className: "rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container p-6 text-center", children: [
4136
+ )
4137
+ ] }, `${p.pid}-${p.startedAt ?? p.command}`);
4138
+ }) }) }) : /* @__PURE__ */ jsxs23("div", { className: "rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container p-6 text-center", children: [
4026
4139
  /* @__PURE__ */ jsx26(Terminal4, { className: "mx-auto h-8 w-8 text-muted-foreground mb-2" }),
4027
4140
  /* @__PURE__ */ jsx26("p", { className: "text-sm text-muted-foreground", children: "No processes running" })
4028
4141
  ] }),
4029
- /* @__PURE__ */ jsxs23("div", { className: "flex items-center gap-3", children: [
4030
- /* @__PURE__ */ jsx26(
4031
- "input",
4032
- {
4033
- type: "text",
4034
- placeholder: "Command (e.g. node server.js)",
4035
- value: newCommand,
4036
- onChange: (e) => setNewCommand(e.target.value),
4037
- onKeyDown: (e) => e.key === "Enter" && handleSpawn(),
4038
- className: "flex-1 rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container-low px-3 py-2 text-sm font-mono text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring"
4039
- }
4040
- ),
4142
+ /* @__PURE__ */ jsxs23("form", { onSubmit: (event) => {
4143
+ event.preventDefault();
4144
+ handleSpawn();
4145
+ }, className: "flex min-w-0 flex-wrap items-end gap-2", children: [
4146
+ /* @__PURE__ */ jsxs23("div", { className: "min-w-0 flex-[1_1_12rem]", children: [
4147
+ /* @__PURE__ */ jsx26("label", { htmlFor: commandId, className: "mb-1 block text-xs font-medium text-muted-foreground", children: "Command" }),
4148
+ /* @__PURE__ */ jsx26(
4149
+ "input",
4150
+ {
4151
+ ref: commandInputRef,
4152
+ id: commandId,
4153
+ type: "text",
4154
+ placeholder: "node app.js",
4155
+ value: newCommand,
4156
+ onChange: (e) => setNewCommand(e.target.value),
4157
+ className: `w-full min-w-0 rounded-lg border bg-surface-container-low px-3 py-2 text-sm font-mono text-foreground placeholder:text-muted-foreground ${focusField2}`
4158
+ }
4159
+ )
4160
+ ] }),
4041
4161
  /* @__PURE__ */ jsxs23(
4042
4162
  "button",
4043
4163
  {
4044
- type: "button",
4045
- onClick: handleSpawn,
4164
+ type: "submit",
4046
4165
  disabled: !newCommand.trim(),
4047
- className: "inline-flex items-center gap-2 rounded-lg bg-primary/20 border border-primary/30 px-4 py-2 text-sm font-medium text-primary hover:bg-primary hover:text-primary-foreground transition-colors disabled:opacity-50",
4166
+ className: `inline-flex h-10 shrink-0 items-center gap-2 rounded-lg border border-[var(--border-accent)] bg-[var(--accent-surface-soft)] px-3 text-sm font-medium text-[var(--accent-text)] transition-colors hover:bg-[var(--btn-primary-bg)] hover:text-[var(--btn-primary-text)] disabled:opacity-50 ${focusRing6}`,
4048
4167
  children: [
4049
- /* @__PURE__ */ jsx26(Plus5, { className: "h-4 w-4" }),
4168
+ /* @__PURE__ */ jsx26(Plus5, { "aria-hidden": "true", className: "h-4 w-4" }),
4050
4169
  "Spawn"
4051
4170
  ]
4052
4171
  }
@@ -4058,6 +4177,7 @@ function ProcessList({ processes, onSpawn, onKill, loading = false, className })
4058
4177
  // src/dashboard/network-config.tsx
4059
4178
  import * as React15 from "react";
4060
4179
  import { Network as Network2, Plus as Plus6, Trash2 as Trash24, ShieldAlert } from "lucide-react";
4180
+ import { focusField as focusField3, focusRing as focusRing7 } from "@tangle-network/ui/utils";
4061
4181
  import { jsx as jsx27, jsxs as jsxs24 } from "react/jsx-runtime";
4062
4182
  function NetworkConfig({ config, onUpdate, loading = false, className }) {
4063
4183
  const [newCidr, setNewCidr] = React15.useState("");
@@ -4127,8 +4247,9 @@ function NetworkConfig({ config, onUpdate, loading = false, className }) {
4127
4247
  "button",
4128
4248
  {
4129
4249
  type: "button",
4250
+ "aria-label": `Remove ${cidr} from allowlist`,
4130
4251
  onClick: () => handleRemoveCidr(cidr),
4131
- className: "p-1 text-muted-foreground hover:text-destructive transition-colors rounded",
4252
+ className: `p-2 text-muted-foreground hover:text-destructive transition-colors rounded ${focusRing7}`,
4132
4253
  children: /* @__PURE__ */ jsx27(Trash24, { className: "h-3 w-3" })
4133
4254
  }
4134
4255
  )
@@ -4138,11 +4259,12 @@ function NetworkConfig({ config, onUpdate, loading = false, className }) {
4138
4259
  "input",
4139
4260
  {
4140
4261
  type: "text",
4262
+ "aria-label": "CIDR to allow",
4141
4263
  placeholder: "CIDR (e.g. 10.0.0.0/8)",
4142
4264
  value: newCidr,
4143
4265
  onChange: (e) => setNewCidr(e.target.value),
4144
4266
  onKeyDown: (e) => e.key === "Enter" && handleAddCidr(),
4145
- className: "flex-1 rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container-low px-3 py-2 text-sm font-mono text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring"
4267
+ className: `flex-1 rounded-lg border bg-surface-container-low px-3 py-2 text-sm font-mono text-foreground placeholder:text-muted-foreground ${focusField3}`
4146
4268
  }
4147
4269
  ),
4148
4270
  /* @__PURE__ */ jsxs24(
@@ -4166,7 +4288,8 @@ function NetworkConfig({ config, onUpdate, loading = false, className }) {
4166
4288
  // src/dashboard/backend-config.tsx
4167
4289
  import * as React16 from "react";
4168
4290
  import { Bot as Bot2, Plus as Plus7, Trash2 as Trash25, Server, Wrench } from "lucide-react";
4169
- import { Fragment as Fragment13, jsx as jsx28, jsxs as jsxs25 } from "react/jsx-runtime";
4291
+ import { focusField as focusField4, focusRing as focusRing8 } from "@tangle-network/ui/utils";
4292
+ import { Fragment as Fragment14, jsx as jsx28, jsxs as jsxs25 } from "react/jsx-runtime";
4170
4293
  function BackendConfig({
4171
4294
  status,
4172
4295
  mcpServers,
@@ -4223,7 +4346,7 @@ function BackendConfig({
4223
4346
  ), children: status.running ? "Running" : "Stopped" }) }),
4224
4347
  /* @__PURE__ */ jsx28("dt", { className: "text-muted-foreground", children: "Model" }),
4225
4348
  /* @__PURE__ */ jsx28("dd", { className: "font-mono text-xs", children: status.model ?? "Default" }),
4226
- status.provider && /* @__PURE__ */ jsxs25(Fragment13, { children: [
4349
+ status.provider && /* @__PURE__ */ jsxs25(Fragment14, { children: [
4227
4350
  /* @__PURE__ */ jsx28("dt", { className: "text-muted-foreground", children: "Provider" }),
4228
4351
  /* @__PURE__ */ jsx28("dd", { className: "font-mono text-xs", children: status.provider })
4229
4352
  ] })
@@ -4269,44 +4392,53 @@ function BackendConfig({
4269
4392
  "button",
4270
4393
  {
4271
4394
  type: "button",
4395
+ "aria-label": `Remove ${s.name} MCP server`,
4272
4396
  onClick: () => onRemoveMcp(s.name),
4273
- className: "p-1 text-muted-foreground hover:text-destructive transition-colors rounded",
4397
+ className: `p-2 text-muted-foreground hover:text-destructive transition-colors rounded ${focusRing8}`,
4274
4398
  children: /* @__PURE__ */ jsx28(Trash25, { className: "h-3.5 w-3.5" })
4275
4399
  }
4276
4400
  )
4277
4401
  ] })
4278
4402
  ] }, s.name)) }) : /* @__PURE__ */ jsx28("div", { className: "p-4 text-center", children: /* @__PURE__ */ jsx28("p", { className: "text-xs text-muted-foreground", children: "No MCP servers configured" }) }),
4279
4403
  showAddMcp && /* @__PURE__ */ jsxs25("div", { className: "p-4 border-t border-[var(--md3-outline-variant)] bg-surface-container-high space-y-2", children: [
4280
- /* @__PURE__ */ jsx28(
4281
- "input",
4282
- {
4283
- type: "text",
4284
- placeholder: "Server name",
4285
- value: mcpName,
4286
- onChange: (e) => setMcpName(e.target.value),
4287
- className: "w-full rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container-low px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring"
4288
- }
4289
- ),
4290
- /* @__PURE__ */ jsx28(
4291
- "input",
4292
- {
4293
- type: "text",
4294
- placeholder: "Command (e.g. npx @server/mcp)",
4295
- value: mcpCommand,
4296
- onChange: (e) => setMcpCommand(e.target.value),
4297
- className: "w-full rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container-low px-3 py-2 text-sm font-mono text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring"
4298
- }
4299
- ),
4300
- /* @__PURE__ */ jsx28(
4301
- "input",
4302
- {
4303
- type: "text",
4304
- placeholder: "Arguments (space-separated, optional)",
4305
- value: mcpArgs,
4306
- onChange: (e) => setMcpArgs(e.target.value),
4307
- className: "w-full rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container-low px-3 py-2 text-sm font-mono text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring"
4308
- }
4309
- ),
4404
+ /* @__PURE__ */ jsxs25("label", { className: "block space-y-1 text-xs font-medium text-muted-foreground", children: [
4405
+ /* @__PURE__ */ jsx28("span", { children: "Server name" }),
4406
+ /* @__PURE__ */ jsx28(
4407
+ "input",
4408
+ {
4409
+ type: "text",
4410
+ value: mcpName,
4411
+ onChange: (e) => setMcpName(e.target.value),
4412
+ className: `w-full rounded-lg border bg-surface-container-low px-3 py-2 text-sm text-foreground ${focusField4}`
4413
+ }
4414
+ )
4415
+ ] }),
4416
+ /* @__PURE__ */ jsxs25("label", { className: "block space-y-1 text-xs font-medium text-muted-foreground", children: [
4417
+ /* @__PURE__ */ jsx28("span", { children: "Command" }),
4418
+ /* @__PURE__ */ jsx28(
4419
+ "input",
4420
+ {
4421
+ type: "text",
4422
+ placeholder: "npx @server/mcp",
4423
+ value: mcpCommand,
4424
+ onChange: (e) => setMcpCommand(e.target.value),
4425
+ className: `w-full rounded-lg border bg-surface-container-low px-3 py-2 text-sm font-mono text-foreground placeholder:text-muted-foreground ${focusField4}`
4426
+ }
4427
+ )
4428
+ ] }),
4429
+ /* @__PURE__ */ jsxs25("label", { className: "block space-y-1 text-xs font-medium text-muted-foreground", children: [
4430
+ /* @__PURE__ */ jsx28("span", { children: "Arguments (optional)" }),
4431
+ /* @__PURE__ */ jsx28(
4432
+ "input",
4433
+ {
4434
+ type: "text",
4435
+ placeholder: "Space-separated arguments",
4436
+ value: mcpArgs,
4437
+ onChange: (e) => setMcpArgs(e.target.value),
4438
+ className: `w-full rounded-lg border bg-surface-container-low px-3 py-2 text-sm font-mono text-foreground placeholder:text-muted-foreground ${focusField4}`
4439
+ }
4440
+ )
4441
+ ] }),
4310
4442
  /* @__PURE__ */ jsxs25("div", { className: "flex justify-end gap-2 pt-1", children: [
4311
4443
  /* @__PURE__ */ jsx28(
4312
4444
  "button",
@@ -4336,6 +4468,7 @@ function BackendConfig({
4336
4468
  // src/dashboard/snapshot-list.tsx
4337
4469
  import * as React17 from "react";
4338
4470
  import { Camera, Clock as Clock5, HardDrive as HardDrive2, Plus as Plus8, RotateCcw } from "lucide-react";
4471
+ import { focusField as focusField5 } from "@tangle-network/ui/utils";
4339
4472
  import { jsx as jsx29, jsxs as jsxs26 } from "react/jsx-runtime";
4340
4473
  function formatBytes(bytes) {
4341
4474
  if (bytes == null || bytes < 0) return "-";
@@ -4385,7 +4518,7 @@ function SnapshotList({ snapshots, onCreate, onRestore, onSaveAsTemplate, loadin
4385
4518
  value: tags,
4386
4519
  onChange: (e) => setTags(e.target.value),
4387
4520
  onKeyDown: (e) => e.key === "Enter" && handleCreate(),
4388
- className: "w-full rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container-low px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring"
4521
+ className: `w-full rounded-lg border bg-surface-container-low px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground ${focusField5}`
4389
4522
  }
4390
4523
  ),
4391
4524
  /* @__PURE__ */ jsxs26("div", { className: "flex justify-end gap-2", children: [
@@ -4469,7 +4602,7 @@ function SnapshotList({ snapshots, onCreate, onRestore, onSaveAsTemplate, loadin
4469
4602
  }
4470
4603
 
4471
4604
  // src/dashboard/promo-banner.tsx
4472
- import { Fragment as Fragment14, jsx as jsx30, jsxs as jsxs27 } from "react/jsx-runtime";
4605
+ import { Fragment as Fragment15, jsx as jsx30, jsxs as jsxs27 } from "react/jsx-runtime";
4473
4606
  function PromoBanner({
4474
4607
  title,
4475
4608
  description,
@@ -4484,7 +4617,7 @@ function PromoBanner({
4484
4617
  "mt-6 inline-flex items-center gap-2 rounded-md border border-white/20 bg-[var(--btn-primary-bg)] px-4 py-2 text-sm font-medium text-[var(--btn-primary-text)] transition-colors",
4485
4618
  disabled ? "opacity-50 cursor-not-allowed" : "hover:bg-[var(--btn-primary-hover)]"
4486
4619
  );
4487
- const buttonContent = /* @__PURE__ */ jsxs27(Fragment14, { children: [
4620
+ const buttonContent = /* @__PURE__ */ jsxs27(Fragment15, { children: [
4488
4621
  buttonLabel,
4489
4622
  /* @__PURE__ */ jsxs27("svg", { "aria-hidden": "true", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "h-4 w-4", children: [
4490
4623
  /* @__PURE__ */ jsx30("path", { d: "M5 12h14" }),