@meetreeve/ui 0.6.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -33,6 +33,7 @@ var src_exports = {};
33
33
  __export(src_exports, {
34
34
  AppSidebar: () => AppSidebar,
35
35
  BrandGlyph: () => BrandGlyph,
36
+ ConnectorPicker: () => ConnectorPicker,
36
37
  ContextSwitcher: () => ContextSwitcher,
37
38
  Flyout: () => Flyout,
38
39
  GLYPH_CANVAS_FILL: () => GLYPH_CANVAS_FILL,
@@ -90,6 +91,7 @@ var WORDMARK_WEIGHT = 800;
90
91
  var WORDMARK_WEIGHT_CLASS = "font-extrabold";
91
92
  var WORDMARK_TEXT_LG = "text-base";
92
93
  var MONOGRAM_TEXT_LG = "text-[17px]";
94
+ var MONOGRAM_TEXT_XL = "text-[26px]";
93
95
  var LOCKUP_GAP = "gap-2";
94
96
  var LOCKUP_ROW = "flex items-baseline";
95
97
  var GLYPH_CANVAS_FILL = 0.65;
@@ -129,9 +131,9 @@ function faviconUrl(domain) {
129
131
  return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(host)}&sz=128`;
130
132
  }
131
133
  function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
132
- const box = size === "lg" ? "h-6 w-6" : "h-5 w-5";
133
- const img = size === "lg" ? "h-4 w-4" : "h-3.5 w-3.5";
134
- const dim = size === "lg" ? 16 : 14;
134
+ const box = size === "xl" ? "h-10 w-10" : size === "lg" ? "h-6 w-6" : "h-5 w-5";
135
+ const img = size === "xl" ? "h-7 w-7" : size === "lg" ? "h-4 w-4" : "h-3.5 w-3.5";
136
+ const dim = size === "xl" ? 28 : size === "lg" ? 16 : 14;
135
137
  const candidates = [];
136
138
  if (brand.favicon) candidates.push(brand.favicon);
137
139
  if (brand.logo) candidates.push(brand.logo);
@@ -184,8 +186,10 @@ function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
184
186
  // DEV-5139 rule 2: at "lg" the letter runs 17px = the text-base
185
187
  // wordmark (16px) × MARK_CAP_RATIO (1.08) — a lone round glyph at
186
188
  // exact parity optically reads small. "sm" has no wordmark partner
187
- // in its surfaces; SIZE unchanged pending a lockup audit there.
188
- size === "lg" ? MONOGRAM_TEXT_LG : "text-[11px]",
189
+ // in its surfaces; unchanged pending a lockup audit there. "xl"
190
+ // (DEV-5331) is a standalone glyph with no wordmark at all — rule 5
191
+ // (GLYPH_CANVAS_FILL) governs it instead of rule 2.
192
+ size === "xl" ? MONOGRAM_TEXT_XL : size === "lg" ? MONOGRAM_TEXT_LG : "text-[11px]",
189
193
  box,
190
194
  className
191
195
  ),
@@ -752,11 +756,22 @@ function useLadderRung(ladder, containerRef, options = {}) {
752
756
  var import_react7 = require("react");
753
757
  var import_jsx_runtime3 = require("react/jsx-runtime");
754
758
  var PANEL_GAP = 4;
755
- function Flyout({ label, icon, active, children, linkComponent, className }) {
759
+ function Flyout({
760
+ label,
761
+ icon,
762
+ active,
763
+ children,
764
+ linkComponent,
765
+ collapsed = false,
766
+ onOpenChange,
767
+ forceClosed = false,
768
+ className
769
+ }) {
756
770
  const [hoverOpen, setHoverOpen] = (0, import_react7.useState)(false);
757
771
  const [clickOpen, setClickOpen] = (0, import_react7.useState)(false);
758
772
  const [panelPosition, setPanelPosition] = (0, import_react7.useState)(null);
759
- const open = hoverOpen || clickOpen;
773
+ const wantsOpen = hoverOpen || clickOpen;
774
+ const open = wantsOpen && !forceClosed;
760
775
  const panelId = (0, import_react7.useId)();
761
776
  const triggerRef = (0, import_react7.useRef)(null);
762
777
  const panelRef = (0, import_react7.useRef)(null);
@@ -768,6 +783,17 @@ function Flyout({ label, icon, active, children, linkComponent, className }) {
768
783
  const rect = triggerRef.current?.getBoundingClientRect();
769
784
  if (rect) setPanelPosition({ top: rect.top, left: rect.right + PANEL_GAP });
770
785
  }, []);
786
+ const onOpenChangeRef = (0, import_react7.useRef)(onOpenChange);
787
+ onOpenChangeRef.current = onOpenChange;
788
+ (0, import_react7.useEffect)(() => {
789
+ onOpenChangeRef.current?.(wantsOpen);
790
+ }, [wantsOpen]);
791
+ (0, import_react7.useEffect)(() => {
792
+ if (forceClosed) {
793
+ setHoverOpen(false);
794
+ setClickOpen(false);
795
+ }
796
+ }, [forceClosed]);
771
797
  (0, import_react7.useLayoutEffect)(() => {
772
798
  if (open) reposition();
773
799
  }, [open, reposition]);
@@ -808,9 +834,11 @@ function Flyout({ label, icon, active, children, linkComponent, className }) {
808
834
  "data-testid": "ladder-flyout-trigger",
809
835
  "aria-expanded": open,
810
836
  "aria-controls": panelId,
837
+ title: collapsed ? label : void 0,
811
838
  onClick: () => setClickOpen((v) => !v),
812
839
  className: cn(
813
840
  "group flex w-full items-center gap-2.5 rounded-md px-3 py-1.5 text-sm transition-colors",
841
+ collapsed && "justify-center px-0",
814
842
  active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
815
843
  ),
816
844
  children: [
@@ -822,7 +850,7 @@ function Flyout({ label, icon, active, children, linkComponent, className }) {
822
850
  "aria-hidden": "true"
823
851
  }
824
852
  ),
825
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "truncate", children: label })
853
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: cn("truncate", collapsed && "sr-only"), children: label })
826
854
  ]
827
855
  }
828
856
  ),
@@ -873,6 +901,9 @@ function Flyout({ label, icon, active, children, linkComponent, className }) {
873
901
 
874
902
  // src/app-sidebar.tsx
875
903
  var import_jsx_runtime4 = require("react/jsx-runtime");
904
+ var DEFAULT_COLLAPSE_STORAGE_KEY = "reeve-sidebar-collapsed";
905
+ var COLLAPSED_WIDTH_CLASS = "w-14";
906
+ var EXPANDED_WIDTH_CLASS = "w-64";
876
907
  function isHrefActive(href, activePath) {
877
908
  return activePath === href || activePath.startsWith(`${href}/`);
878
909
  }
@@ -882,7 +913,8 @@ function NavLink({
882
913
  icon,
883
914
  label,
884
915
  testId,
885
- linkComponent
916
+ linkComponent,
917
+ collapsed = false
886
918
  }) {
887
919
  const Icon = resolveIcon(icon);
888
920
  const LinkEl = linkComponent ?? "a";
@@ -892,8 +924,10 @@ function NavLink({
892
924
  href,
893
925
  "data-testid": testId,
894
926
  "aria-current": active ? "page" : void 0,
927
+ title: collapsed ? label : void 0,
895
928
  className: cn(
896
929
  "group flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm transition-colors",
930
+ collapsed && "justify-center px-0",
897
931
  active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
898
932
  ),
899
933
  children: [
@@ -905,7 +939,7 @@ function NavLink({
905
939
  "aria-hidden": "true"
906
940
  }
907
941
  ),
908
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "truncate", children: label })
942
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: cn("truncate", collapsed && "sr-only"), children: label })
909
943
  ]
910
944
  }
911
945
  );
@@ -921,16 +955,54 @@ function AppSidebar({
921
955
  ladderOptions,
922
956
  linkComponent,
923
957
  isActive,
958
+ collapsible = true,
959
+ defaultCollapsed = false,
960
+ onCollapsedChange,
961
+ storageKey = DEFAULT_COLLAPSE_STORAGE_KEY,
924
962
  className
925
963
  }) {
926
964
  const isActiveFn = isActive ?? isHrefActive;
927
965
  const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINTS.md}px)`);
966
+ const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
928
967
  const [mobileOpen, setMobileOpen] = (0, import_react8.useState)(false);
929
968
  const containerRef = (0, import_react8.useRef)(null);
930
969
  const drawerId = (0, import_react8.useId)();
931
970
  const mobileTriggerRef = (0, import_react8.useRef)(null);
932
971
  const mobileDrawerRef = (0, import_react8.useRef)(null);
933
972
  const mobileCloseRef = (0, import_react8.useRef)(null);
973
+ const [collapsed, setCollapsed] = (0, import_react8.useState)(defaultCollapsed);
974
+ const [collapseHydrated, setCollapseHydrated] = (0, import_react8.useState)(false);
975
+ (0, import_react8.useEffect)(() => {
976
+ if (!collapsible) return;
977
+ try {
978
+ const stored = localStorage.getItem(storageKey);
979
+ if (stored === "true") setCollapsed(true);
980
+ else if (stored === "false") setCollapsed(false);
981
+ } catch {
982
+ }
983
+ setCollapseHydrated(true);
984
+ }, []);
985
+ (0, import_react8.useEffect)(() => {
986
+ if (!collapsible || !collapseHydrated) return;
987
+ try {
988
+ localStorage.setItem(storageKey, String(collapsed));
989
+ } catch {
990
+ }
991
+ }, [collapsed, collapseHydrated, collapsible, storageKey]);
992
+ function handleToggleCollapsed() {
993
+ setCollapsed((prev) => {
994
+ const next = !prev;
995
+ onCollapsedChange?.(next);
996
+ return next;
997
+ });
998
+ }
999
+ const [openFlyoutId, setOpenFlyoutId] = (0, import_react8.useState)(null);
1000
+ const handleFlyoutOpenChange = (0, import_react8.useCallback)((id, isOpen) => {
1001
+ setOpenFlyoutId((current) => {
1002
+ if (isOpen) return id;
1003
+ return current === id ? null : current;
1004
+ });
1005
+ }, []);
934
1006
  const moduleMap = (0, import_react8.useMemo)(() => new Map(modules.map((m) => [m.moduleId, m])), [modules]);
935
1007
  const registryForLadder = (0, import_react8.useMemo)(
936
1008
  () => modules.map((m) => ({ moduleId: m.moduleId, group: m.group })),
@@ -952,7 +1024,7 @@ function AppSidebar({
952
1024
  triggerRef: mobileTriggerRef,
953
1025
  initialFocusRef: mobileCloseRef
954
1026
  });
955
- function renderAppItem(item, testIdPrefix) {
1027
+ function renderAppItem(item, testIdPrefix, collapsedRow = false) {
956
1028
  const def = moduleMap.get(item.moduleId);
957
1029
  if (!def) return null;
958
1030
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
@@ -963,12 +1035,13 @@ function AppSidebar({
963
1035
  icon: def.icon,
964
1036
  label: def.label,
965
1037
  testId: `${testIdPrefix}-${item.moduleId}`,
966
- linkComponent
1038
+ linkComponent,
1039
+ collapsed: collapsedRow
967
1040
  },
968
1041
  item.moduleId
969
1042
  );
970
1043
  }
971
- function renderGroupItem(item) {
1044
+ function renderGroupItem(item, collapsedRow = false) {
972
1045
  const children = item.children.map((moduleId) => {
973
1046
  const def = moduleMap.get(moduleId);
974
1047
  const href = def?.href ?? "#";
@@ -986,27 +1059,48 @@ function AppSidebar({
986
1059
  label: item.label,
987
1060
  icon: item.icon,
988
1061
  active,
989
- children,
990
- linkComponent
1062
+ linkComponent,
1063
+ collapsed: collapsedRow,
1064
+ forceClosed: openFlyoutId !== null && openFlyoutId !== item.id,
1065
+ onOpenChange: (isOpen) => handleFlyoutOpenChange(item.id, isOpen),
1066
+ children
991
1067
  },
992
1068
  item.id
993
1069
  );
994
1070
  }
995
- const brandBlock = brand && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
996
- "a",
997
- {
998
- href: brand.href,
999
- "data-testid": "app-sidebar-brand",
1000
- className: "flex items-center gap-2.5 border-b border-border px-5 py-4",
1001
- children: [
1002
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "grid size-7 place-items-center rounded-md bg-primary text-primary-foreground", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "font-mono text-sm font-bold", children: (brand.label[0] ?? "r").toUpperCase() }) }),
1003
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "truncate text-sm font-semibold tracking-tight", children: brand.label })
1004
- ]
1071
+ function brandBlockFor(collapsedMode) {
1072
+ if (!brand) return null;
1073
+ if (collapsedMode) {
1074
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1075
+ "a",
1076
+ {
1077
+ href: brand.href,
1078
+ "data-testid": "app-sidebar-brand",
1079
+ title: brand.label,
1080
+ className: "flex items-center justify-center border-b border-border px-1 py-4",
1081
+ children: [
1082
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "sr-only", children: brand.label }),
1083
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BrandGlyph, { brand: { name: brand.label }, size: "xl" })
1084
+ ]
1085
+ }
1086
+ );
1005
1087
  }
