@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.mjs CHANGED
@@ -36,6 +36,7 @@ var WORDMARK_WEIGHT = 800;
36
36
  var WORDMARK_WEIGHT_CLASS = "font-extrabold";
37
37
  var WORDMARK_TEXT_LG = "text-base";
38
38
  var MONOGRAM_TEXT_LG = "text-[17px]";
39
+ var MONOGRAM_TEXT_XL = "text-[26px]";
39
40
  var LOCKUP_GAP = "gap-2";
40
41
  var LOCKUP_ROW = "flex items-baseline";
41
42
  var GLYPH_CANVAS_FILL = 0.65;
@@ -75,9 +76,9 @@ function faviconUrl(domain) {
75
76
  return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(host)}&sz=128`;
76
77
  }
77
78
  function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
78
- const box = size === "lg" ? "h-6 w-6" : "h-5 w-5";
79
- const img = size === "lg" ? "h-4 w-4" : "h-3.5 w-3.5";
80
- const dim = size === "lg" ? 16 : 14;
79
+ const box = size === "xl" ? "h-10 w-10" : size === "lg" ? "h-6 w-6" : "h-5 w-5";
80
+ const img = size === "xl" ? "h-7 w-7" : size === "lg" ? "h-4 w-4" : "h-3.5 w-3.5";
81
+ const dim = size === "xl" ? 28 : size === "lg" ? 16 : 14;
81
82
  const candidates = [];
82
83
  if (brand.favicon) candidates.push(brand.favicon);
83
84
  if (brand.logo) candidates.push(brand.logo);
@@ -130,8 +131,10 @@ function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
130
131
  // DEV-5139 rule 2: at "lg" the letter runs 17px = the text-base
131
132
  // wordmark (16px) × MARK_CAP_RATIO (1.08) — a lone round glyph at
132
133
  // exact parity optically reads small. "sm" has no wordmark partner
133
- // in its surfaces; SIZE unchanged pending a lockup audit there.
134
- size === "lg" ? MONOGRAM_TEXT_LG : "text-[11px]",
134
+ // in its surfaces; unchanged pending a lockup audit there. "xl"
135
+ // (DEV-5331) is a standalone glyph with no wordmark at all — rule 5
136
+ // (GLYPH_CANVAS_FILL) governs it instead of rule 2.
137
+ size === "xl" ? MONOGRAM_TEXT_XL : size === "lg" ? MONOGRAM_TEXT_LG : "text-[11px]",
135
138
  box,
136
139
  className
137
140
  ),
@@ -415,8 +418,8 @@ function useResponsivePlaceholder(desktop, mobile) {
415
418
  }
416
419
 
417
420
  // src/app-sidebar.tsx
418
- import { useEffect as useEffect5, useId as useId3, useMemo, useRef as useRef4, useState as useState6 } from "react";
419
- import { Menu as Menu2, X as X2 } from "lucide-react";
421
+ import { useCallback as useCallback3, useEffect as useEffect5, useId as useId3, useMemo, useRef as useRef4, useState as useState6 } from "react";
422
+ import { Menu as Menu2, PanelLeftClose, PanelLeftOpen, X as X2 } from "lucide-react";
420
423
 
421
424
  // src/lib/resolve-icon.ts
422
425
  import * as LucideIcons from "lucide-react";
@@ -698,11 +701,22 @@ function useLadderRung(ladder, containerRef, options = {}) {
698
701
  import { useCallback as useCallback2, useEffect as useEffect4, useId as useId2, useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState5 } from "react";
699
702
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
700
703
  var PANEL_GAP = 4;
701
- function Flyout({ label, icon, active, children, linkComponent, className }) {
704
+ function Flyout({
705
+ label,
706
+ icon,
707
+ active,
708
+ children,
709
+ linkComponent,
710
+ collapsed = false,
711
+ onOpenChange,
712
+ forceClosed = false,
713
+ className
714
+ }) {
702
715
  const [hoverOpen, setHoverOpen] = useState5(false);
703
716
  const [clickOpen, setClickOpen] = useState5(false);
704
717
  const [panelPosition, setPanelPosition] = useState5(null);
705
- const open = hoverOpen || clickOpen;
718
+ const wantsOpen = hoverOpen || clickOpen;
719
+ const open = wantsOpen && !forceClosed;
706
720
  const panelId = useId2();
707
721
  const triggerRef = useRef3(null);
708
722
  const panelRef = useRef3(null);
@@ -714,6 +728,17 @@ function Flyout({ label, icon, active, children, linkComponent, className }) {
714
728
  const rect = triggerRef.current?.getBoundingClientRect();
715
729
  if (rect) setPanelPosition({ top: rect.top, left: rect.right + PANEL_GAP });
716
730
  }, []);
731
+ const onOpenChangeRef = useRef3(onOpenChange);
732
+ onOpenChangeRef.current = onOpenChange;
733
+ useEffect4(() => {
734
+ onOpenChangeRef.current?.(wantsOpen);
735
+ }, [wantsOpen]);
736
+ useEffect4(() => {
737
+ if (forceClosed) {
738
+ setHoverOpen(false);
739
+ setClickOpen(false);
740
+ }
741
+ }, [forceClosed]);
717
742
  useLayoutEffect2(() => {
718
743
  if (open) reposition();
719
744
  }, [open, reposition]);
@@ -754,9 +779,11 @@ function Flyout({ label, icon, active, children, linkComponent, className }) {
754
779
  "data-testid": "ladder-flyout-trigger",
755
780
  "aria-expanded": open,
756
781
  "aria-controls": panelId,
782
+ title: collapsed ? label : void 0,
757
783
  onClick: () => setClickOpen((v) => !v),
758
784
  className: cn(
759
785
  "group flex w-full items-center gap-2.5 rounded-md px-3 py-1.5 text-sm transition-colors",
786
+ collapsed && "justify-center px-0",
760
787
  active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
761
788
  ),
762
789
  children: [
@@ -768,7 +795,7 @@ function Flyout({ label, icon, active, children, linkComponent, className }) {
768
795
  "aria-hidden": "true"
769
796
  }
770
797
  ),
771
- /* @__PURE__ */ jsx3("span", { className: "truncate", children: label })
798
+ /* @__PURE__ */ jsx3("span", { className: cn("truncate", collapsed && "sr-only"), children: label })
772
799
  ]
773
800
  }
774
801
  ),
@@ -819,6 +846,9 @@ function Flyout({ label, icon, active, children, linkComponent, className }) {
819
846
 
820
847
  // src/app-sidebar.tsx
821
848
  import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
849
+ var DEFAULT_COLLAPSE_STORAGE_KEY = "reeve-sidebar-collapsed";
850
+ var COLLAPSED_WIDTH_CLASS = "w-14";
851
+ var EXPANDED_WIDTH_CLASS = "w-64";
822
852
  function isHrefActive(href, activePath) {
823
853
  return activePath === href || activePath.startsWith(`${href}/`);
824
854
  }
@@ -828,7 +858,8 @@ function NavLink({
828
858
  icon,
829
859
  label,
830
860
  testId,
831
- linkComponent
861
+ linkComponent,
862
+ collapsed = false
832
863
  }) {
833
864
  const Icon = resolveIcon(icon);
834
865
  const LinkEl = linkComponent ?? "a";
@@ -838,8 +869,10 @@ function NavLink({
838
869
  href,
839
870
  "data-testid": testId,
840
871
  "aria-current": active ? "page" : void 0,
872
+ title: collapsed ? label : void 0,
841
873
  className: cn(
842
874
  "group flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm transition-colors",
875
+ collapsed && "justify-center px-0",
843
876
  active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
844
877
  ),
845
878
  children: [
@@ -851,7 +884,7 @@ function NavLink({
851
884
  "aria-hidden": "true"
852
885
  }
853
886
  ),
854
- /* @__PURE__ */ jsx4("span", { className: "truncate", children: label })
887
+ /* @__PURE__ */ jsx4("span", { className: cn("truncate", collapsed && "sr-only"), children: label })
855
888
  ]
856
889
  }
857
890
  );
@@ -867,16 +900,54 @@ function AppSidebar({
867
900
  ladderOptions,
868
901
  linkComponent,
869
902
  isActive,
903
+ collapsible = true,
904
+ defaultCollapsed = false,
905
+ onCollapsedChange,
906
+ storageKey = DEFAULT_COLLAPSE_STORAGE_KEY,
870
907
  className
871
908
  }) {
872
909
  const isActiveFn = isActive ?? isHrefActive;
873
910
  const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINTS.md}px)`);
