@meetreeve/ui 0.8.0 → 0.9.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
@@ -784,13 +784,23 @@ function Flyout({
784
784
  className: cn(
785
785
  "group flex w-full items-center gap-2.5 rounded-md px-3 py-1.5 text-sm transition-colors",
786
786
  collapsed && "justify-center px-0",
787
- active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
787
+ // DEV-6586: this trigger renders as a row inside AppSidebar's own
788
+ // `<nav>` (a `bg-sidebar` surface that stays dark navy in both
789
+ // themes) — same fix as NavLink, same measured ratios (default
790
+ // 8.05:1, hover 13.65:1, active 10.95:1). The flyout PANEL below
791
+ // is intentionally left on document tokens: it renders via
792
+ // `position: fixed`, floating over the main content area on
793
+ // `bg-background`, not over the sidebar.
794
+ active ? "bg-sidebar-accent text-sidebar-accent-foreground" : "text-sidebar-foreground/70 hover:bg-sidebar-foreground/5 hover:text-sidebar-foreground"
788
795
  ),
789
796
  children: [
790
797
  /* @__PURE__ */ jsx3(
791
798
  Icon,
792
799
  {
793
- className: cn("size-4 shrink-0", active ? "text-primary" : "text-muted-foreground"),
800
+ className: cn(
801
+ "size-4 shrink-0",
802
+ active ? "text-sidebar-accent-foreground" : "text-sidebar-foreground/70"
803
+ ),
794
804
  strokeWidth: 1.5,
795
805
  "aria-hidden": "true"
796
806
  }
@@ -873,13 +883,23 @@ function NavLink({
873
883
  className: cn(
874
884
  "group flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm transition-colors",
875
885
  collapsed && "justify-center px-0",
876
- active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
886
+ // DEV-6586: the sidebar is a permanently dark-navy surface in BOTH
887
+ // app themes (see `--sidebar-*` in globals.css) — styling from
888
+ // document-theme tokens (text-foreground/text-muted-foreground) put
889
+ // dark-on-dark text here in light mode. `--sidebar-foreground` /
890
+ // `--sidebar-accent(-foreground)` are theme-invariant, so hover/
891
+ // active reliably INCREASE legibility instead of erasing it:
892
+ // default 8.05:1, hover 13.65:1, active 10.95:1 (all >= 4.5:1 AA).
893
+ active ? "bg-sidebar-accent text-sidebar-accent-foreground" : "text-sidebar-foreground/70 hover:bg-sidebar-foreground/5 hover:text-sidebar-foreground"
877
894
  ),
878
895
  children: [
879
896
  /* @__PURE__ */ jsx4(
880
897
  Icon,
881
898
  {
882
- className: cn("size-4 shrink-0", active ? "text-primary" : "text-muted-foreground"),
899
+ className: cn(
900
+ "size-4 shrink-0",
901
+ active ? "text-sidebar-accent-foreground" : "text-sidebar-foreground/70"
902
+ ),
883
903
  strokeWidth: 1.5,
884
904
  "aria-hidden": "true"
885
905
  }
@@ -1022,7 +1042,7 @@ function AppSidebar({
1022
1042
  href: brand.href,
1023
1043
  "data-testid": "app-sidebar-brand",
1024
1044
  title: brand.label,
1025
- className: "flex items-center justify-center border-b border-border px-1 py-4",
1045
+ className: "flex items-center justify-center border-b border-sidebar-border px-1 py-4",
1026
1046
  children: [
1027
1047
  /* @__PURE__ */ jsx4("span", { className: "sr-only", children: brand.label }),
1028
1048
  /* @__PURE__ */ jsx4(BrandGlyph, { brand: { name: brand.label }, size: "xl" })
@@ -1035,9 +1055,9 @@ function AppSidebar({
1035
1055
  {
1036
1056
  href: brand.href,
1037
1057
  "data-testid": "app-sidebar-brand",
1038
- className: "flex items-center gap-2.5 border-b border-border px-5 py-4",
1058
+ className: "flex items-center gap-2.5 border-b border-sidebar-border px-5 py-4 text-sidebar-foreground",
1039
1059
  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() }) }),
1060
+ /* @__PURE__ */ jsx4("span", { className: "grid size-7 place-items-center rounded-md bg-sidebar-primary text-sidebar-primary-foreground", children: /* @__PURE__ */ jsx4("span", { className: "font-mono text-sm font-bold", children: (brand.label[0] ?? "r").toUpperCase() }) }),
1041
1061
  /* @__PURE__ */ jsx4("span", { className: "truncate text-sm font-semibold tracking-tight", children: brand.label })
1042
1062
  ]
1043
1063
  }
@@ -1093,7 +1113,7 @@ function AppSidebar({
1093
1113
  "aria-modal": "true",
1094
1114
  "aria-label": "Navigation",
1095
1115
  "data-testid": "app-sidebar-mobile-drawer",
1096
- className: "absolute inset-y-0 left-0 flex w-72 flex-col overflow-y-auto border-r border-border bg-sidebar",
1116
+ className: "absolute inset-y-0 left-0 flex w-72 flex-col overflow-y-auto border-r border-sidebar-border bg-sidebar",
1097
1117
  children: [
1098
1118
  /* @__PURE__ */ jsx4("div", { className: "sticky top-0 z-10 flex justify-end bg-sidebar p-2", children: /* @__PURE__ */ jsx4(
1099
1119
  "button",
@@ -1104,7 +1124,7 @@ function AppSidebar({
1104
1124
  "aria-label": `Close ${menuLabel.toLowerCase()}`,
1105
1125
  onClick: () => setMobileOpen(false),
1106
1126
  style: TOUCH_TARGET_STYLE,
1107
- className: "flex items-center justify-center rounded-md p-2 text-foreground hover:bg-foreground/5",
1127
+ className: "flex items-center justify-center rounded-md p-2 text-sidebar-foreground/70 hover:bg-sidebar-foreground/5 hover:text-sidebar-foreground",
1108
1128
  children: /* @__PURE__ */ jsx4(X2, { className: "h-5 w-5", "aria-hidden": "true" })
1109
1129
  }
1110
1130
  ) }),
@@ -1122,7 +1142,7 @@ function AppSidebar({
1122
1142
  {
1123
1143
  "data-testid": "app-sidebar-desktop",
1124
1144
  className: cn(
1125
- "hidden shrink-0 flex-col border-r border-border bg-sidebar md:flex",
1145
+ "hidden shrink-0 flex-col border-r border-sidebar-border bg-sidebar md:flex",
1126
1146
  collapsed ? COLLAPSED_WIDTH_CLASS : EXPANDED_WIDTH_CLASS,
1127
1147
  // DEV-5331: a plain CSS width transition is enough — no new deps
1128
1148
  // for a spring/JS animation. Respect prefers-reduced-motion.
@@ -1143,7 +1163,7 @@ function AppSidebar({
1143
1163
  ) })
1144
1164
  }
1145
1165
  ),
1146
- collapsible && /* @__PURE__ */ jsx4("div", { className: "border-t border-border px-2 py-2", children: /* @__PURE__ */ jsxs4(
1166
+ collapsible && /* @__PURE__ */ jsx4("div", { className: "border-t border-sidebar-border px-2 py-2", children: /* @__PURE__ */ jsxs4(
1147
1167
  "button",
1148
1168
  {
1149
1169
  type: "button",
@@ -1153,7 +1173,7 @@ function AppSidebar({
1153
1173
  title: collapsed ? "Expand sidebar" : "Collapse sidebar",
1154
1174
  onClick: handleToggleCollapsed,
1155
1175
  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",
1176
+ "flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm text-sidebar-foreground/70 transition-colors hover:bg-sidebar-foreground/5 hover:text-sidebar-foreground",
1157
1177
  collapsed && "justify-center px-0"
1158
1178
  ),
1159
1179
  children: [
@@ -1291,18 +1311,18 @@ function ContextSwitcher({
1291
1311
  type: "button",
1292
1312
  title: active?.name || placeholder,
1293
1313
  "aria-label": active?.name || placeholder,
1294
- className: "flex h-10 w-10 items-center justify-center rounded-lg text-zinc-700 transition-colors hover:bg-zinc-900/5 dark:text-zinc-300 dark:hover:bg-white/5",
1314
+ className: "flex h-10 w-10 items-center justify-center rounded-lg text-sidebar-foreground/70 transition-colors hover:bg-sidebar-foreground/5 hover:text-sidebar-foreground",
1295
1315
  children: /* @__PURE__ */ jsx6(BrandGlyph, { brand: toBrand(active ?? { name: "" }), size: "lg" })
1296
1316
  }
1297
1317
  ) : /* @__PURE__ */ jsxs5(
1298
1318
  "button",
1299
1319
  {
1300
1320
  type: "button",
1301
- className: "group flex items-center gap-2 rounded-md border border-border bg-card/60 px-2.5 py-1.5 text-sm font-medium transition-colors hover:bg-muted",
1321
+ className: "group flex items-center gap-2 rounded-md border border-sidebar-border bg-sidebar-accent/60 px-2.5 py-1.5 text-sm font-medium transition-colors hover:bg-sidebar",
1302
1322
  children: [
1303
1323
  /* @__PURE__ */ jsx6(BrandGlyph, { brand: toBrand(active ?? { name: "" }) }),
1304
- /* @__PURE__ */ jsx6("span", { className: "max-w-[140px] truncate text-foreground/90", children: active?.name || placeholder }),
1305
- /* @__PURE__ */ jsx6(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" })
1324
+ /* @__PURE__ */ jsx6("span", { className: "max-w-[140px] truncate text-sidebar-accent-foreground", children: active?.name || placeholder }),
1325
+ /* @__PURE__ */ jsx6(ChevronDown, { className: "h-3.5 w-3.5 text-sidebar-foreground/70 transition-transform group-data-[state=open]:rotate-180" })
1306
1326
  ]
1307
1327
  }
1308
1328
  ) }),
@@ -1348,6 +1368,10 @@ import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-run
1348
1368
  var DEFAULT_CONNECT_ERROR = "Couldn't start the connection. Please try again.";
1349
1369
  var DEFAULT_DISCONNECT_ERROR = "Couldn't disconnect. Please try again.";
1350
1370
  var DEFAULT_LOAD_ERROR = "Couldn't load connections. Please try again.";
1371
+ var DEFAULT_GRANTS_LOAD_ERROR = "Couldn't load sharing. Please try again.";
1372
+ var DEFAULT_GRANT_ERROR = "Couldn't share. Please try again.";
1373
+ var DEFAULT_REVOKE_ERROR = "Couldn't revoke access. Please try again.";
1374
+ var DEFAULT_ORG_DISABLED_REASON = "Org-wide connections aren't available to you.";
1351
1375
  var SKELETON_COUNT = 3;
1352
1376
  function capabilityLabel(capability) {
1353
1377
  if (capability === "read") return "Read only";
@@ -1362,6 +1386,17 @@ function formatSince(since) {
1362
1386
  function errorMessageFrom(err, fallback) {
1363
1387
  return err instanceof Error && err.message ? err.message : fallback;
1364
1388
  }
1389
+ function ownerLabel(entry) {
1390
+ const connection = entry.connection;
1391
+ if (connection.status !== "connected") return null;
1392
+ if (connection.kind === "org") return "Org";
1393
+ if (connection.owned_by_viewer) return "Yours";
1394
+ if (connection.shared_by) return `Shared by ${connection.shared_by.name}`;
1395
+ return null;
1396
+ }
1397
+ function isSharedWithViewer(entry) {
1398
+ return entry.connection.status === "connected" && Boolean(entry.connection.shared_by) && !entry.connection.owned_by_viewer;
1399
+ }
1365
1400
  function ConnectorPicker({
1366
1401
  client,
1367
1402
  purpose,
@@ -1373,6 +1408,7 @@ function ConnectorPicker({
1373
1408
  const [entries, setEntries] = useState7(null);
1374
1409
  const [loadError, setLoadError] = useState7(null);
1375
1410
  const [uiState, setUiState] = useState7({});
1411
+ const grantsSupported = typeof client.listGrants === "function" && typeof client.grant === "function" && typeof client.revokeGrant === "function";
1376
1412
  const clientRef = useRef5(client);
1377
1413
  clientRef.current = client;
1378
1414
  const purposeRef = useRef5(purpose);
@@ -1431,10 +1467,10 @@ function ConnectorPicker({
1431
1467
  };
1432
1468
  }, [gated, refreshAfterReturn]);
1433
1469
  const handleConnect = useCallback4(
1434
- async (entry) => {
1470
+ async (entry, kind) => {
1435
1471
  setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "connecting" } }));
1436
1472
  try {
1437
- const result = await clientRef.current.connect(entry);
1473
+ const result = await clientRef.current.connect(entry, { kind });
1438
1474
  if (result?.connect_url) {
1439
1475
  window.open(result.connect_url, "_blank", "noopener,noreferrer");
1440
1476
  return;
@@ -1474,121 +1510,429 @@ function ConnectorPicker({
1474
1510
  },
1475
1511
  [load]
1476
1512
  );
1513
+ const listGrants = useCallback4(
1514
+ (entry) => clientRef.current.listGrants?.(entry) ?? Promise.resolve([]),
1515
+ []
1516
+ );
1517
+ const addGrant = useCallback4(
1518
+ (entry, input) => clientRef.current.grant?.(entry, input) ?? Promise.resolve(),
1519
+ []
1520
+ );
1521
+ const revokeGrant = useCallback4(
1522
+ (entry, grantId) => clientRef.current.revokeGrant?.(entry, grantId) ?? Promise.resolve(),
1523
+ []
1524
+ );
1477
1525
  if (gated) return null;
1478
1526
  const containerClassName = cn(
1479
1527
  "flex flex-col",
1480
1528
  layout === "tiles" ? "gap-3 sm:grid sm:grid-cols-2 lg:grid-cols-3" : "divide-y divide-border"
1481
1529
  );
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
- ))
1530
+ const ownEntries = entries?.filter((entry) => !isSharedWithViewer(entry)) ?? null;
1531
+ const sharedEntries = entries?.filter(isSharedWithViewer) ?? [];
1532
+ return /* @__PURE__ */ jsxs6("div", { "data-testid": "connector-picker", "data-layout": layout, className: "flex flex-col gap-4", children: [
1533
+ /* @__PURE__ */ jsxs6("div", { className: containerClassName, children: [
1534
+ entries === null && !loadError && Array.from({ length: SKELETON_COUNT }).map((_, i) => /* @__PURE__ */ jsxs6(
1535
+ "div",
1536
+ {
1537
+ "data-testid": "connector-picker-skeleton-item",
1538
+ "aria-hidden": "true",
1539
+ className: "flex items-center gap-3 rounded-lg border border-border p-3",
1540
+ children: [
1541
+ /* @__PURE__ */ jsx7("span", { className: "h-8 w-8 shrink-0 animate-pulse rounded bg-foreground/10" }),
1542
+ /* @__PURE__ */ jsx7("span", { className: "h-3 flex-1 animate-pulse rounded bg-foreground/10" })
1543
+ ]
1544
+ },
1545
+ i
1546
+ )),
1547
+ loadError && /* @__PURE__ */ jsxs6(
1548
+ "div",
1549
+ {
1550
+ role: "alert",
1551
+ "data-testid": "connector-picker-load-error",
1552
+ className: "flex items-center justify-between gap-3 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive",
1553
+ children: [
1554
+ /* @__PURE__ */ jsx7("span", { children: loadError }),
1555
+ /* @__PURE__ */ jsx7("button", { type: "button", onClick: () => void load(), className: "shrink-0 underline", children: "Retry" })
1556
+ ]
1557
+ }
1558
+ ),
1559
+ entries !== null && entries.length === 0 && !loadError && /* @__PURE__ */ jsx7("p", { "data-testid": "connector-picker-empty", className: "text-sm text-muted-foreground", children: "No connections available." }),
1560
+ ownEntries?.map((entry) => /* @__PURE__ */ jsx7(
1561
+ ConnectorTile,
1562
+ {
1563
+ entry,
1564
+ ui: uiState[entry.slug],
1565
+ layout,
1566
+ grantsSupported,
1567
+ onConnect: handleConnect,
1568
+ onDisconnect: handleDisconnect,
1569
+ listGrants,
1570
+ addGrant,
1571
+ revokeGrant
1572
+ },
1573
+ entry.slug
1574
+ ))
1575
+ ] }),
1576
+ sharedEntries.length > 0 && /* @__PURE__ */ jsxs6("section", { "data-testid": "connector-picker-shared-with-me", className: "flex flex-col gap-2", children: [
1577
+ /* @__PURE__ */ jsx7("h3", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: "Shared with me" }),
1578
+ /* @__PURE__ */ jsx7("div", { className: containerClassName, children: sharedEntries.map((entry) => /* @__PURE__ */ jsx7(
1579
+ ConnectorTile,
1580
+ {
1581
+ entry,
1582
+ ui: uiState[entry.slug],
1583
+ layout,
1584
+ shared: true,
1585
+ grantsSupported,
1586
+ onConnect: handleConnect,
1587
+ onDisconnect: handleDisconnect,
1588
+ listGrants,
1589
+ addGrant,
1590
+ revokeGrant
1591
+ },
1592
+ entry.slug
1593
+ )) })
1594
+ ] })
1520
1595
  ] });
1521
1596
  }
1522
1597
  function ConnectorTile({
1523
1598
  entry,
1524
1599
  ui,
1525
1600
  layout,
1601
+ shared = false,
1602
+ grantsSupported,
1526
1603
  onConnect,
1527
- onDisconnect
1604
+ onDisconnect,
1605
+ listGrants,
1606
+ addGrant,
1607
+ revokeGrant
1528
1608
  }) {
1529
1609
  const isConnected = entry.connection.status === "connected";
1530
1610
  const isConnecting = ui?.kind === "connecting";
1531
1611
  const isDisconnecting = ui?.kind === "disconnecting";
1532
1612
  const errorMessage = ui?.kind === "error" ? ui.message : null;
1613
+ const [kind, setKind] = useState7("personal");
1614
+ const [sharingOpen, setSharingOpen] = useState7(false);
1615
+ const orgPermitted = entry.org_connect?.permitted === true;
1616
+ const orgDisabledReason = entry.org_connect?.reason || DEFAULT_ORG_DISABLED_REASON;
1617
+ const attribution = ownerLabel(entry);
1618
+ const canDisconnect = isConnected && !shared;
1619
+ const canManageSharing = isConnected && entry.connection.owned_by_viewer === true && grantsSupported;
1533
1620
  return /* @__PURE__ */ jsxs6(
1534
1621
  "div",
1535
1622
  {
1536
1623
  "data-testid": `connector-tile-${entry.slug}`,
1537
1624
  "data-connected": isConnected || void 0,
1538
1625
  "data-deprecated": entry.deprecated || void 0,
1539
- className: cn("flex items-center gap-3 p-3", layout === "tiles" && "rounded-lg border border-border"),
1626
+ className: cn("flex flex-col p-3", layout === "tiles" && "rounded-lg border border-border"),
1540
1627
  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",
1628
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-3", children: [
1629
+ /* @__PURE__ */ jsx7(BrandGlyph, { brand: { name: entry.name, logo: entry.logo }, size: "lg" }),
1630
+ /* @__PURE__ */ jsxs6("div", { className: "min-w-0 flex-1", children: [
1631
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", children: [
1632
+ /* @__PURE__ */ jsx7("p", { className: "truncate text-sm font-medium", children: entry.name }),
1633
+ entry.deprecated && /* @__PURE__ */ jsx7(
1634
+ "span",
1635
+ {
1636
+ "data-testid": `connector-deprecated-${entry.slug}`,
1637
+ className: "shrink-0 rounded-full bg-foreground/10 px-2 py-0.5 text-[11px] text-muted-foreground",
1638
+ children: "No longer available"
1639
+ }
1640
+ )
1641
+ ] }),
1642
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
1643
+ /* @__PURE__ */ jsx7("span", { "data-testid": `connector-capability-${entry.slug}`, children: capabilityLabel(entry.capability) }),
1644
+ entry.category && /* @__PURE__ */ jsxs6(Fragment3, { children: [
1645
+ /* @__PURE__ */ jsx7("span", { "aria-hidden": "true", children: "\xB7" }),
1646
+ /* @__PURE__ */ jsx7("span", { className: "truncate", children: entry.category })
1647
+ ] })
1648
+ ] }),
1649
+ isConnected && /* @__PURE__ */ jsx7(
1650
+ "p",
1547
1651
  {
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"
1652
+ "data-testid": `connector-connected-since-${entry.slug}`,
1653
+ className: "text-xs text-muted-foreground",
1654
+ children: entry.connection.since ? `Connected since ${formatSince(entry.connection.since)}` : "Connected"
1551
1655
  }
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
- ] })
1656
+ ),
1657
+ attribution && /* @__PURE__ */ jsx7(
1658
+ "p",
1659
+ {
1660
+ "data-testid": `connector-owner-${entry.slug}`,
1661
+ className: "text-xs text-muted-foreground",
1662
+ children: attribution
1663
+ }
1664
+ ),
1665
+ !isConnected && /* @__PURE__ */ jsxs6(
1666
+ "div",
1667
+ {
1668
+ "data-testid": `connector-kind-toggle-${entry.slug}`,
1669
+ role: "group",
1670
+ "aria-label": "Connect as",
1671
+ className: "mt-1 flex items-center gap-1",
1672
+ children: [
1673
+ /* @__PURE__ */ jsx7(
1674
+ "button",
1675
+ {
1676
+ type: "button",
1677
+ "data-testid": `connector-kind-personal-${entry.slug}`,
1678
+ "aria-pressed": kind === "personal",
1679
+ disabled: isConnecting,
1680
+ onClick: () => setKind("personal"),
1681
+ className: cn(
1682
+ "rounded-full border px-2 py-0.5 text-[11px]",
1683
+ kind === "personal" ? "border-primary bg-primary/10 font-medium" : "border-border text-muted-foreground",
1684
+ "disabled:opacity-50"
1685
+ ),
1686
+ children: "Personal"
1687
+ }
1688
+ ),
1689
+ /* @__PURE__ */ jsxs6(
1690
+ "button",
1691
+ {
1692
+ type: "button",
1693
+ "data-testid": `connector-kind-org-${entry.slug}`,
1694
+ "aria-pressed": kind === "org",
1695
+ disabled: isConnecting || !orgPermitted,
1696
+ title: orgPermitted ? void 0 : orgDisabledReason,
1697
+ onClick: () => setKind("org"),
1698
+ className: cn(
1699
+ "rounded-full border px-2 py-0.5 text-[11px]",
1700
+ kind === "org" ? "border-primary bg-primary/10 font-medium" : "border-border text-muted-foreground",
1701
+ "disabled:opacity-50"
1702
+ ),
1703
+ children: [
1704
+ "Org",
1705
+ !orgPermitted && /* @__PURE__ */ jsxs6("span", { className: "sr-only", children: [
1706
+ " (",
1707
+ orgDisabledReason,
1708
+ ")"
1709
+ ] })
1710
+ ]
1711
+ }
1712
+ )
1713
+ ]
1714
+ }
1715
+ ),
1716
+ errorMessage && /* @__PURE__ */ jsx7("p", { role: "alert", "data-testid": `connector-error-${entry.slug}`, className: "text-xs text-destructive", children: errorMessage })
1560
1717
  ] }),
1561
- isConnected && /* @__PURE__ */ jsx7(
1562
- "p",
1718
+ isConnected ? /* @__PURE__ */ jsxs6("div", { className: "flex shrink-0 flex-col items-end gap-1", children: [
1719
+ canDisconnect && /* @__PURE__ */ jsx7(
1720
+ "button",
1721
+ {
1722
+ type: "button",
1723
+ "data-testid": `connector-disconnect-${entry.slug}`,
1724
+ onClick: () => onDisconnect(entry),
1725
+ disabled: isDisconnecting,
1726
+ className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-destructive hover:bg-destructive/10 disabled:opacity-50",
1727
+ children: isDisconnecting ? "Disconnecting\u2026" : "Disconnect"
1728
+ }
1729
+ ),
1730
+ canManageSharing && /* @__PURE__ */ jsx7(
1731
+ "button",
1732
+ {
1733
+ type: "button",
1734
+ "data-testid": `connector-manage-sharing-${entry.slug}`,
1735
+ "aria-expanded": sharingOpen,
1736
+ onClick: () => setSharingOpen((open) => !open),
1737
+ className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-muted-foreground hover:bg-foreground/10",
1738
+ children: sharingOpen ? "Hide sharing" : "Manage sharing"
1739
+ }
1740
+ )
1741
+ ] }) : /* @__PURE__ */ jsx7(
1742
+ "button",
1563
1743
  {
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"
1744
+ type: "button",
1745
+ "data-testid": `connector-connect-${entry.slug}`,
1746
+ onClick: () => onConnect(entry, kind),
1747
+ disabled: isConnecting,
1748
+ className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
1749
+ children: isConnecting ? "Connecting\u2026" : "Connect"
1567
1750
  }
1568
- ),
1569
- errorMessage && /* @__PURE__ */ jsx7("p", { role: "alert", "data-testid": `connector-error-${entry.slug}`, className: "text-xs text-destructive", children: errorMessage })
1751
+ )
1570
1752
  ] }),
1571
- isConnected ? /* @__PURE__ */ jsx7(
1572
- "button",
1753
+ canManageSharing && sharingOpen && /* @__PURE__ */ jsx7(
1754
+ ManageSharing,
1573
1755
  {
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"
1756
+ entry,
1757
+ listGrants,
1758
+ addGrant,
1759
+ revokeGrant
1580
1760
  }
1581
- ) : /* @__PURE__ */ jsx7(
1582
- "button",
1761
+ )
1762
+ ]
1763
+ }
1764
+ );
1765
+ }
1766
+ function ManageSharing({
1767
+ entry,
1768
+ listGrants,
1769
+ addGrant,
1770
+ revokeGrant
1771
+ }) {
1772
+ const [grants, setGrants] = useState7(null);
1773
+ const [error, setError] = useState7(null);
1774
+ const [busy, setBusy] = useState7(false);
1775
+ const [granteeType, setGranteeType] = useState7("user");
1776
+ const [granteeId, setGranteeId] = useState7("");
1777
+ const roleOptions = entry.org_connect?.role_options ?? [];
1778
+ const entryRef = useRef5(entry);
1779
+ entryRef.current = entry;
1780
+ const load = useCallback4(async () => {
1781
+ try {
1782
+ setGrants(await listGrants(entryRef.current));
1783
+ setError(null);
1784
+ } catch (err) {
1785
+ setError(errorMessageFrom(err, DEFAULT_GRANTS_LOAD_ERROR));
1786
+ }
1787
+ }, [listGrants]);
1788
+ useEffect6(() => {
1789
+ void load();
1790
+ }, [load]);
1791
+ const handleAdd = useCallback4(async () => {
1792
+ const id = granteeId.trim();
1793
+ if (!id) return;
1794
+ setBusy(true);
1795
+ try {
1796
+ await addGrant(entryRef.current, { granteeType, granteeId: id });
1797
+ setGranteeId("");
1798
+ await load();
1799
+ } catch (err) {
1800
+ setError(errorMessageFrom(err, DEFAULT_GRANT_ERROR));
1801
+ } finally {
1802
+ setBusy(false);
1803
+ }
1804
+ }, [addGrant, granteeId, granteeType, load]);
1805
+ const handleRevoke = useCallback4(
1806
+ async (grantId) => {
1807
+ setBusy(true);
1808
+ try {
1809
+ await revokeGrant(entryRef.current, grantId);
1810
+ await load();
1811
+ } catch (err) {
1812
+ setError(errorMessageFrom(err, DEFAULT_REVOKE_ERROR));
1813
+ } finally {
1814
+ setBusy(false);
1815
+ }
1816
+ },
1817
+ [load, revokeGrant]
1818
+ );
1819
+ return /* @__PURE__ */ jsxs6(
1820
+ "div",
1821
+ {
1822
+ "data-testid": `connector-sharing-panel-${entry.slug}`,
1823
+ className: "mt-2 flex flex-col gap-2 rounded-md border border-border bg-foreground/5 p-2",
1824
+ children: [
1825
+ grants === null && !error && /* @__PURE__ */ jsx7(
1826
+ "p",
1583
1827
  {
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"
1828
+ "data-testid": `connector-grants-loading-${entry.slug}`,
1829
+ className: "text-xs text-muted-foreground",
1830
+ children: "Loading sharing\u2026"
1590
1831
  }
1591
- )
1832
+ ),
1833
+ error && /* @__PURE__ */ jsx7(
1834
+ "p",
1835
+ {
1836
+ role: "alert",
1837
+ "data-testid": `connector-sharing-error-${entry.slug}`,
1838
+ className: "text-xs text-destructive",
1839
+ children: error
1840
+ }
1841
+ ),
1842
+ grants !== null && grants.length === 0 && /* @__PURE__ */ jsx7(
1843
+ "p",
1844
+ {
1845
+ "data-testid": `connector-grants-empty-${entry.slug}`,
1846
+ className: "text-xs text-muted-foreground",
1847
+ children: "Not shared with anyone yet."
1848
+ }
1849
+ ),
1850
+ grants?.map((grant) => /* @__PURE__ */ jsxs6(
1851
+ "div",
1852
+ {
1853
+ "data-testid": `connector-grant-${entry.slug}-${grant.id}`,
1854
+ className: "flex items-center justify-between gap-2 text-xs",
1855
+ children: [
1856
+ /* @__PURE__ */ jsxs6("span", { className: "truncate", children: [
1857
+ grant.grantee_name || grant.grantee_id,
1858
+ /* @__PURE__ */ jsxs6("span", { className: "text-muted-foreground", children: [
1859
+ " ",
1860
+ "\xB7 ",
1861
+ grant.grantee_type === "role" ? "Role" : "User"
1862
+ ] })
1863
+ ] }),
1864
+ /* @__PURE__ */ jsx7(
1865
+ "button",
1866
+ {
1867
+ type: "button",
1868
+ "data-testid": `connector-grant-revoke-${entry.slug}-${grant.id}`,
1869
+ onClick: () => void handleRevoke(grant.id),
1870
+ disabled: busy,
1871
+ className: "shrink-0 rounded-md px-2 py-0.5 font-medium text-destructive hover:bg-destructive/10 disabled:opacity-50",
1872
+ children: "Revoke"
1873
+ }
1874
+ )
1875
+ ]
1876
+ },
1877
+ grant.id
1878
+ )),
1879
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1.5", children: [
1880
+ /* @__PURE__ */ jsxs6(
1881
+ "select",
1882
+ {
1883
+ "data-testid": `connector-grant-type-${entry.slug}`,
1884
+ "aria-label": "Grantee type",
1885
+ value: granteeType,
1886
+ onChange: (event) => {
1887
+ setGranteeType(event.target.value);
1888
+ setGranteeId("");
1889
+ },
1890
+ disabled: busy,
1891
+ className: "rounded-md border border-border bg-transparent px-1.5 py-1 text-xs disabled:opacity-50",
1892
+ children: [
1893
+ /* @__PURE__ */ jsx7("option", { value: "user", children: "User" }),
1894
+ /* @__PURE__ */ jsx7("option", { value: "role", children: "Role" })
1895
+ ]
1896
+ }
1897
+ ),
1898
+ granteeType === "user" ? /* @__PURE__ */ jsx7(
1899
+ "input",
1900
+ {
1901
+ "data-testid": `connector-grant-grantee-${entry.slug}`,
1902
+ "aria-label": "User ID",
1903
+ value: granteeId,
1904
+ onChange: (event) => setGranteeId(event.target.value),
1905
+ placeholder: "User ID",
1906
+ disabled: busy,
1907
+ className: "min-w-0 flex-1 rounded-md border border-border bg-transparent px-1.5 py-1 text-xs disabled:opacity-50"
1908
+ }
1909
+ ) : /* @__PURE__ */ jsxs6(
1910
+ "select",
1911
+ {
1912
+ "data-testid": `connector-grant-role-${entry.slug}`,
1913
+ "aria-label": "Role",
1914
+ value: granteeId,
1915
+ onChange: (event) => setGranteeId(event.target.value),
1916
+ disabled: busy,
1917
+ className: "min-w-0 flex-1 rounded-md border border-border bg-transparent px-1.5 py-1 text-xs disabled:opacity-50",
1918
+ children: [
1919
+ /* @__PURE__ */ jsx7("option", { value: "", children: "Select a role\u2026" }),
1920
+ roleOptions.map((role) => /* @__PURE__ */ jsx7("option", { value: role.id, children: role.name }, role.id))
1921
+ ]
1922
+ }
1923
+ ),
1924
+ /* @__PURE__ */ jsx7(
1925
+ "button",
1926
+ {
1927
+ type: "button",
1928
+ "data-testid": `connector-grant-add-${entry.slug}`,
1929
+ onClick: () => void handleAdd(),
1930
+ disabled: busy || !granteeId.trim(),
1931
+ className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
1932
+ children: "Share"
1933
+ }
1934
+ )
1935
+ ] })
1592
1936
  ]
1593
1937
  }
1594
1938
  );