1006
- );
1007
- function topItemBlock(testId) {
1088
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1089
+ "a",
1090
+ {
1091
+ href: brand.href,
1092
+ "data-testid": "app-sidebar-brand",
1093
+ className: "flex items-center gap-2.5 border-b border-border px-5 py-4",
1094
+ children: [
1095
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "grid size-7 place-items-center rounded-md bg-primary text-primary-foreground", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "font-mono text-sm font-bold", children: (brand.label[0] ?? "r").toUpperCase() }) }),
1096
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "truncate text-sm font-semibold tracking-tight", children: brand.label })
1097
+ ]
1098
+ }
1099
+ );
1100
+ }
1101
+ function topItemBlock(testId, collapsedMode = false) {
1008
1102
  if (!topItem) return null;
1009
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "px-3 pt-4", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1103
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: cn("px-3 pt-4", collapsedMode && "flex justify-center px-0"), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1010
1104
  NavLink,
1011
1105
  {
1012
1106
  href: topItem.href,
@@ -1014,7 +1108,8 @@ function AppSidebar({
1014
1108
  icon: topItem.icon,
1015
1109
  label: topItem.label,
1016
1110
  testId,
1017
- linkComponent
1111
+ linkComponent,
1112
+ collapsed: collapsedMode
1018
1113
  }
1019
1114
  ) });
