@meetreeve/ui 0.13.0 → 0.15.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.d.mts CHANGED
@@ -632,7 +632,7 @@ declare function ContextSwitcher({ items, activeRefId, onSelect, footer, placeho
632
632
  * errors, and a return-from-OAuth refresh). Every label is derived from the
633
633
  * entry data the client returns — this file must never hardcode a specific
634
634
  * provider's name (an explicit acceptance criterion); a host app supplies
635
- * that entirely through `ConnectableEntry.name` / `.logo` / `.category`.
635
+ * that entirely through `ConnectableEntry.name` / `.logo`.
636
636
  *
637
637
  * Transport-injection precedent: `linkComponent` (DEV-4429/DEV-4391) —
638
638
  * inject the transport, keep behavior in the package. Here the "transport"
@@ -649,7 +649,17 @@ declare function ContextSwitcher({ items, activeRefId, onSelect, footer, placeho
649
649
  * optional grant methods, so a pre-CD-5 client keeps working unchanged.
650
650
  */
651
651
  type ConnectableTransport = "composio" | "native";
652
+ /**
653
+ * The widest access an entry offers. "write" is retired (DEV-13661: no
654
+ * provider ever only acted, so its old label was never true); it stays in the
655
+ * union so an older server payload still type-checks, and it renders as
656
+ * "Read + write".
657
+ */
652
658
  type ConnectableCapability = "read" | "write" | "readwrite";
659
+ /** What a connection lets Reeve do: ingest/query, or also act (DEV-13661). */
660
+ type ConnectableAccess = "read" | "readwrite";
661
+ /** The ONE access -> words mapping; nothing is labeled as acting alone (DEV-13661). */
662
+ declare function accessLabel(access: ConnectableCapability): string;
653
663
  type ConnectionKind = "personal" | "org";
654
664
  type ConnectorGranteeType = "user" | "role";
655
665
  /** An RBAC role a grant can target, injected via the entry data (CD-2). */
@@ -664,7 +674,7 @@ interface ConnectorRoleOption {
664
674
  */
665
675
  interface OrgConnectPolicy {
666
676
  permitted: boolean;
667
- /** Human-readable reason shown on the disabled org option. */
677
+ /** Why org connect isn't permitted (server-authored). */
668
678
  reason?: string | null;
669
679
  /** Role options offered by manage-sharing's "role" grantee type. */
670
680
  role_options?: ConnectorRoleOption[];
@@ -806,7 +816,24 @@ interface ConnectableEntry {
806
816
  * picker never assumes an email shape or synthesizes this itself.
807
817
  */
808
818
  account_label?: string | null;
819
+ /**
820
+ * What this live connection lets Reeve do (DEV-13661). Absent on a
821
+ * pre-DEV-13661 payload, where the entry's `capability` stands in.
822
+ */
823
+ access?: ConnectableAccess | null;
809
824
  };
825
+ /**
826
+ * The accesses this entry may be connected as (DEV-13661), in
827
+ * ("read", "readwrite") order. More than one renders a Read / Read + write
828
+ * choice whose pick reaches `client.connect` as `opts.access`.
829
+ */
830
+ access_options?: ConnectableAccess[] | null;
831
+ /**
832
+ * Every connection provider key that belongs to this entry's provider
833
+ * (its own slug first), so a host can group stored connections onto one
834
+ * row per provider (DEV-13661).
835
+ */
836
+ provider_keys?: string[] | null;
810
837
  /** true = tenant has a live connection but the entry left the allowlist. */
811
838
  deprecated: boolean;
812
839
  /** Org-connect permission + role options for this entry (CD-4). */
@@ -841,9 +868,12 @@ interface ConnectorPickerClient {
841
868
  * composio -> open the returned connect_url; native may resolve with none.
842
869
  * `opts.kind` is additive (DEV-5654): a pre-CD-5 client declaring only
843
870
  * `connect(entry)` still satisfies this signature and simply ignores it.
871
+ * `opts.access` (DEV-13661) is the row's Read / Read + write pick, sent
872
+ * only when the entry offers a choice.
844
873
  */
845
874
  connect(entry: ConnectableEntry, opts?: {
846
875
  kind: ConnectionKind;
876
+ access?: ConnectableAccess;
847
877
  }): Promise<{
848
878
  connect_url?: string;
849
879
  }>;
@@ -895,4 +925,4 @@ interface ConnectorPickerProps {
895
925
  }
896
926
  declare function ConnectorPicker({ client, purpose, lens, layout, onConnected, }: ConnectorPickerProps): react.JSX.Element | null;
897
927
 
898
- export { type AppRungItem, AppSidebar, type AppSidebarBrand, type AppSidebarModuleDef, type AppSidebarProps, type AppSidebarTopItem, type Brand, BrandGlyph, type BrandGlyphProps, type ConnectableCapability, type ConnectableEntry, type ConnectableTransport, type ConnectionKind, type ConnectorConnectionHealth, type ConnectorCredentialField, type ConnectorCredentialTestResult, type ConnectorGrant, type ConnectorGrantInput, type ConnectorGranteeType, type ConnectorGuideStep, ConnectorPicker, type ConnectorPickerClient, type ConnectorPickerLens, type ConnectorPickerProps, type ConnectorRoleOption, type ConnectorSetupGuide, type ContextOrgGroup, ContextSwitcher, type ContextSwitcherItem, type ContextSwitcherProps, type FallbackModuleDef, Flyout, type FlyoutChild, type FlyoutProps, GLYPH_CANVAS_FILL, type GroupRungItem, type HeaderNavItem, InfoTip, type InfoTipProps, LOCKUP_GAP, LOCKUP_ROW, MARK_CAP_RATIO, MARK_WEIGHT, type OrgConnectPolicy, REEVE_ACCENT, ResponsiveHeader, type ResponsiveHeaderProps, type RungItem, type SidebarLadder, type SidebarLadderParsed, type SidebarRung, type UseLadderRungOptions, WORDMARK_WEIGHT, deriveFallbackLadder, faviconUrl, groupByOrg, reconcileLadder, resolveLadder, useLadderRung, useResponsivePlaceholder, zSidebarLadder };
928
+ export { type AppRungItem, AppSidebar, type AppSidebarBrand, type AppSidebarModuleDef, type AppSidebarProps, type AppSidebarTopItem, type Brand, BrandGlyph, type BrandGlyphProps, type ConnectableAccess, type ConnectableCapability, type ConnectableEntry, type ConnectableTransport, type ConnectionKind, type ConnectorConnectionHealth, type ConnectorCredentialField, type ConnectorCredentialTestResult, type ConnectorGrant, type ConnectorGrantInput, type ConnectorGranteeType, type ConnectorGuideStep, ConnectorPicker, type ConnectorPickerClient, type ConnectorPickerLens, type ConnectorPickerProps, type ConnectorRoleOption, type ConnectorSetupGuide, type ContextOrgGroup, ContextSwitcher, type ContextSwitcherItem, type ContextSwitcherProps, type FallbackModuleDef, Flyout, type FlyoutChild, type FlyoutProps, GLYPH_CANVAS_FILL, type GroupRungItem, type HeaderNavItem, InfoTip, type InfoTipProps, LOCKUP_GAP, LOCKUP_ROW, MARK_CAP_RATIO, MARK_WEIGHT, type OrgConnectPolicy, REEVE_ACCENT, ResponsiveHeader, type ResponsiveHeaderProps, type RungItem, type SidebarLadder, type SidebarLadderParsed, type SidebarRung, type UseLadderRungOptions, WORDMARK_WEIGHT, accessLabel, deriveFallbackLadder, faviconUrl, groupByOrg, reconcileLadder, resolveLadder, useLadderRung, useResponsivePlaceholder, zSidebarLadder };
package/dist/index.d.ts CHANGED
@@ -632,7 +632,7 @@ declare function ContextSwitcher({ items, activeRefId, onSelect, footer, placeho
632
632
  * errors, and a return-from-OAuth refresh). Every label is derived from the
633
633
  * entry data the client returns — this file must never hardcode a specific
634
634
  * provider's name (an explicit acceptance criterion); a host app supplies
635
- * that entirely through `ConnectableEntry.name` / `.logo` / `.category`.
635
+ * that entirely through `ConnectableEntry.name` / `.logo`.
636
636
  *
637
637
  * Transport-injection precedent: `linkComponent` (DEV-4429/DEV-4391) —
638
638
  * inject the transport, keep behavior in the package. Here the "transport"
@@ -649,7 +649,17 @@ declare function ContextSwitcher({ items, activeRefId, onSelect, footer, placeho
649
649
  * optional grant methods, so a pre-CD-5 client keeps working unchanged.
650
650
  */
651
651
  type ConnectableTransport = "composio" | "native";
652
+ /**
653
+ * The widest access an entry offers. "write" is retired (DEV-13661: no
654
+ * provider ever only acted, so its old label was never true); it stays in the
655
+ * union so an older server payload still type-checks, and it renders as
656
+ * "Read + write".
657
+ */
652
658
  type ConnectableCapability = "read" | "write" | "readwrite";
659
+ /** What a connection lets Reeve do: ingest/query, or also act (DEV-13661). */
660
+ type ConnectableAccess = "read" | "readwrite";
661
+ /** The ONE access -> words mapping; nothing is labeled as acting alone (DEV-13661). */
662
+ declare function accessLabel(access: ConnectableCapability): string;
653
663
  type ConnectionKind = "personal" | "org";
654
664
  type ConnectorGranteeType = "user" | "role";
655
665
  /** An RBAC role a grant can target, injected via the entry data (CD-2). */
@@ -664,7 +674,7 @@ interface ConnectorRoleOption {
664
674
  */
665
675
  interface OrgConnectPolicy {
666
676
  permitted: boolean;
667
- /** Human-readable reason shown on the disabled org option. */
677
+ /** Why org connect isn't permitted (server-authored). */
668
678
  reason?: string | null;
669
679
  /** Role options offered by manage-sharing's "role" grantee type. */
670
680
  role_options?: ConnectorRoleOption[];
@@ -806,7 +816,24 @@ interface ConnectableEntry {
806
816
  * picker never assumes an email shape or synthesizes this itself.
807
817
  */
808
818
  account_label?: string | null;
819
+ /**
820
+ * What this live connection lets Reeve do (DEV-13661). Absent on a
821
+ * pre-DEV-13661 payload, where the entry's `capability` stands in.
822
+ */
823
+ access?: ConnectableAccess | null;
809
824
  };
825
+ /**
826
+ * The accesses this entry may be connected as (DEV-13661), in
827
+ * ("read", "readwrite") order. More than one renders a Read / Read + write
828
+ * choice whose pick reaches `client.connect` as `opts.access`.
829
+ */
830
+ access_options?: ConnectableAccess[] | null;
831
+ /**
832
+ * Every connection provider key that belongs to this entry's provider
833
+ * (its own slug first), so a host can group stored connections onto one
834
+ * row per provider (DEV-13661).
835
+ */
836
+ provider_keys?: string[] | null;
810
837
  /** true = tenant has a live connection but the entry left the allowlist. */
811
838
  deprecated: boolean;
812
839
  /** Org-connect permission + role options for this entry (CD-4). */
@@ -841,9 +868,12 @@ interface ConnectorPickerClient {
841
868
  * composio -> open the returned connect_url; native may resolve with none.
842
869
  * `opts.kind` is additive (DEV-5654): a pre-CD-5 client declaring only
843
870
  * `connect(entry)` still satisfies this signature and simply ignores it.
871
+ * `opts.access` (DEV-13661) is the row's Read / Read + write pick, sent
872
+ * only when the entry offers a choice.
844
873
  */
845
874
  connect(entry: ConnectableEntry, opts?: {
846
875
  kind: ConnectionKind;
876
+ access?: ConnectableAccess;
847
877
  }): Promise<{
848
878
  connect_url?: string;
849
879
  }>;
@@ -895,4 +925,4 @@ interface ConnectorPickerProps {
895
925
  }
896
926
  declare function ConnectorPicker({ client, purpose, lens, layout, onConnected, }: ConnectorPickerProps): react.JSX.Element | null;
897
927
 
898
- export { type AppRungItem, AppSidebar, type AppSidebarBrand, type AppSidebarModuleDef, type AppSidebarProps, type AppSidebarTopItem, type Brand, BrandGlyph, type BrandGlyphProps, type ConnectableCapability, type ConnectableEntry, type ConnectableTransport, type ConnectionKind, type ConnectorConnectionHealth, type ConnectorCredentialField, type ConnectorCredentialTestResult, type ConnectorGrant, type ConnectorGrantInput, type ConnectorGranteeType, type ConnectorGuideStep, ConnectorPicker, type ConnectorPickerClient, type ConnectorPickerLens, type ConnectorPickerProps, type ConnectorRoleOption, type ConnectorSetupGuide, type ContextOrgGroup, ContextSwitcher, type ContextSwitcherItem, type ContextSwitcherProps, type FallbackModuleDef, Flyout, type FlyoutChild, type FlyoutProps, GLYPH_CANVAS_FILL, type GroupRungItem, type HeaderNavItem, InfoTip, type InfoTipProps, LOCKUP_GAP, LOCKUP_ROW, MARK_CAP_RATIO, MARK_WEIGHT, type OrgConnectPolicy, REEVE_ACCENT, ResponsiveHeader, type ResponsiveHeaderProps, type RungItem, type SidebarLadder, type SidebarLadderParsed, type SidebarRung, type UseLadderRungOptions, WORDMARK_WEIGHT, deriveFallbackLadder, faviconUrl, groupByOrg, reconcileLadder, resolveLadder, useLadderRung, useResponsivePlaceholder, zSidebarLadder };
928
+ export { type AppRungItem, AppSidebar, type AppSidebarBrand, type AppSidebarModuleDef, type AppSidebarProps, type AppSidebarTopItem, type Brand, BrandGlyph, type BrandGlyphProps, type ConnectableAccess, type ConnectableCapability, type ConnectableEntry, type ConnectableTransport, type ConnectionKind, type ConnectorConnectionHealth, type ConnectorCredentialField, type ConnectorCredentialTestResult, type ConnectorGrant, type ConnectorGrantInput, type ConnectorGranteeType, type ConnectorGuideStep, ConnectorPicker, type ConnectorPickerClient, type ConnectorPickerLens, type ConnectorPickerProps, type ConnectorRoleOption, type ConnectorSetupGuide, type ContextOrgGroup, ContextSwitcher, type ContextSwitcherItem, type ContextSwitcherProps, type FallbackModuleDef, Flyout, type FlyoutChild, type FlyoutProps, GLYPH_CANVAS_FILL, type GroupRungItem, type HeaderNavItem, InfoTip, type InfoTipProps, LOCKUP_GAP, LOCKUP_ROW, MARK_CAP_RATIO, MARK_WEIGHT, type OrgConnectPolicy, REEVE_ACCENT, ResponsiveHeader, type ResponsiveHeaderProps, type RungItem, type SidebarLadder, type SidebarLadderParsed, type SidebarRung, type UseLadderRungOptions, WORDMARK_WEIGHT, accessLabel, deriveFallbackLadder, faviconUrl, groupByOrg, reconcileLadder, resolveLadder, useLadderRung, useResponsivePlaceholder, zSidebarLadder };
package/dist/index.js CHANGED
@@ -45,6 +45,7 @@ __export(src_exports, {
45
45
  REEVE_ACCENT: () => REEVE_ACCENT,
46
46
  ResponsiveHeader: () => ResponsiveHeader,
47
47
  WORDMARK_WEIGHT: () => WORDMARK_WEIGHT,
48
+ accessLabel: () => accessLabel,
48
49
  deriveFallbackLadder: () => deriveFallbackLadder,
49
50
  faviconUrl: () => faviconUrl,
50
51
  groupByOrg: () => groupByOrg,
@@ -1463,6 +1464,9 @@ function ContextSwitcher({
1463
1464
  // src/connector-picker.tsx
1464
1465
  var import_react9 = require("react");
1465
1466
  var import_jsx_runtime8 = require("react/jsx-runtime");
1467
+ function accessLabel(access) {
1468
+ return access === "read" ? "Read" : "Read + write";
1469
+ }
1466
1470
  var DEFAULT_CONNECT_ERROR = "Couldn't start the connection. Please try again.";
1467
1471
  var MAX_RECONNECT_REFOCUS_CHECKS = 3;
1468
1472
  var RECONNECT_UNCONFIRMED_ERROR = "Couldn't confirm the reconnect \u2014 the connection still reports unhealthy. Try again, or disconnect and reconnect.";
@@ -1471,7 +1475,6 @@ var DEFAULT_LOAD_ERROR = "Couldn't load connections. Please try again.";
1471
1475
  var DEFAULT_GRANTS_LOAD_ERROR = "Couldn't load sharing. Please try again.";
1472
1476
  var DEFAULT_GRANT_ERROR = "Couldn't share. Please try again.";
1473
1477
  var DEFAULT_REVOKE_ERROR = "Couldn't revoke access. Please try again.";
1474
- var DEFAULT_ORG_DISABLED_REASON = "Org-wide connections aren't available to you.";
1475
1478
  var DEFAULT_CREDENTIAL_ERROR = "Couldn't save that credential. Please try again.";
1476
1479
  var DEFAULT_CREDENTIAL_TEST_ERROR = "Couldn't test that credential. Please try again.";
1477
1480
  var DEFAULT_RECONNECT_MESSAGE = "This connection needs to be reconnected to keep working.";
@@ -1485,15 +1488,17 @@ function hasUngroundedPlaceholder(text) {
1485
1488
  function isDevEnvironment() {
1486
1489
  return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
1487
1490
  }
1488
- function capabilityLabel(capability) {
1489
- if (capability === "read") return "Read only";
1490
- if (capability === "write") return "Write only";
1491
- return "Read & write";
1491
+ function tileAccess(entry) {
1492
+ if (entry.connection.status === "connected" && entry.connection.access) {
1493
+ return entry.connection.access;
1494
+ }
1495
+ return entry.capability;
1492
1496
  }
1493
- function formatSince(since) {
1494
- const parsed = new Date(since);
1495
- if (Number.isNaN(parsed.getTime())) return since;
1496
- return parsed.toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
1497
+ function accessChoices(entry) {
1498
+ const options = (entry.access_options ?? []).filter(
1499
+ (a) => a === "read" || a === "readwrite"
1500
+ );
1501
+ return options.length > 1 ? options : [];
1497
1502
  }
1498
1503
  function errorMessageFrom(err, fallback) {
1499
1504
  return err instanceof Error && err.message ? err.message : fallback;
@@ -1612,7 +1617,7 @@ function ConnectorPicker({
1612
1617
  };
1613
1618
  }, [gated, refreshAfterReturn]);
1614
1619
  const handleConnect = (0, import_react9.useCallback)(
1615
- async (entry, kind) => {
1620
+ async (entry, kind, access) => {
1616
1621
  if (isUnhealthy(entry)) {
1617
1622
  reconnectingRef.current.add(entry.slug);
1618
1623
  } else {
@@ -1621,7 +1626,10 @@ function ConnectorPicker({
1621
1626
  reconnectChecksRef.current.delete(entry.slug);
1622
1627
  setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "connecting" } }));
1623
1628
  try {
1624
- const result = await clientRef.current.connect(entry, { kind });
1629
+ const result = await clientRef.current.connect(
1630
+ entry,
1631
+ access ? { kind, access } : { kind }
1632
+ );
1625
1633
  if (result?.connect_url) {
1626
1634
  window.open(result.connect_url, "_blank", "noopener,noreferrer");
1627
1635
  return;
@@ -1811,6 +1819,9 @@ function ConnectorTile({
1811
1819
  const errorMessage = ui?.kind === "error" ? ui.message : null;
1812
1820
  const unhealthy = isUnhealthy(entry);
1813
1821
  const [kind, setKind] = (0, import_react9.useState)("personal");
1822
+ const choices = accessChoices(entry);
1823
+ const [access, setAccess] = (0, import_react9.useState)(null);
1824
+ const connectAccess = choices.length ? access && choices.includes(access) ? access : choices[choices.length - 1] : void 0;
1814
1825
  const [sharingOpen, setSharingOpen] = (0, import_react9.useState)(false);
1815
1826
  const [setupOpen, setSetupOpen] = (0, import_react9.useState)(false);
1816
1827
  const credentialFields = entry.credential_fields ?? [];
@@ -1818,7 +1829,7 @@ function ConnectorTile({
1818
1829
  const guide = entry.guide ?? null;
1819
1830
  const canSetUp = !isConnected && (pasteSupported || guide !== null);
1820
1831
  const orgPermitted = entry.org_connect?.permitted === true;
1821
- const orgDisabledReason = entry.org_connect?.reason || DEFAULT_ORG_DISABLED_REASON;
1832
+ const connectKind = orgPermitted ? kind : "personal";
1822
1833
  const attribution = ownerLabel(entry);
1823
1834
  const canDisconnect = isConnected && !shared;
1824
1835
  const canManageSharing = isConnected && entry.connection.owned_by_viewer === true && grantsSupported;
@@ -1845,20 +1856,22 @@ function ConnectorTile({
1845
1856
  )
1846
1857
  ] }),
1847
1858
  /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
1848
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { "data-testid": `connector-capability-${entry.slug}`, children: capabilityLabel(entry.capability) }),
1849
- entry.category && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
1859
+ (isConnected || choices.length === 0) && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { "data-testid": `connector-capability-${entry.slug}`, children: accessLabel(tileAccess(entry)) }),
1860
+ isConnected && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
1850
1861
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { "aria-hidden": "true", children: "\xB7" }),
1851
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "truncate", children: entry.category })
1862
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1863
+ "span",
1864
+ {
1865
+ "data-testid": `connector-connected-${entry.slug}`,
1866
+ className: "inline-flex items-center gap-1",
1867
+ children: [
1868
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { "aria-hidden": "true", className: "h-1.5 w-1.5 rounded-full bg-emerald-500" }),
1869
+ "Connected"
1870
+ ]
1871
+ }
1872
+ )
1852
1873
  ] })
1853
1874
  ] }),
1854
- isConnected && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1855
- "p",
1856
- {
1857
- "data-testid": `connector-connected-since-${entry.slug}`,
1858
- className: "text-xs text-muted-foreground",
1859
- children: entry.connection.since ? `Connected since ${formatSince(entry.connection.since)}` : "Connected"
1860
- }
1861
- ),
1862
1875
  isConnected && !shared && entry.connection.account_label && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1863
1876
  "p",
1864
1877
  {
@@ -1895,7 +1908,33 @@ function ConnectorTile({
1895
1908
  children: attribution
1896
1909
  }
1897
1910
  ),
1898
- !isConnected && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1911
+ !isConnected && choices.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1912
+ "div",
1913
+ {
1914
+ "data-testid": `connector-access-choice-${entry.slug}`,
1915
+ role: "group",
1916
+ "aria-label": "Access",
1917
+ className: "mt-1 flex items-center gap-1",
1918
+ children: choices.map((option) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1919
+ "button",
1920
+ {
1921
+ type: "button",
1922
+ "data-testid": `connector-access-${option}-${entry.slug}`,
1923
+ "aria-pressed": connectAccess === option,
1924
+ disabled: isConnecting,
1925
+ onClick: () => setAccess(option),
1926
+ className: cn(
1927
+ "rounded-full border px-2 py-0.5 text-[11px]",
1928
+ connectAccess === option ? "border-primary bg-primary/10 font-medium" : "border-border text-muted-foreground",
1929
+ "disabled:opacity-50"
1930
+ ),
1931
+ children: accessLabel(option)
1932
+ },
1933
+ option
1934
+ ))
1935
+ }
1936
+ ),
1937
+ !isConnected && orgPermitted && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1899
1938
  "div",
1900
1939
  {
1901
1940
  "data-testid": `connector-kind-toggle-${entry.slug}`,
@@ -1910,6 +1949,7 @@ function ConnectorTile({
1910
1949
  "data-testid": `connector-kind-personal-${entry.slug}`,
1911
1950
  "aria-pressed": kind === "personal",
1912
1951
  disabled: isConnecting,
1952
+ title: "Just for you",
1913
1953
  onClick: () => setKind("personal"),
1914
1954
  className: cn(
1915
1955
  "rounded-full border px-2 py-0.5 text-[11px]",
@@ -1919,28 +1959,21 @@ function ConnectorTile({
1919
1959
  children: "Personal"
1920
1960
  }
1921
1961
  ),
1922
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1962
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1923
1963
  "button",
1924
1964
  {
1925
1965
  type: "button",
1926
1966
  "data-testid": `connector-kind-org-${entry.slug}`,
1927
1967
  "aria-pressed": kind === "org",
1928
- disabled: isConnecting || !orgPermitted,
1929
- title: orgPermitted ? void 0 : orgDisabledReason,
1968
+ disabled: isConnecting,
1969
+ title: "For your whole org",
1930
1970
  onClick: () => setKind("org"),
1931
1971
  className: cn(
1932
1972
  "rounded-full border px-2 py-0.5 text-[11px]",
1933
1973
  kind === "org" ? "border-primary bg-primary/10 font-medium" : "border-border text-muted-foreground",
1934
1974
  "disabled:opacity-50"
1935
1975
  ),
1936
- children: [
1937
- "Org",
1938
- !orgPermitted && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "sr-only", children: [
1939
- " (",
1940
- orgDisabledReason,
1941
- ")"
1942
- ] })
1943
- ]
1976
+ children: "Org"
1944
1977
  }
1945
1978
  )
1946
1979
  ]
@@ -1954,13 +1987,17 @@ function ConnectorTile({
1954
1987
  {
1955
1988
  type: "button",
1956
1989
  "data-testid": `connector-reconnect-${entry.slug}`,
1957
- onClick: () => onConnect(entry, entry.connection.kind ?? "personal"),
1990
+ onClick: () => onConnect(entry, entry.connection.kind ?? "personal", entry.connection.access ?? void 0),
1958
1991
  disabled: isConnecting || isDisconnecting,
1959
1992
  className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
1960
1993
  children: isConnecting ? "Reconnecting\u2026" : "Reconnect"
1961
1994
  }
1962
1995
  ),
1963
- canDisconnect && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1996
+ canDisconnect && // DEV-11580: the revoke reassurance lives in the title only —
1997
+ // as an always-visible paragraph it wrapped to four lines
1998
+ // beside every connected tile. Muted until hover so a
1999
+ // destructive action isn't the loudest thing on the tile.
2000
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1964
2001
  "button",
1965
2002
  {
1966
2003
  type: "button",
@@ -1968,27 +2005,10 @@ function ConnectorTile({
1968
2005
  title: DISCONNECT_REVOKES_HINT,
1969
2006
  onClick: () => onDisconnect(entry),
1970
2007
  disabled: isDisconnecting || isConnecting,
1971
- className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-destructive hover:bg-destructive/10 disabled:opacity-50",
2008
+ className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-muted-foreground hover:bg-destructive/10 hover:text-destructive disabled:opacity-50",
1972
2009
  children: isDisconnecting ? "Disconnecting\u2026" : "Disconnect"
1973
2010
  }
1974
2011
  ),
1975
- canDisconnect && // Outcome-eval finding (DEV-7873): with no width cap, this
1976
- // paragraph's one long sentence forced the WHOLE shrink-0
1977
- // button column to grow to its natural (unwrapped) width,
1978
- // squeezing the sibling `min-w-0 flex-1` text column down to
1979
- // one word per line at any realistic tile width. RTL's jsdom
1980
- // tests never caught it — jsdom doesn't lay out real flexbox
1981
- // sizing, so every textContent assertion still passed. A
1982
- // narrow max-width forces the hint to wrap within the same
1983
- // footprint as the button above it instead.
1984
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1985
- "p",
1986
- {
1987
- "data-testid": `connector-disconnect-hint-${entry.slug}`,
1988
- className: "max-w-28 text-right text-[11px] text-muted-foreground",
1989
- children: DISCONNECT_REVOKES_HINT
1990
- }
1991
- ),
1992
2012
  canManageSharing && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1993
2013
  "button",
1994
2014
  {
@@ -2006,7 +2026,7 @@ function ConnectorTile({
2006
2026
  {
2007
2027
  type: "button",
2008
2028
  "data-testid": `connector-connect-${entry.slug}`,
2009
- onClick: () => onConnect(entry, kind),
2029
+ onClick: () => onConnect(entry, connectKind, connectAccess),
2010
2030
  disabled: isConnecting,
2011
2031
  className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
2012
2032
  children: isConnecting ? "Connecting\u2026" : "Connect"
@@ -2039,7 +2059,7 @@ function ConnectorTile({
2039
2059
  {
2040
2060
  entry,
2041
2061
  fields: credentialFields,
2042
- kind,
2062
+ kind: connectKind,
2043
2063
  verify: guide?.verify ?? null,
2044
2064
  testSupported: credentialTestSupported,
2045
2065
  onSubmitCredentials,
@@ -2579,6 +2599,7 @@ function ManageSharing({
2579
2599
  REEVE_ACCENT,
2580
2600
  ResponsiveHeader,
2581
2601
  WORDMARK_WEIGHT,
2602
+ accessLabel,
2582
2603
  deriveFallbackLadder,
2583
2604
  faviconUrl,
2584
2605
  groupByOrg,
package/dist/index.mjs CHANGED
@@ -1407,6 +1407,9 @@ function ContextSwitcher({
1407
1407
  // src/connector-picker.tsx
1408
1408
  import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef5, useState as useState7 } from "react";
1409
1409
  import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1410
+ function accessLabel(access) {
1411
+ return access === "read" ? "Read" : "Read + write";
1412
+ }
1410
1413
  var DEFAULT_CONNECT_ERROR = "Couldn't start the connection. Please try again.";
1411
1414
  var MAX_RECONNECT_REFOCUS_CHECKS = 3;
1412
1415
  var RECONNECT_UNCONFIRMED_ERROR = "Couldn't confirm the reconnect \u2014 the connection still reports unhealthy. Try again, or disconnect and reconnect.";
@@ -1415,7 +1418,6 @@ var DEFAULT_LOAD_ERROR = "Couldn't load connections. Please try again.";
1415
1418
  var DEFAULT_GRANTS_LOAD_ERROR = "Couldn't load sharing. Please try again.";
1416
1419
  var DEFAULT_GRANT_ERROR = "Couldn't share. Please try again.";
1417
1420
  var DEFAULT_REVOKE_ERROR = "Couldn't revoke access. Please try again.";
1418
- var DEFAULT_ORG_DISABLED_REASON = "Org-wide connections aren't available to you.";
1419
1421
  var DEFAULT_CREDENTIAL_ERROR = "Couldn't save that credential. Please try again.";
1420
1422
  var DEFAULT_CREDENTIAL_TEST_ERROR = "Couldn't test that credential. Please try again.";
1421
1423
  var DEFAULT_RECONNECT_MESSAGE = "This connection needs to be reconnected to keep working.";
@@ -1429,15 +1431,17 @@ function hasUngroundedPlaceholder(text) {
1429
1431
  function isDevEnvironment() {
1430
1432
  return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
1431
1433
  }
1432
- function capabilityLabel(capability) {
1433
- if (capability === "read") return "Read only";
1434
- if (capability === "write") return "Write only";
1435
- return "Read & write";
1434
+ function tileAccess(entry) {
1435
+ if (entry.connection.status === "connected" && entry.connection.access) {
1436
+ return entry.connection.access;
1437
+ }
1438
+ return entry.capability;
1436
1439
  }
1437
- function formatSince(since) {
1438
- const parsed = new Date(since);
1439
- if (Number.isNaN(parsed.getTime())) return since;
1440
- return parsed.toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
1440
+ function accessChoices(entry) {
1441
+ const options = (entry.access_options ?? []).filter(
1442
+ (a) => a === "read" || a === "readwrite"
1443
+ );
1444
+ return options.length > 1 ? options : [];
1441
1445
  }
1442
1446
  function errorMessageFrom(err, fallback) {
1443
1447
  return err instanceof Error && err.message ? err.message : fallback;
@@ -1556,7 +1560,7 @@ function ConnectorPicker({
1556
1560
  };
1557
1561
  }, [gated, refreshAfterReturn]);
1558
1562
  const handleConnect = useCallback4(
1559
- async (entry, kind) => {
1563
+ async (entry, kind, access) => {
1560
1564
  if (isUnhealthy(entry)) {
1561
1565
  reconnectingRef.current.add(entry.slug);
1562
1566
  } else {
@@ -1565,7 +1569,10 @@ function ConnectorPicker({
1565
1569
  reconnectChecksRef.current.delete(entry.slug);
1566
1570
  setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "connecting" } }));
1567
1571
  try {
1568
- const result = await clientRef.current.connect(entry, { kind });
1572
+ const result = await clientRef.current.connect(
1573
+ entry,
1574
+ access ? { kind, access } : { kind }
1575
+ );
1569
1576
  if (result?.connect_url) {
1570
1577
  window.open(result.connect_url, "_blank", "noopener,noreferrer");
1571
1578
  return;
@@ -1755,6 +1762,9 @@ function ConnectorTile({
1755
1762
  const errorMessage = ui?.kind === "error" ? ui.message : null;
1756
1763
  const unhealthy = isUnhealthy(entry);
1757
1764
  const [kind, setKind] = useState7("personal");
1765
+ const choices = accessChoices(entry);
1766
+ const [access, setAccess] = useState7(null);
1767
+ const connectAccess = choices.length ? access && choices.includes(access) ? access : choices[choices.length - 1] : void 0;
1758
1768
  const [sharingOpen, setSharingOpen] = useState7(false);
1759
1769
  const [setupOpen, setSetupOpen] = useState7(false);
1760
1770
  const credentialFields = entry.credential_fields ?? [];
@@ -1762,7 +1772,7 @@ function ConnectorTile({
1762
1772
  const guide = entry.guide ?? null;
1763
1773
  const canSetUp = !isConnected && (pasteSupported || guide !== null);
1764
1774
  const orgPermitted = entry.org_connect?.permitted === true;
1765
- const orgDisabledReason = entry.org_connect?.reason || DEFAULT_ORG_DISABLED_REASON;
1775
+ const connectKind = orgPermitted ? kind : "personal";
1766
1776
  const attribution = ownerLabel(entry);
1767
1777
  const canDisconnect = isConnected && !shared;
1768
1778
  const canManageSharing = isConnected && entry.connection.owned_by_viewer === true && grantsSupported;
@@ -1789,20 +1799,22 @@ function ConnectorTile({
1789
1799
  )
1790
1800
  ] }),
1791
1801
  /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
1792
- /* @__PURE__ */ jsx8("span", { "data-testid": `connector-capability-${entry.slug}`, children: capabilityLabel(entry.capability) }),
1793
- entry.category && /* @__PURE__ */ jsxs7(Fragment4, { children: [
1802
+ (isConnected || choices.length === 0) && /* @__PURE__ */ jsx8("span", { "data-testid": `connector-capability-${entry.slug}`, children: accessLabel(tileAccess(entry)) }),
1803
+ isConnected && /* @__PURE__ */ jsxs7(Fragment4, { children: [
1794
1804
  /* @__PURE__ */ jsx8("span", { "aria-hidden": "true", children: "\xB7" }),
1795
- /* @__PURE__ */ jsx8("span", { className: "truncate", children: entry.category })
1805
+ /* @__PURE__ */ jsxs7(
1806
+ "span",
1807
+ {
1808
+ "data-testid": `connector-connected-${entry.slug}`,
1809
+ className: "inline-flex items-center gap-1",
1810
+ children: [
1811
+ /* @__PURE__ */ jsx8("span", { "aria-hidden": "true", className: "h-1.5 w-1.5 rounded-full bg-emerald-500" }),
1812
+ "Connected"
1813
+ ]
1814
+ }
1815
+ )
1796
1816
  ] })
1797
1817
  ] }),
1798
- isConnected && /* @__PURE__ */ jsx8(
1799
- "p",
1800
- {
1801
- "data-testid": `connector-connected-since-${entry.slug}`,
1802
- className: "text-xs text-muted-foreground",
1803
- children: entry.connection.since ? `Connected since ${formatSince(entry.connection.since)}` : "Connected"
1804
- }
1805
- ),
1806
1818
  isConnected && !shared && entry.connection.account_label && /* @__PURE__ */ jsxs7(
1807
1819
  "p",
1808
1820
  {
@@ -1839,7 +1851,33 @@ function ConnectorTile({
1839
1851
  children: attribution
1840
1852
  }
1841
1853
  ),
1842
- !isConnected && /* @__PURE__ */ jsxs7(
1854
+ !isConnected && choices.length > 0 && /* @__PURE__ */ jsx8(
1855
+ "div",
1856
+ {
1857
+ "data-testid": `connector-access-choice-${entry.slug}`,
1858
+ role: "group",
1859
+ "aria-label": "Access",
1860
+ className: "mt-1 flex items-center gap-1",
1861
+ children: choices.map((option) => /* @__PURE__ */ jsx8(
1862
+ "button",
1863
+ {
1864
+ type: "button",
1865
+ "data-testid": `connector-access-${option}-${entry.slug}`,
1866
+ "aria-pressed": connectAccess === option,
1867
+ disabled: isConnecting,
1868
+ onClick: () => setAccess(option),
1869
+ className: cn(
1870
+ "rounded-full border px-2 py-0.5 text-[11px]",
1871
+ connectAccess === option ? "border-primary bg-primary/10 font-medium" : "border-border text-muted-foreground",
1872
+ "disabled:opacity-50"
1873
+ ),
1874
+ children: accessLabel(option)
1875
+ },
1876
+ option
1877
+ ))
1878
+ }
1879
+ ),
1880
+ !isConnected && orgPermitted && /* @__PURE__ */ jsxs7(
1843
1881
  "div",
1844
1882
  {
1845
1883
  "data-testid": `connector-kind-toggle-${entry.slug}`,
@@ -1854,6 +1892,7 @@ function ConnectorTile({
1854
1892
  "data-testid": `connector-kind-personal-${entry.slug}`,
1855
1893
  "aria-pressed": kind === "personal",
1856
1894
  disabled: isConnecting,
1895
+ title: "Just for you",
1857
1896
  onClick: () => setKind("personal"),
1858
1897
  className: cn(
1859
1898
  "rounded-full border px-2 py-0.5 text-[11px]",
@@ -1863,28 +1902,21 @@ function ConnectorTile({
1863
1902
  children: "Personal"
1864
1903
  }
1865
1904
  ),
1866
- /* @__PURE__ */ jsxs7(
1905
+ /* @__PURE__ */ jsx8(
1867
1906
  "button",
1868
1907
  {
1869
1908
  type: "button",
1870
1909
  "data-testid": `connector-kind-org-${entry.slug}`,
1871
1910
  "aria-pressed": kind === "org",
1872
- disabled: isConnecting || !orgPermitted,
1873
- title: orgPermitted ? void 0 : orgDisabledReason,
1911
+ disabled: isConnecting,
1912
+ title: "For your whole org",
1874
1913
  onClick: () => setKind("org"),
1875
1914
  className: cn(
1876
1915
  "rounded-full border px-2 py-0.5 text-[11px]",
1877
1916
  kind === "org" ? "border-primary bg-primary/10 font-medium" : "border-border text-muted-foreground",
1878
1917
  "disabled:opacity-50"
1879
1918
  ),
1880
- children: [
1881
- "Org",
1882
- !orgPermitted && /* @__PURE__ */ jsxs7("span", { className: "sr-only", children: [
1883
- " (",
1884
- orgDisabledReason,
1885
- ")"
1886
- ] })
1887
- ]
1919
+ children: "Org"
1888
1920
  }
1889
1921
  )
1890
1922
  ]
@@ -1898,13 +1930,17 @@ function ConnectorTile({
1898
1930
  {
1899
1931
  type: "button",
1900
1932
  "data-testid": `connector-reconnect-${entry.slug}`,
1901
- onClick: () => onConnect(entry, entry.connection.kind ?? "personal"),
1933
+ onClick: () => onConnect(entry, entry.connection.kind ?? "personal", entry.connection.access ?? void 0),
1902
1934
  disabled: isConnecting || isDisconnecting,
1903
1935
  className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
1904
1936
  children: isConnecting ? "Reconnecting\u2026" : "Reconnect"
1905
1937
  }
1906
1938
  ),
1907
- canDisconnect && /* @__PURE__ */ jsx8(
1939
+ canDisconnect && // DEV-11580: the revoke reassurance lives in the title only —
1940
+ // as an always-visible paragraph it wrapped to four lines
1941
+ // beside every connected tile. Muted until hover so a
1942
+ // destructive action isn't the loudest thing on the tile.
1943
+ /* @__PURE__ */ jsx8(
1908
1944
  "button",
1909
1945
  {
1910
1946
  type: "button",
@@ -1912,27 +1948,10 @@ function ConnectorTile({
1912
1948
  title: DISCONNECT_REVOKES_HINT,
1913
1949
  onClick: () => onDisconnect(entry),
1914
1950
  disabled: isDisconnecting || isConnecting,
1915
- className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-destructive hover:bg-destructive/10 disabled:opacity-50",
1951
+ className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-muted-foreground hover:bg-destructive/10 hover:text-destructive disabled:opacity-50",
1916
1952
  children: isDisconnecting ? "Disconnecting\u2026" : "Disconnect"
1917
1953
  }
1918
1954
  ),
1919
- canDisconnect && // Outcome-eval finding (DEV-7873): with no width cap, this
1920
- // paragraph's one long sentence forced the WHOLE shrink-0
1921
- // button column to grow to its natural (unwrapped) width,
1922
- // squeezing the sibling `min-w-0 flex-1` text column down to
1923
- // one word per line at any realistic tile width. RTL's jsdom
1924
- // tests never caught it — jsdom doesn't lay out real flexbox
1925
- // sizing, so every textContent assertion still passed. A
1926
- // narrow max-width forces the hint to wrap within the same
1927
- // footprint as the button above it instead.
1928
- /* @__PURE__ */ jsx8(
1929
- "p",
1930
- {
1931
- "data-testid": `connector-disconnect-hint-${entry.slug}`,
1932
- className: "max-w-28 text-right text-[11px] text-muted-foreground",
1933
- children: DISCONNECT_REVOKES_HINT
1934
- }
1935
- ),
1936
1955
  canManageSharing && /* @__PURE__ */ jsx8(
1937
1956
  "button",
1938
1957
  {
@@ -1950,7 +1969,7 @@ function ConnectorTile({
1950
1969
  {
1951
1970
  type: "button",
1952
1971
  "data-testid": `connector-connect-${entry.slug}`,
1953
- onClick: () => onConnect(entry, kind),
1972
+ onClick: () => onConnect(entry, connectKind, connectAccess),
1954
1973
  disabled: isConnecting,
1955
1974
  className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
1956
1975
  children: isConnecting ? "Connecting\u2026" : "Connect"
@@ -1983,7 +2002,7 @@ function ConnectorTile({
1983
2002
  {
1984
2003
  entry,
1985
2004
  fields: credentialFields,
1986
- kind,
2005
+ kind: connectKind,
1987
2006
  verify: guide?.verify ?? null,
1988
2007
  testSupported: credentialTestSupported,
1989
2008
  onSubmitCredentials,
@@ -2522,6 +2541,7 @@ export {
2522
2541
  REEVE_ACCENT,
2523
2542
  ResponsiveHeader,
2524
2543
  WORDMARK_WEIGHT,
2544
+ accessLabel,
2525
2545
  deriveFallbackLadder,
2526
2546
  faviconUrl,
2527
2547
  groupByOrg,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meetreeve/ui",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Shared brand-adaptive UI primitives for MindFortress frontends — min-size tokens, a favicon→logo→monogram BrandGlyph, a responsive header that keeps the search bar usable on mobile, and AppSidebar's viewport-driven auto-collapse ladder.",
5
5
  "license": "UNLICENSED",
6
6
  "main": "./dist/index.js",