911
+ const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
874
912
  const [mobileOpen, setMobileOpen] = useState6(false);
875
913
  const containerRef = useRef4(null);
876
914
  const drawerId = useId3();
877
915
  const mobileTriggerRef = useRef4(null);
878
916
  const mobileDrawerRef = useRef4(null);
879
917
  const mobileCloseRef = useRef4(null);
918
+ const [collapsed, setCollapsed] = useState6(defaultCollapsed);
919
+ const [collapseHydrated, setCollapseHydrated] = useState6(false);
920
+ useEffect5(() => {
921
+ if (!collapsible) return;
922
+ try {
923
+ const stored = localStorage.getItem(storageKey);
924
+ if (stored === "true") setCollapsed(true);
925
+ else if (stored === "false") setCollapsed(false);
926
+ } catch {
927
+ }
928
+ setCollapseHydrated(true);
929
+ }, []);
930
+ useEffect5(() => {
931
+ if (!collapsible || !collapseHydrated) return;
932
+ try {
933
+ localStorage.setItem(storageKey, String(collapsed));
934
+ } catch {
935
+ }
936
+ }, [collapsed, collapseHydrated, collapsible, storageKey]);
937
+ function handleToggleCollapsed() {
938
+ setCollapsed((prev) => {
939
+ const next = !prev;
940
+ onCollapsedChange?.(next);
941
+ return next;
942
+ });
943
+ }
944
+ const [openFlyoutId, setOpenFlyoutId] = useState6(null);
945
+ const handleFlyoutOpenChange = useCallback3((id, isOpen) => {
946
+ setOpenFlyoutId((current) => {
947
+ if (isOpen) return id;
948
+ return current === id ? null : current;
949
+ });
950
+ }, []);
880
951
  const moduleMap = useMemo(() => new Map(modules.map((m) => [m.moduleId, m])), [modules]);