1020
1115
  }
@@ -1068,7 +1163,7 @@ function AppSidebar({
1068
1163
  children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react2.X, { className: "h-5 w-5", "aria-hidden": "true" })
1069
1164
  }
1070
1165
  ) }),
1071
- brandBlock,
1166
+ brandBlockFor(false),
1072
1167
  topItemBlock("app-sidebar-mobile-top-item"),
1073
1168
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("nav", { className: "flex-1 px-3 py-4", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex flex-col gap-0.5", children: rung0.items.map(
1074
1169
  (item) => item.type === "app" ? renderAppItem(item, "app-sidebar-mobile-navlink") : null
@@ -1081,19 +1176,47 @@ function AppSidebar({
1081
1176
  "aside",
1082
1177
  {
1083
1178
  "data-testid": "app-sidebar-desktop",
1084
- className: cn("hidden w-64 shrink-0 flex-col border-r border-border bg-sidebar md:flex", className),
1179
+ className: cn(
1180
+ "hidden shrink-0 flex-col border-r border-border bg-sidebar md:flex",
1181
+ collapsed ? COLLAPSED_WIDTH_CLASS : EXPANDED_WIDTH_CLASS,
1182
+ // DEV-5331: a plain CSS width transition is enough — no new deps
1183
+ // for a spring/JS animation. Respect prefers-reduced-motion.
1184
+ !prefersReducedMotion && "transition-[width] duration-200 ease-out",
1185
+ className
1186
+ ),
1085
1187
  children: [
1086
- brandBlock,
1087
- topItemBlock("app-sidebar-top-item"),
1188
+ brandBlockFor(collapsed),
1189
+ topItemBlock("app-sidebar-top-item", collapsed),
1088
1190
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1089
1191
  "nav",
1090
1192
  {
1091
1193
  ref: containerRef,
1092
1194
  "data-testid": "app-sidebar-nav",
1093
1195
  className: "flex-1 overflow-y-auto px-3 py-4",
1094
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex flex-col gap-0.5", children: rung.items.map((item) => item.type === "app" ? renderAppItem(item, "app-sidebar-navlink") : renderGroupItem(item)) })
1196
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex flex-col gap-0.5", children: rung.items.map(
1197
+ (item) => item.type === "app" ? renderAppItem(item, "app-sidebar-navlink", collapsed) : renderGroupItem(item, collapsed)
1198
+ ) })
1095
1199
  }
1096
- )
1200
+ ),
1201
+ collapsible && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "border-t border-border px-2 py-2", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1202
+ "button",
1203
+ {
1204
+ type: "button",
1205
+ "data-testid": "app-sidebar-collapse-toggle",
1206
+ "aria-pressed": collapsed,
1207
+ "aria-label": collapsed ? "Expand sidebar" : "Collapse sidebar",
1208
+ title: collapsed ? "Expand sidebar" : "Collapse sidebar",
1209
+ onClick: handleToggleCollapsed,
1210
+ className: cn(
1211
+ "flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-foreground/5 hover:text-foreground",
1212
+ collapsed && "justify-center px-0"
1213
+ ),
1214
+ children: [
1215
+ collapsed ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react2.PanelLeftOpen, { className: "size-4 shrink-0", strokeWidth: 1.5, "aria-hidden": "true" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react2.PanelLeftClose, { className: "size-4 shrink-0", strokeWidth: 1.5, "aria-hidden": "true" }),
1216
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: cn("truncate", collapsed && "sr-only"), children: collapsed ? "Expand sidebar" : "Collapse sidebar" })
1217
+ ]
1218
+ }
1219
+ ) })
1097
1220
  ]