881
952
  const registryForLadder = useMemo(
882
953
  () => modules.map((m) => ({ moduleId: m.moduleId, group: m.group })),
@@ -898,7 +969,7 @@ function AppSidebar({
898
969
  triggerRef: mobileTriggerRef,
899
970
  initialFocusRef: mobileCloseRef
900
971
  });
901
- function renderAppItem(item, testIdPrefix) {
972
+ function renderAppItem(item, testIdPrefix, collapsedRow = false) {
902
973
  const def = moduleMap.get(item.moduleId);
903
974
  if (!def) return null;
904
975
  return /* @__PURE__ */ jsx4(
@@ -909,12 +980,13 @@ function AppSidebar({
909
980
  icon: def.icon,
910
981
  label: def.label,
911
982
  testId: `${testIdPrefix}-${item.moduleId}`,
912
- linkComponent
983
+ linkComponent,
984
+ collapsed: collapsedRow
913
985
  },
914
986
  item.moduleId
915
987
  );
916
988
  }
917
- function renderGroupItem(item) {
989
+ function renderGroupItem(item, collapsedRow = false) {
918
990
  const children = item.children.map((moduleId) => {
919
991
  const def = moduleMap.get(moduleId);
920
992
  const href = def?.href ?? "#";
@@ -932,27 +1004,48 @@ function AppSidebar({
932
1004
  label: item.label,
933
1005
  icon: item.icon,
934
1006
  active,
935
- children,
936
- linkComponent
1007
+ linkComponent,
1008
+ collapsed: collapsedRow,
1009
+ forceClosed: openFlyoutId !== null && openFlyoutId !== item.id,
1010
+ onOpenChange: (isOpen) => handleFlyoutOpenChange(item.id, isOpen),
1011
+ children
937
1012
  },
938
1013
  item.id
939
1014
  );
940
1015
  }
941
- const brandBlock = brand && /* @__PURE__ */ jsxs4(
942
- "a",
943
- {
944
- href: brand.href,
945
- "data-testid": "app-sidebar-brand",
946
- className: "flex items-center gap-2.5 border-b border-border px-5 py-4",
947
- children: [
948
- /* @__PURE__ */ jsx4("span", { className: "grid size-7 place-items-center rounded-md bg-primary text-primary-foreground", children: /* @__PURE__ */ jsx4("span", { className: "font-mono text-sm font-bold", children: (brand.label[0] ?? "r").toUpperCase() }) }),
949
- /* @__PURE__ */ jsx4("span", { className: "truncate text-sm font-semibold tracking-tight", children: brand.label })
950
- ]
1016
+ function brandBlockFor(collapsedMode) {
1017
+ if (!brand) return null;
1018
+ if (collapsedMode) {
1019
+ return /* @__PURE__ */ jsxs4(
1020
+ "a",
1021
+ {
1022
+ href: brand.href,
1023
+ "data-testid": "app-sidebar-brand",
1024
+ title: brand.label,
1025
+ className: "flex items-center justify-center border-b border-border px-1 py-4",
1026
+ children: [
1027
+ /* @__PURE__ */ jsx4("span", { className: "sr-only", children: brand.label }),
1028
+ /* @__PURE__ */ jsx4(BrandGlyph, { brand: { name: brand.label }, size: "xl" })
1029
+ ]
1030
+ }
1031
+ );
951
1032
  }
952
- );
953
- function topItemBlock(testId) {
1033
+ return /* @__PURE__ */ jsxs4(
1034
+ "a",
1035
+ {
1036
+ href: brand.href,
1037
+ "data-testid": "app-sidebar-brand",
1038
+ className: "flex items-center gap-2.5 border-b border-border px-5 py-4",
1039
+ children: [
1040
+ /* @__PURE__ */ jsx4("span", { className: "grid size-7 place-items-center rounded-md bg-primary text-primary-foreground", children: /* @__PURE__ */ jsx4("span", { className: "font-mono text-sm font-bold", children: (brand.label[0] ?? "r").toUpperCase() }) }),
1041
+ /* @__PURE__ */ jsx4("span", { className: "truncate text-sm font-semibold tracking-tight", children: brand.label })
1042
+ ]
1043
+ }
1044
+ );
1045
+ }
1046
+ function topItemBlock(testId, collapsedMode = false) {
954
1047
  if (!topItem) return null;
955
- return /* @__PURE__ */ jsx4("div", { className: "px-3 pt-4", children: /* @__PURE__ */ jsx4(
1048
+ return /* @__PURE__ */ jsx4("div", { className: cn("px-3 pt-4", collapsedMode && "flex justify-center px-0"), children: /* @__PURE__ */ jsx4(
956
1049
  NavLink,
957
1050
  {
958
1051
  href: topItem.href,
@@ -960,7 +1053,8 @@ function AppSidebar({
960
1053
  icon: topItem.icon,
961
1054
  label: topItem.label,
962
1055
  testId,
963
- linkComponent
1056
+ linkComponent,
1057
+ collapsed: collapsedMode
964
1058
  }
965
1059
  ) });
966
1060
  }
@@ -1014,7 +1108,7 @@ function AppSidebar({
1014
1108
  children: /* @__PURE__ */ jsx4(X2, { className: "h-5 w-5", "aria-hidden": "true" })
1015
1109
  }
1016
1110
  ) }),
1017
- brandBlock,
1111
+ brandBlockFor(false),
1018
1112
  topItemBlock("app-sidebar-mobile-top-item"),
1019
1113
  /* @__PURE__ */ jsx4("nav", { className: "flex-1 px-3 py-4", children: /* @__PURE__ */ jsx4("div", { className: "flex flex-col gap-0.5", children: rung0.items.map(
1020
1114
  (item) => item.type === "app" ? renderAppItem(item, "app-sidebar-mobile-navlink") : null
@@ -1027,19 +1121,47 @@ function AppSidebar({
1027
1121
  "aside",
1028
1122
  {
1029
1123
  "data-testid": "app-sidebar-desktop",
1030
- className: cn("hidden w-64 shrink-0 flex-col border-r border-border bg-sidebar md:flex", className),
1124
+ className: cn(
1125
+ "hidden shrink-0 flex-col border-r border-border bg-sidebar md:flex",
1126
+ collapsed ? COLLAPSED_WIDTH_CLASS : EXPANDED_WIDTH_CLASS,
1127
+ // DEV-5331: a plain CSS width transition is enough — no new deps
1128
+ // for a spring/JS animation. Respect prefers-reduced-motion.
1129
+ !prefersReducedMotion && "transition-[width] duration-200 ease-out",
1130
+ className
1131
+ ),
1031
1132
  children: [
1032
- brandBlock,
1033
- topItemBlock("app-sidebar-top-item"),
1133
+ brandBlockFor(collapsed),
1134
+ topItemBlock("app-sidebar-top-item", collapsed),
1034
1135
  /* @__PURE__ */ jsx4(
1035
1136
  "nav",
1036
1137
  {
1037
1138
  ref: containerRef,
1038
1139
  "data-testid": "app-sidebar-nav",
1039
1140
  className: "flex-1 overflow-y-auto px-3 py-4",
1040
- children: /* @__PURE__ */ jsx4("div", { className: "flex flex-col gap-0.5", children: rung.items.map((item) => item.type === "app" ? renderAppItem(item, "app-sidebar-navlink") : renderGroupItem(item)) })
1141
+ children: /* @__PURE__ */ jsx4("div", { className: "flex flex-col gap-0.5", children: rung.items.map(
1142
+ (item) => item.type === "app" ? renderAppItem(item, "app-sidebar-navlink", collapsed) : renderGroupItem(item, collapsed)
1143
+ ) })
1041
1144
  }
1042
- )
1145
+ ),
1146
+ collapsible && /* @__PURE__ */ jsx4("div", { className: "border-t border-border px-2 py-2", children: /* @__PURE__ */ jsxs4(
1147
+ "button",
1148
+ {
1149
+ type: "button",
1150
+ "data-testid": "app-sidebar-collapse-toggle",
1151
+ "aria-pressed": collapsed,
1152
+ "aria-label": collapsed ? "Expand sidebar" : "Collapse sidebar",
1153
+ title: collapsed ? "Expand sidebar" : "Collapse sidebar",
1154
+ onClick: handleToggleCollapsed,
1155
+ className: cn(
1156
+ "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",
1157
+ collapsed && "justify-center px-0"
1158
+ ),
1159
+ children: [
1160
+ collapsed ? /* @__PURE__ */ jsx4(PanelLeftOpen, { className: "size-4 shrink-0", strokeWidth: 1.5, "aria-hidden": "true" }) : /* @__PURE__ */ jsx4(PanelLeftClose, { className: "size-4 shrink-0", strokeWidth: 1.5, "aria-hidden": "true" }),
1161
+ /* @__PURE__ */ jsx4("span", { className: cn("truncate", collapsed && "sr-only"), children: collapsed ? "Expand sidebar" : "Collapse sidebar" })
1162
+ ]
1163
+ }
1164
+ ) })
1043
1165
  ]
1044
1166
  }
1045
1167
  )
@@ -1219,9 +1341,262 @@ function ContextSwitcher({
1219
1341
  )
1220
1342
  ] });
1221
1343
  }
1344
+
1345
+ // src/connector-picker.tsx
1346
+ import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef5, useState as useState7 } from "react";
1347
+ import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1348
+ var DEFAULT_CONNECT_ERROR = "Couldn't start the connection. Please try again.";
1349
+ var DEFAULT_DISCONNECT_ERROR = "Couldn't disconnect. Please try again.";
1350
+ var DEFAULT_LOAD_ERROR = "Couldn't load connections. Please try again.";
1351
+ var SKELETON_COUNT = 3;
1352
+ function capabilityLabel(capability) {
1353
+ if (capability === "read") return "Read only";
1354
+ if (capability === "write") return "Write only";
1355
+ return "Read & write";
1356
+ }
1357
+ function formatSince(since) {
1358
+ const parsed = new Date(since);
1359
+ if (Number.isNaN(parsed.getTime())) return since;
1360
+ return parsed.toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
1361
+ }
1362
+ function errorMessageFrom(err, fallback) {
1363
+ return err instanceof Error && err.message ? err.message : fallback;
1364
+ }
1365
+ function ConnectorPicker({
1366
+ client,
1367
+ purpose,
1368
+ lens = "owner",
1369
+ layout = "tiles",
1370
+ onConnected
1371
+ }) {
1372
+ const gated = lens !== "owner";
1373
+ const [entries, setEntries] = useState7(null);
1374
+ const [loadError, setLoadError] = useState7(null);
1375
+ const [uiState, setUiState] = useState7({});
1376
+ const clientRef = useRef5(client);
1377
+ clientRef.current = client;
1378
+ const purposeRef = useRef5(purpose);
1379
+ purposeRef.current = purpose;
1380
+ const onConnectedRef = useRef5(onConnected);
1381
+ onConnectedRef.current = onConnected;
1382
+ const uiStateRef = useRef5(uiState);
1383
+ uiStateRef.current = uiState;
1384
+ const load = useCallback4(async () => {
1385
+ if (gated) return null;
1386
+ try {
1387
+ const list = await clientRef.current.listConnectable({ purpose: purposeRef.current });
1388
+ setEntries(list);
1389
+ setLoadError(null);
1390
+ return list;
1391
+ } catch (err) {
1392
+ setLoadError(errorMessageFrom(err, DEFAULT_LOAD_ERROR));
1393
+ return null;
1394
+ }
1395
+ }, [gated]);
1396
+ useEffect6(() => {
1397
+ if (gated) return;
1398
+ void load();
1399
+ }, [gated, load, purpose]);
1400
+ const refreshAfterReturn = useCallback4(async () => {
1401
+ const list = await load();
1402
+ if (!list) return;
1403
+ setUiState((prev) => {
1404
+ let changed = false;
1405
+ const next = { ...prev };
1406
+ for (const entry of list) {
1407
+ if (next[entry.slug]?.kind === "connecting" && entry.connection.status === "connected") {
1408
+ changed = true;
1409
+ delete next[entry.slug];
1410
+ onConnectedRef.current?.(entry);
1411
+ }
1412
+ }
1413
+ return changed ? next : prev;
1414
+ });
1415
+ }, [load]);
1416
+ useEffect6(() => {
1417
+ if (gated) return;
1418
+ function handleReturn() {
1419
+ if (document.visibilityState === "hidden") return;
1420
+ const hasPendingConnect = Object.values(uiStateRef.current).some(
1421
+ (s) => s.kind === "connecting"
1422
+ );
1423
+ if (!hasPendingConnect) return;
1424
+ void refreshAfterReturn();
1425
+ }
1426
+ window.addEventListener("focus", handleReturn);
1427
+ document.addEventListener("visibilitychange", handleReturn);
1428
+ return () => {
1429
+ window.removeEventListener("focus", handleReturn);
1430
+ document.removeEventListener("visibilitychange", handleReturn);
1431
+ };
1432
+ }, [gated, refreshAfterReturn]);
1433
+ const handleConnect = useCallback4(
1434
+ async (entry) => {
1435
+ setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "connecting" } }));
1436
+ try {
1437
+ const result = await clientRef.current.connect(entry);
1438
+ if (result?.connect_url) {
1439
+ window.open(result.connect_url, "_blank", "noopener,noreferrer");
1440
+ return;
1441
+ }
1442
+ const list = await load();
1443
+ const updated = list?.find((e) => e.slug === entry.slug) ?? null;
1444
+ setUiState((prev) => {
1445
+ const next = { ...prev };
1446
+ delete next[entry.slug];
1447
+ return next;
1448
+ });
1449
+ if (updated?.connection.status === "connected") {
1450
+ onConnectedRef.current?.(updated);
1451
+ }
1452
+ } catch (err) {
1453
+ const message = errorMessageFrom(err, DEFAULT_CONNECT_ERROR);
1454
+ setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "error", message } }));
1455
+ }
1456
+ },
1457
+ [load]
1458
+ );
1459
+ const handleDisconnect = useCallback4(
1460
+ async (entry) => {
1461
+ setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "disconnecting" } }));
1462
+ try {
1463
+ await clientRef.current.disconnect(entry);
1464
+ await load();
1465
+ setUiState((prev) => {
1466
+ const next = { ...prev };
1467
+ delete next[entry.slug];
1468
+ return next;
1469
+ });
1470
+ } catch (err) {
1471
+ const message = errorMessageFrom(err, DEFAULT_DISCONNECT_ERROR);
1472
+ setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "error", message } }));
1473
+ }
1474
+ },
1475
+ [load]
1476
+ );
1477
+ if (gated) return null;
1478
+ const containerClassName = cn(
1479
+ "flex flex-col",
1480
+ layout === "tiles" ? "gap-3 sm:grid sm:grid-cols-2 lg:grid-cols-3" : "divide-y divide-border"
1481
+ );
1482
+ return /* @__PURE__ */ jsxs6("div", { "data-testid": "connector-picker", "data-layout": layout, className: containerClassName, children: [
1483
+ entries === null && !loadError && Array.from({ length: SKELETON_COUNT }).map((_, i) => /* @__PURE__ */ jsxs6(
1484
+ "div",
1485
+ {
1486
+ "data-testid": "connector-picker-skeleton-item",
1487
+ "aria-hidden": "true",
1488
+ className: "flex items-center gap-3 rounded-lg border border-border p-3",
1489
+ children: [
1490
+ /* @__PURE__ */ jsx7("span", { className: "h-8 w-8 shrink-0 animate-pulse rounded bg-foreground/10" }),
1491
+ /* @__PURE__ */ jsx7("span", { className: "h-3 flex-1 animate-pulse rounded bg-foreground/10" })
1492
+ ]
1493
+ },
1494
+ i
1495
+ )),
1496
+ loadError && /* @__PURE__ */ jsxs6(
1497
+ "div",
1498
+ {
1499
+ role: "alert",
1500
+ "data-testid": "connector-picker-load-error",
1501
+ className: "flex items-center justify-between gap-3 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive",
1502
+ children: [
1503
+ /* @__PURE__ */ jsx7("span", { children: loadError }),
1504
+ /* @__PURE__ */ jsx7("button", { type: "button", onClick: () => void load(), className: "shrink-0 underline", children: "Retry" })
1505
+ ]
1506
+ }
1507
+ ),
1508
+ entries !== null && entries.length === 0 && !loadError && /* @__PURE__ */ jsx7("p", { "data-testid": "connector-picker-empty", className: "text-sm text-muted-foreground", children: "No connections available." }),
1509
+ entries?.map((entry) => /* @__PURE__ */ jsx7(
1510
+ ConnectorTile,
1511
+ {
1512
+ entry,
1513
+ ui: uiState[entry.slug],
1514
+ layout,
1515
+ onConnect: handleConnect,
1516
+ onDisconnect: handleDisconnect
1517
+ },
1518
+ entry.slug
1519
+ ))
1520
+ ] });
1521
+ }
1522
+ function ConnectorTile({
1523
+ entry,
1524
+ ui,
1525
+ layout,
1526
+ onConnect,
1527
+ onDisconnect
1528
+ }) {
1529
+ const isConnected = entry.connection.status === "connected";
1530
+ const isConnecting = ui?.kind === "connecting";
1531
+ const isDisconnecting = ui?.kind === "disconnecting";
1532
+ const errorMessage = ui?.kind === "error" ? ui.message : null;
1533
+ return /* @__PURE__ */ jsxs6(
1534
+ "div",
1535
+ {
1536
+ "data-testid": `connector-tile-${entry.slug}`,
1537
+ "data-connected": isConnected || void 0,
1538
+ "data-deprecated": entry.deprecated || void 0,
1539
+ className: cn("flex items-center gap-3 p-3", layout === "tiles" && "rounded-lg border border-border"),
1540
+ children: [
1541
+ /* @__PURE__ */ jsx7(BrandGlyph, { brand: { name: entry.name, logo: entry.logo }, size: "lg" }),
1542
+ /* @__PURE__ */ jsxs6("div", { className: "min-w-0 flex-1", children: [
1543
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", children: [
1544
+ /* @__PURE__ */ jsx7("p", { className: "truncate text-sm font-medium", children: entry.name }),
1545
+ entry.deprecated && /* @__PURE__ */ jsx7(
1546
+ "span",
1547
+ {
1548
+ "data-testid": `connector-deprecated-${entry.slug}`,
1549
+ className: "shrink-0 rounded-full bg-foreground/10 px-2 py-0.5 text-[11px] text-muted-foreground",
1550
+ children: "No longer available"
1551
+ }
1552
+ )
1553
+ ] }),
1554
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
1555
+ /* @__PURE__ */ jsx7("span", { "data-testid": `connector-capability-${entry.slug}`, children: capabilityLabel(entry.capability) }),
1556
+ entry.category && /* @__PURE__ */ jsxs6(Fragment3, { children: [
1557
+ /* @__PURE__ */ jsx7("span", { "aria-hidden": "true", children: "\xB7" }),
1558
+ /* @__PURE__ */ jsx7("span", { className: "truncate", children: entry.category })
1559
+ ] })
1560
+ ] }),
1561
+ isConnected && /* @__PURE__ */ jsx7(
1562
+ "p",
1563
+ {
1564
+ "data-testid": `connector-connected-since-${entry.slug}`,
1565
+ className: "text-xs text-muted-foreground",
1566
+ children: entry.connection.since ? `Connected since ${formatSince(entry.connection.since)}` : "Connected"
1567
+ }
1568
+ ),
1569
+ errorMessage && /* @__PURE__ */ jsx7("p", { role: "alert", "data-testid": `connector-error-${entry.slug}`, className: "text-xs text-destructive", children: errorMessage })
1570
+ ] }),
1571
+ isConnected ? /* @__PURE__ */ jsx7(
1572
+ "button",
1573
+ {
1574
+ type: "button",
1575
+ "data-testid": `connector-disconnect-${entry.slug}`,
1576
+ onClick: () => onDisconnect(entry),
1577
+ disabled: isDisconnecting,
1578
+ className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-destructive hover:bg-destructive/10 disabled:opacity-50",
1579
+ children: isDisconnecting ? "Disconnecting\u2026" : "Disconnect"
1580
+ }
1581
+ ) : /* @__PURE__ */ jsx7(
1582
+ "button",
1583
+ {
1584
+ type: "button",
1585
+ "data-testid": `connector-connect-${entry.slug}`,
1586
+ onClick: () => onConnect(entry),
1587
+ disabled: isConnecting,
1588
+ className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
1589
+ children: isConnecting ? "Connecting\u2026" : "Connect"
1590
+ }
1591
+ )
1592
+ ]
1593
+ }
1594
+ );
1595
+ }
1222
1596
  export {
1223
1597
  AppSidebar,
1224
1598
  BrandGlyph,
1599
+ ConnectorPicker,
1225
1600
  ContextSwitcher,
1226
1601
  Flyout,
1227
1602
  GLYPH_CANVAS_FILL,
@@ -51,6 +51,17 @@ declare const WORDMARK_WEIGHT_CLASS = "font-extrabold";
51
51
  */
52
52
  declare const WORDMARK_TEXT_LG = "text-base";
53
53
  declare const MONOGRAM_TEXT_LG = "text-[17px]";
54
+ /**
55
+ * "xl" — a bare, STANDALONE glyph slot with no adjacent wordmark at all
56
+ * (e.g. AppSidebar's collapsed 56px icon rail, DEV-5331 — Matt's live
57
+ * review found the "lg" monogram too small there). No wordmark to pair
58
+ * with means rule 2 (MARK_CAP_RATIO) doesn't apply; rule 5
59
+ * (GLYPH_CANVAS_FILL) does instead — same governing rule as a favicon/
60
+ * avatar tile. Canvas = 40px (`h-10 w-10`); letter cap height = 40 ×
61
+ * GLYPH_CANVAS_FILL (0.65) = 26px, landing in Matt's requested ~24-28px
62
+ * range for a rail comfortably wider than the glyph itself.
63
+ */
64
+ declare const MONOGRAM_TEXT_XL = "text-[26px]";
54
65
  /**
55
66
  * Rule 3, applied: 8px gap ≈ 0.70× the wordmark's ~11.5px cap height (at
56
67
  * text-base) — inside the 0.5–0.8 band. Named so nobody "fixes" it
@@ -65,4 +76,4 @@ declare const LOCKUP_ROW = "flex items-baseline";
65
76
  /** Rule 5 — standalone glyph cap height as a fraction of the canvas edge. */
66
77
  declare const GLYPH_CANVAS_FILL = 0.65;
67
78
 
68
- export { GLYPH_CANVAS_FILL, LOCKUP_GAP, LOCKUP_ROW, MARK_CAP_RATIO, MARK_WEIGHT, MARK_WEIGHT_CLASS, MONOGRAM_TEXT_LG, WORDMARK_TEXT_LG, WORDMARK_WEIGHT, WORDMARK_WEIGHT_CLASS };
79
+ export { GLYPH_CANVAS_FILL, LOCKUP_GAP, LOCKUP_ROW, MARK_CAP_RATIO, MARK_WEIGHT, MARK_WEIGHT_CLASS, MONOGRAM_TEXT_LG, MONOGRAM_TEXT_XL, WORDMARK_TEXT_LG, WORDMARK_WEIGHT, WORDMARK_WEIGHT_CLASS };
@@ -51,6 +51,17 @@ declare const WORDMARK_WEIGHT_CLASS = "font-extrabold";
51
51
  */
52
52
  declare const WORDMARK_TEXT_LG = "text-base";
53
53
  declare const MONOGRAM_TEXT_LG = "text-[17px]";
54
+ /**
55
+ * "xl" — a bare, STANDALONE glyph slot with no adjacent wordmark at all
56
+ * (e.g. AppSidebar's collapsed 56px icon rail, DEV-5331 — Matt's live
57
+ * review found the "lg" monogram too small there). No wordmark to pair
58
+ * with means rule 2 (MARK_CAP_RATIO) doesn't apply; rule 5
59
+ * (GLYPH_CANVAS_FILL) does instead — same governing rule as a favicon/
60
+ * avatar tile. Canvas = 40px (`h-10 w-10`); letter cap height = 40 ×
61
+ * GLYPH_CANVAS_FILL (0.65) = 26px, landing in Matt's requested ~24-28px
62
+ * range for a rail comfortably wider than the glyph itself.
63
+ */
64
+ declare const MONOGRAM_TEXT_XL = "text-[26px]";
54
65
  /**
55
66
  * Rule 3, applied: 8px gap ≈ 0.70× the wordmark's ~11.5px cap height (at
56
67
  * text-base) — inside the 0.5–0.8 band. Named so nobody "fixes" it
@@ -65,4 +76,4 @@ declare const LOCKUP_ROW = "flex items-baseline";
65
76
  /** Rule 5 — standalone glyph cap height as a fraction of the canvas edge. */
66
77
  declare const GLYPH_CANVAS_FILL = 0.65;
67
78
 
68
- export { GLYPH_CANVAS_FILL, LOCKUP_GAP, LOCKUP_ROW, MARK_CAP_RATIO, MARK_WEIGHT, MARK_WEIGHT_CLASS, MONOGRAM_TEXT_LG, WORDMARK_TEXT_LG, WORDMARK_WEIGHT, WORDMARK_WEIGHT_CLASS };
79
+ export { GLYPH_CANVAS_FILL, LOCKUP_GAP, LOCKUP_ROW, MARK_CAP_RATIO, MARK_WEIGHT, MARK_WEIGHT_CLASS, MONOGRAM_TEXT_LG, MONOGRAM_TEXT_XL, WORDMARK_TEXT_LG, WORDMARK_WEIGHT, WORDMARK_WEIGHT_CLASS };