1098
1221
  }
1099
1222
  )
@@ -1273,10 +1396,263 @@ function ContextSwitcher({
1273
1396
  )
1274
1397
  ] });
1275
1398
  }
1399
+
1400
+ // src/connector-picker.tsx
1401
+ var import_react9 = require("react");
1402
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1403
+ var DEFAULT_CONNECT_ERROR = "Couldn't start the connection. Please try again.";
1404
+ var DEFAULT_DISCONNECT_ERROR = "Couldn't disconnect. Please try again.";
1405
+ var DEFAULT_LOAD_ERROR = "Couldn't load connections. Please try again.";
1406
+ var SKELETON_COUNT = 3;
1407
+ function capabilityLabel(capability) {
1408
+ if (capability === "read") return "Read only";
1409
+ if (capability === "write") return "Write only";
1410
+ return "Read & write";
1411
+ }
1412
+ function formatSince(since) {
1413
+ const parsed = new Date(since);
1414
+ if (Number.isNaN(parsed.getTime())) return since;
1415
+ return parsed.toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
1416
+ }
1417
+ function errorMessageFrom(err, fallback) {
1418
+ return err instanceof Error && err.message ? err.message : fallback;
1419
+ }
1420
+ function ConnectorPicker({
1421
+ client,
1422
+ purpose,
1423
+ lens = "owner",
1424
+ layout = "tiles",
1425
+ onConnected
1426
+ }) {
1427
+ const gated = lens !== "owner";
1428
+ const [entries, setEntries] = (0, import_react9.useState)(null);
1429
+ const [loadError, setLoadError] = (0, import_react9.useState)(null);
1430
+ const [uiState, setUiState] = (0, import_react9.useState)({});
1431
+ const clientRef = (0, import_react9.useRef)(client);
1432
+ clientRef.current = client;
1433
+ const purposeRef = (0, import_react9.useRef)(purpose);
1434
+ purposeRef.current = purpose;
1435
+ const onConnectedRef = (0, import_react9.useRef)(onConnected);
1436
+ onConnectedRef.current = onConnected;
1437
+ const uiStateRef = (0, import_react9.useRef)(uiState);
1438
+ uiStateRef.current = uiState;
1439
+ const load = (0, import_react9.useCallback)(async () => {
1440
+ if (gated) return null;
1441
+ try {
1442
+ const list = await clientRef.current.listConnectable({ purpose: purposeRef.current });
1443
+ setEntries(list);
1444
+ setLoadError(null);
1445
+ return list;
1446
+ } catch (err) {
1447
+ setLoadError(errorMessageFrom(err, DEFAULT_LOAD_ERROR));
1448
+ return null;
1449
+ }
1450
+ }, [gated]);
1451
+ (0, import_react9.useEffect)(() => {
1452
+ if (gated) return;
1453
+ void load();
1454
+ }, [gated, load, purpose]);
1455
+ const refreshAfterReturn = (0, import_react9.useCallback)(async () => {
1456
+ const list = await load();
1457
+ if (!list) return;
1458
+ setUiState((prev) => {
1459
+ let changed = false;
1460
+ const next = { ...prev };
1461
+ for (const entry of list) {
1462
+ if (next[entry.slug]?.kind === "connecting" && entry.connection.status === "connected") {
1463
+ changed = true;
1464
+ delete next[entry.slug];
1465
+ onConnectedRef.current?.(entry);
1466
+ }
1467
+ }
1468
+ return changed ? next : prev;
1469
+ });
1470
+ }, [load]);
1471
+ (0, import_react9.useEffect)(() => {
1472
+ if (gated) return;
1473
+ function handleReturn() {
1474
+ if (document.visibilityState === "hidden") return;
1475
+ const hasPendingConnect = Object.values(uiStateRef.current).some(
1476
+ (s) => s.kind === "connecting"
1477
+ );
1478
+ if (!hasPendingConnect) return;
1479
+ void refreshAfterReturn();
1480
+ }
1481
+ window.addEventListener("focus", handleReturn);
1482
+ document.addEventListener("visibilitychange", handleReturn);
1483
+ return () => {
1484
+ window.removeEventListener("focus", handleReturn);
1485
+ document.removeEventListener("visibilitychange", handleReturn);
1486
+ };
1487
+ }, [gated, refreshAfterReturn]);
1488
+ const handleConnect = (0, import_react9.useCallback)(
1489
+ async (entry) => {
1490
+ setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "connecting" } }));
1491
+ try {
1492
+ const result = await clientRef.current.connect(entry);
1493
+ if (result?.connect_url) {
1494
+ window.open(result.connect_url, "_blank", "noopener,noreferrer");
1495
+ return;
1496
+ }
1497
+ const list = await load();
1498
+ const updated = list?.find((e) => e.slug === entry.slug) ?? null;
1499
+ setUiState((prev) => {
1500
+ const next = { ...prev };
1501
+ delete next[entry.slug];
1502
+ return next;
1503
+ });
1504
+ if (updated?.connection.status === "connected") {
1505
+ onConnectedRef.current?.(updated);
1506
+ }
1507
+ } catch (err) {
1508
+ const message = errorMessageFrom(err, DEFAULT_CONNECT_ERROR);
1509
+ setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "error", message } }));
1510
+ }
1511
+ },
1512
+ [load]
1513
+ );
1514
+ const handleDisconnect = (0, import_react9.useCallback)(
1515
+ async (entry) => {
1516
+ setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "disconnecting" } }));
1517
+ try {
1518
+ await clientRef.current.disconnect(entry);
1519
+ await load();
1520
+ setUiState((prev) => {
1521
+ const next = { ...prev };
1522
+ delete next[entry.slug];
1523
+ return next;
1524
+ });
1525
+ } catch (err) {
1526
+ const message = errorMessageFrom(err, DEFAULT_DISCONNECT_ERROR);
1527
+ setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "error", message } }));
1528
+ }
1529
+ },
1530
+ [load]
1531
+ );
1532
+ if (gated) return null;
1533
+ const containerClassName = cn(
1534
+ "flex flex-col",
1535
+ layout === "tiles" ? "gap-3 sm:grid sm:grid-cols-2 lg:grid-cols-3" : "divide-y divide-border"
1536
+ );
1537
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { "data-testid": "connector-picker", "data-layout": layout, className: containerClassName, children: [
1538
+ entries === null && !loadError && Array.from({ length: SKELETON_COUNT }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1539
+ "div",
1540
+ {
1541
+ "data-testid": "connector-picker-skeleton-item",
1542
+ "aria-hidden": "true",
1543
+ className: "flex items-center gap-3 rounded-lg border border-border p-3",
1544
+ children: [
1545
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "h-8 w-8 shrink-0 animate-pulse rounded bg-foreground/10" }),
1546
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "h-3 flex-1 animate-pulse rounded bg-foreground/10" })
1547
+ ]
1548
+ },
1549
+ i
1550
+ )),
1551
+ loadError && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1552
+ "div",
1553
+ {
1554
+ role: "alert",
1555
+ "data-testid": "connector-picker-load-error",
1556
+ className: "flex items-center justify-between gap-3 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive",
1557
+ children: [
1558
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: loadError }),
1559
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", onClick: () => void load(), className: "shrink-0 underline", children: "Retry" })
1560
+ ]
1561
+ }
1562
+ ),
1563
+ entries !== null && entries.length === 0 && !loadError && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { "data-testid": "connector-picker-empty", className: "text-sm text-muted-foreground", children: "No connections available." }),
1564
+ entries?.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1565
+ ConnectorTile,
1566
+ {
1567
+ entry,
1568
+ ui: uiState[entry.slug],
1569
+ layout,
1570
+ onConnect: handleConnect,
1571
+ onDisconnect: handleDisconnect
1572
+ },
1573
+ entry.slug
1574
+ ))
1575
+ ] });
1576
+ }
1577
+ function ConnectorTile({
1578
+ entry,
1579
+ ui,
1580
+ layout,
1581
+ onConnect,
1582
+ onDisconnect
1583
+ }) {
1584
+ const isConnected = entry.connection.status === "connected";
1585
+ const isConnecting = ui?.kind === "connecting";
1586
+ const isDisconnecting = ui?.kind === "disconnecting";
1587
+ const errorMessage = ui?.kind === "error" ? ui.message : null;
1588
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1589
+ "div",
1590
+ {
1591
+ "data-testid": `connector-tile-${entry.slug}`,
1592
+ "data-connected": isConnected || void 0,
1593
+ "data-deprecated": entry.deprecated || void 0,
1594
+ className: cn("flex items-center gap-3 p-3", layout === "tiles" && "rounded-lg border border-border"),
1595
+ children: [
1596
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(BrandGlyph, { brand: { name: entry.name, logo: entry.logo }, size: "lg" }),
1597
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "min-w-0 flex-1", children: [
1598
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center gap-2", children: [
1599
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "truncate text-sm font-medium", children: entry.name }),
1600
+ entry.deprecated && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1601
+ "span",
1602
+ {
1603
+ "data-testid": `connector-deprecated-${entry.slug}`,
1604
+ className: "shrink-0 rounded-full bg-foreground/10 px-2 py-0.5 text-[11px] text-muted-foreground",
1605
+ children: "No longer available"
1606
+ }
1607
+ )
1608
+ ] }),
1609
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
1610
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { "data-testid": `connector-capability-${entry.slug}`, children: capabilityLabel(entry.capability) }),
1611
+ entry.category && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
1612
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { "aria-hidden": "true", children: "\xB7" }),
1613
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "truncate", children: entry.category })
1614
+ ] })
1615
+ ] }),
1616
+ isConnected && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1617
+ "p",
1618
+ {
1619
+ "data-testid": `connector-connected-since-${entry.slug}`,
1620
+ className: "text-xs text-muted-foreground",
1621
+ children: entry.connection.since ? `Connected since ${formatSince(entry.connection.since)}` : "Connected"
1622
+ }
1623
+ ),
1624
+ errorMessage && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { role: "alert", "data-testid": `connector-error-${entry.slug}`, className: "text-xs text-destructive", children: errorMessage })
1625
+ ] }),
1626
+ isConnected ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1627
+ "button",
1628
+ {
1629
+ type: "button",
1630
+ "data-testid": `connector-disconnect-${entry.slug}`,
1631
+ onClick: () => onDisconnect(entry),
1632
+ disabled: isDisconnecting,
1633
+ className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-destructive hover:bg-destructive/10 disabled:opacity-50",
1634
+ children: isDisconnecting ? "Disconnecting\u2026" : "Disconnect"
1635
+ }
1636
+ ) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1637
+ "button",
1638
+ {
1639
+ type: "button",
1640
+ "data-testid": `connector-connect-${entry.slug}`,
1641
+ onClick: () => onConnect(entry),
1642
+ disabled: isConnecting,
1643
+ className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
1644
+ children: isConnecting ? "Connecting\u2026" : "Connect"
1645
+ }
1646
+ )
1647
+ ]
1648
+ }
1649
+ );
1650
+ }
1276
1651
  // Annotate the CommonJS export names for ESM import in node:
1277
1652
  0 && (module.exports = {
1278
1653
  AppSidebar,
1279
1654
  BrandGlyph,
1655
+ ConnectorPicker,
1280
1656
  ContextSwitcher,
1281
1657
  Flyout,
1282
1658
  GLYPH_CANVAS_FILL,