@meetreeve/ui 0.10.0 → 0.12.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 +49 -1
- package/dist/index.d.ts +49 -1
- package/dist/index.js +120 -13
- package/dist/index.mjs +123 -16
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -555,6 +555,17 @@ interface ContextSwitcherItem {
|
|
|
555
555
|
accentColor?: string | null;
|
|
556
556
|
orgId?: string | null;
|
|
557
557
|
orgName?: string | null;
|
|
558
|
+
/**
|
|
559
|
+
* Pre-resolved noun for what this context IS ("Company", "Space", …),
|
|
560
|
+
* rendered as a muted marker on the row. Optional, and omitted for a
|
|
561
|
+
* host's default kind — labelling every row is noise.
|
|
562
|
+
*
|
|
563
|
+
* Resolved by the host (DEV-8585), matching the `favicon` precedent
|
|
564
|
+
* above: two contexts can share a display name (a brand and a spawned
|
|
565
|
+
* company in the same org both render as "MindFortress"), and only the
|
|
566
|
+
* host knows its own vocabulary for telling them apart.
|
|
567
|
+
*/
|
|
568
|
+
kindLabel?: string | null;
|
|
558
569
|
}
|
|
559
570
|
interface ContextOrgGroup {
|
|
560
571
|
orgId: string | null;
|
|
@@ -708,6 +719,19 @@ interface ConnectorCredentialTestResult {
|
|
|
708
719
|
*/
|
|
709
720
|
permissions_note?: string | null;
|
|
710
721
|
}
|
|
722
|
+
/**
|
|
723
|
+
* Connector health as the DEV-1964 probe classifies it — Reeve.Connects-wide
|
|
724
|
+
* vocabulary, not provider-specific (the same shape the adbuyer surfaces
|
|
725
|
+
* already read off `GET /api/connects/v1/connections`). Deliberately
|
|
726
|
+
* tri-state, never a `needs_reconnect` boolean (Matt's call, DEV-7873): a
|
|
727
|
+
* probe that has not run yet ("unknown") or has not run at all (the whole
|
|
728
|
+
* field absent) is not the same claim as a probe that ran and failed.
|
|
729
|
+
*/
|
|
730
|
+
interface ConnectorConnectionHealth {
|
|
731
|
+
status: "healthy" | "unhealthy" | "unknown";
|
|
732
|
+
error_code?: string | null;
|
|
733
|
+
checked_at?: string | null;
|
|
734
|
+
}
|
|
711
735
|
interface ConnectableEntry {
|
|
712
736
|
slug: string;
|
|
713
737
|
name: string;
|
|
@@ -738,6 +762,21 @@ interface ConnectableEntry {
|
|
|
738
762
|
} | null;
|
|
739
763
|
/** true = the viewer owns this connection (enables manage-sharing). */
|
|
740
764
|
owned_by_viewer?: boolean;
|
|
765
|
+
/**
|
|
766
|
+
* Live health probe for this connection (DEV-1964 / DEV-7873), when the
|
|
767
|
+
* backend has one. Absent/undefined is NOT "healthy" — it means no probe
|
|
768
|
+
* has run; the picker only ever reacts to an explicit "unhealthy"
|
|
769
|
+
* status, so "unknown" and absent both render as a plain connected
|
|
770
|
+
* tile, same as before this field existed.
|
|
771
|
+
*/
|
|
772
|
+
health?: ConnectorConnectionHealth | null;
|
|
773
|
+
/**
|
|
774
|
+
* Human-readable identity of the external account this connection
|
|
775
|
+
* authenticated as (an email, a workspace name, whatever the provider's
|
|
776
|
+
* own identity claim resolves to). Optional and provider-agnostic — the
|
|
777
|
+
* picker never assumes an email shape or synthesizes this itself.
|
|
778
|
+
*/
|
|
779
|
+
account_label?: string | null;
|
|
741
780
|
};
|
|
742
781
|
/** true = tenant has a live connection but the entry left the allowlist. */
|
|
743
782
|
deprecated: boolean;
|
|
@@ -755,6 +794,15 @@ interface ConnectableEntry {
|
|
|
755
794
|
* connect button renders, exactly as before.
|
|
756
795
|
*/
|
|
757
796
|
credential_fields?: ConnectorCredentialField[] | null;
|
|
797
|
+
/**
|
|
798
|
+
* Plain-language description of what a LIVE connection actually reads or
|
|
799
|
+
* writes (e.g. "Reads recent activity to build your weekly summary") —
|
|
800
|
+
* entirely server-authored copy, same posture as `category`/`name`.
|
|
801
|
+
* Rendered only once connected (it describes a live grant, not the
|
|
802
|
+
* catalog listing); absent renders nothing. This file must never
|
|
803
|
+
* synthesize this text itself for a specific provider.
|
|
804
|
+
*/
|
|
805
|
+
access_summary?: string | null;
|
|
758
806
|
}
|
|
759
807
|
interface ConnectorPickerClient {
|
|
760
808
|
listConnectable(opts: {
|
|
@@ -818,4 +866,4 @@ interface ConnectorPickerProps {
|
|
|
818
866
|
}
|
|
819
867
|
declare function ConnectorPicker({ client, purpose, lens, layout, onConnected, }: ConnectorPickerProps): react.JSX.Element | null;
|
|
820
868
|
|
|
821
|
-
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 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, 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 };
|
|
869
|
+
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, 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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -555,6 +555,17 @@ interface ContextSwitcherItem {
|
|
|
555
555
|
accentColor?: string | null;
|
|
556
556
|
orgId?: string | null;
|
|
557
557
|
orgName?: string | null;
|
|
558
|
+
/**
|
|
559
|
+
* Pre-resolved noun for what this context IS ("Company", "Space", …),
|
|
560
|
+
* rendered as a muted marker on the row. Optional, and omitted for a
|
|
561
|
+
* host's default kind — labelling every row is noise.
|
|
562
|
+
*
|
|
563
|
+
* Resolved by the host (DEV-8585), matching the `favicon` precedent
|
|
564
|
+
* above: two contexts can share a display name (a brand and a spawned
|
|
565
|
+
* company in the same org both render as "MindFortress"), and only the
|
|
566
|
+
* host knows its own vocabulary for telling them apart.
|
|
567
|
+
*/
|
|
568
|
+
kindLabel?: string | null;
|
|
558
569
|
}
|
|
559
570
|
interface ContextOrgGroup {
|
|
560
571
|
orgId: string | null;
|
|
@@ -708,6 +719,19 @@ interface ConnectorCredentialTestResult {
|
|
|
708
719
|
*/
|
|
709
720
|
permissions_note?: string | null;
|
|
710
721
|
}
|
|
722
|
+
/**
|
|
723
|
+
* Connector health as the DEV-1964 probe classifies it — Reeve.Connects-wide
|
|
724
|
+
* vocabulary, not provider-specific (the same shape the adbuyer surfaces
|
|
725
|
+
* already read off `GET /api/connects/v1/connections`). Deliberately
|
|
726
|
+
* tri-state, never a `needs_reconnect` boolean (Matt's call, DEV-7873): a
|
|
727
|
+
* probe that has not run yet ("unknown") or has not run at all (the whole
|
|
728
|
+
* field absent) is not the same claim as a probe that ran and failed.
|
|
729
|
+
*/
|
|
730
|
+
interface ConnectorConnectionHealth {
|
|
731
|
+
status: "healthy" | "unhealthy" | "unknown";
|
|
732
|
+
error_code?: string | null;
|
|
733
|
+
checked_at?: string | null;
|
|
734
|
+
}
|
|
711
735
|
interface ConnectableEntry {
|
|
712
736
|
slug: string;
|
|
713
737
|
name: string;
|
|
@@ -738,6 +762,21 @@ interface ConnectableEntry {
|
|
|
738
762
|
} | null;
|
|
739
763
|
/** true = the viewer owns this connection (enables manage-sharing). */
|
|
740
764
|
owned_by_viewer?: boolean;
|
|
765
|
+
/**
|
|
766
|
+
* Live health probe for this connection (DEV-1964 / DEV-7873), when the
|
|
767
|
+
* backend has one. Absent/undefined is NOT "healthy" — it means no probe
|
|
768
|
+
* has run; the picker only ever reacts to an explicit "unhealthy"
|
|
769
|
+
* status, so "unknown" and absent both render as a plain connected
|
|
770
|
+
* tile, same as before this field existed.
|
|
771
|
+
*/
|
|
772
|
+
health?: ConnectorConnectionHealth | null;
|
|
773
|
+
/**
|
|
774
|
+
* Human-readable identity of the external account this connection
|
|
775
|
+
* authenticated as (an email, a workspace name, whatever the provider's
|
|
776
|
+
* own identity claim resolves to). Optional and provider-agnostic — the
|
|
777
|
+
* picker never assumes an email shape or synthesizes this itself.
|
|
778
|
+
*/
|
|
779
|
+
account_label?: string | null;
|
|
741
780
|
};
|
|
742
781
|
/** true = tenant has a live connection but the entry left the allowlist. */
|
|
743
782
|
deprecated: boolean;
|
|
@@ -755,6 +794,15 @@ interface ConnectableEntry {
|
|
|
755
794
|
* connect button renders, exactly as before.
|
|
756
795
|
*/
|
|
757
796
|
credential_fields?: ConnectorCredentialField[] | null;
|
|
797
|
+
/**
|
|
798
|
+
* Plain-language description of what a LIVE connection actually reads or
|
|
799
|
+
* writes (e.g. "Reads recent activity to build your weekly summary") —
|
|
800
|
+
* entirely server-authored copy, same posture as `category`/`name`.
|
|
801
|
+
* Rendered only once connected (it describes a live grant, not the
|
|
802
|
+
* catalog listing); absent renders nothing. This file must never
|
|
803
|
+
* synthesize this text itself for a specific provider.
|
|
804
|
+
*/
|
|
805
|
+
access_summary?: string | null;
|
|
758
806
|
}
|
|
759
807
|
interface ConnectorPickerClient {
|
|
760
808
|
listConnectable(opts: {
|
|
@@ -818,4 +866,4 @@ interface ConnectorPickerProps {
|
|
|
818
866
|
}
|
|
819
867
|
declare function ConnectorPicker({ client, purpose, lens, layout, onConnected, }: ConnectorPickerProps): react.JSX.Element | null;
|
|
820
868
|
|
|
821
|
-
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 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, 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 };
|
|
869
|
+
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, 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 };
|
package/dist/index.js
CHANGED
|
@@ -1403,6 +1403,10 @@ function ContextSwitcher({
|
|
|
1403
1403
|
children: [
|
|
1404
1404
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BrandGlyph, { brand: toBrand(item), subtle: true }),
|
|
1405
1405
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "flex-1 truncate text-left text-foreground/90", children: item.name }),
|
|
1406
|
+
item.kindLabel && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
|
|
1407
|
+
" ",
|
|
1408
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "shrink-0 text-[11px] text-muted-foreground", children: item.kindLabel })
|
|
1409
|
+
] }),
|
|
1406
1410
|
isActive && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.Check, { className: "size-3.5 text-emerald-500" })
|
|
1407
1411
|
]
|
|
1408
1412
|
},
|
|
@@ -1421,6 +1425,8 @@ function ContextSwitcher({
|
|
|
1421
1425
|
var import_react9 = require("react");
|
|
1422
1426
|
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
1423
1427
|
var DEFAULT_CONNECT_ERROR = "Couldn't start the connection. Please try again.";
|
|
1428
|
+
var MAX_RECONNECT_REFOCUS_CHECKS = 3;
|
|
1429
|
+
var RECONNECT_UNCONFIRMED_ERROR = "Couldn't confirm the reconnect \u2014 the connection still reports unhealthy. Try again, or disconnect and reconnect.";
|
|
1424
1430
|
var DEFAULT_DISCONNECT_ERROR = "Couldn't disconnect. Please try again.";
|
|
1425
1431
|
var DEFAULT_LOAD_ERROR = "Couldn't load connections. Please try again.";
|
|
1426
1432
|
var DEFAULT_GRANTS_LOAD_ERROR = "Couldn't load sharing. Please try again.";
|
|
@@ -1429,6 +1435,8 @@ var DEFAULT_REVOKE_ERROR = "Couldn't revoke access. Please try again.";
|
|
|
1429
1435
|
var DEFAULT_ORG_DISABLED_REASON = "Org-wide connections aren't available to you.";
|
|
1430
1436
|
var DEFAULT_CREDENTIAL_ERROR = "Couldn't save that credential. Please try again.";
|
|
1431
1437
|
var DEFAULT_CREDENTIAL_TEST_ERROR = "Couldn't test that credential. Please try again.";
|
|
1438
|
+
var DEFAULT_RECONNECT_MESSAGE = "This connection needs to be reconnected to keep working.";
|
|
1439
|
+
var DISCONNECT_REVOKES_HINT = "Disconnecting revokes Reeve's access to this account.";
|
|
1432
1440
|
var UNVERIFIABLE_CREDENTIAL_NOTE = "We can't verify this automatically \u2014 saving it is safe, but nothing has checked it yet.";
|
|
1433
1441
|
var SKELETON_COUNT = 3;
|
|
1434
1442
|
var UNGROUNDED_PLACEHOLDER = /\{\{[^}]*\}\}/;
|
|
@@ -1462,6 +1470,9 @@ function ownerLabel(entry) {
|
|
|
1462
1470
|
function isSharedWithViewer(entry) {
|
|
1463
1471
|
return entry.connection.status === "connected" && Boolean(entry.connection.shared_by) && !entry.connection.owned_by_viewer;
|
|
1464
1472
|
}
|
|
1473
|
+
function isUnhealthy(entry) {
|
|
1474
|
+
return entry.connection.status === "connected" && entry.connection.health?.status === "unhealthy";
|
|
1475
|
+
}
|
|
1465
1476
|
function ConnectorPicker({
|
|
1466
1477
|
client,
|
|
1467
1478
|
purpose,
|
|
@@ -1473,6 +1484,9 @@ function ConnectorPicker({
|
|
|
1473
1484
|
const [entries, setEntries] = (0, import_react9.useState)(null);
|
|
1474
1485
|
const [loadError, setLoadError] = (0, import_react9.useState)(null);
|
|
1475
1486
|
const [uiState, setUiState] = (0, import_react9.useState)({});
|
|
1487
|
+
const reconnectingRef = (0, import_react9.useRef)(/* @__PURE__ */ new Set());
|
|
1488
|
+
const reconnectChecksRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
|
|
1489
|
+
const refreshInFlightRef = (0, import_react9.useRef)(false);
|
|
1476
1490
|
const grantsSupported = typeof client.listGrants === "function" && typeof client.grant === "function" && typeof client.revokeGrant === "function";
|
|
1477
1491
|
const credentialsSupported = typeof client.submitCredentials === "function";
|
|
1478
1492
|
const credentialTestSupported = typeof client.testCredentials === "function";
|
|
@@ -1501,20 +1515,45 @@ function ConnectorPicker({
|
|
|
1501
1515
|
void load();
|
|
1502
1516
|
}, [gated, load, purpose]);
|
|
1503
1517
|
const refreshAfterReturn = (0, import_react9.useCallback)(async () => {
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1518
|
+
if (refreshInFlightRef.current) return;
|
|
1519
|
+
refreshInFlightRef.current = true;
|
|
1520
|
+
try {
|
|
1521
|
+
const list = await load();
|
|
1522
|
+
if (!list) return;
|
|
1523
|
+
const stillUnhealthyChecks = /* @__PURE__ */ new Map();
|
|
1509
1524
|
for (const entry of list) {
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1525
|
+
const stillUnhealthy = reconnectingRef.current.has(entry.slug) && entry.connection.health?.status === "unhealthy";
|
|
1526
|
+
if (!stillUnhealthy) continue;
|
|
1527
|
+
const checks = (reconnectChecksRef.current.get(entry.slug) ?? 0) + 1;
|
|
1528
|
+
reconnectChecksRef.current.set(entry.slug, checks);
|
|
1529
|
+
stillUnhealthyChecks.set(entry.slug, checks);
|
|
1515
1530
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1531
|
+
setUiState((prev) => {
|
|
1532
|
+
let changed = false;
|
|
1533
|
+
const next = { ...prev };
|
|
1534
|
+
for (const entry of list) {
|
|
1535
|
+
if (next[entry.slug]?.kind !== "connecting" || entry.connection.status !== "connected") {
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
const checks = stillUnhealthyChecks.get(entry.slug);
|
|
1539
|
+
if (checks !== void 0 && checks >= MAX_RECONNECT_REFOCUS_CHECKS) {
|
|
1540
|
+
changed = true;
|
|
1541
|
+
reconnectingRef.current.delete(entry.slug);
|
|
1542
|
+
reconnectChecksRef.current.delete(entry.slug);
|
|
1543
|
+
next[entry.slug] = { kind: "error", message: RECONNECT_UNCONFIRMED_ERROR };
|
|
1544
|
+
} else if (checks === void 0) {
|
|
1545
|
+
changed = true;
|
|
1546
|
+
delete next[entry.slug];
|
|
1547
|
+
reconnectingRef.current.delete(entry.slug);
|
|
1548
|
+
reconnectChecksRef.current.delete(entry.slug);
|
|
1549
|
+
onConnectedRef.current?.(entry);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
return changed ? next : prev;
|
|
1553
|
+
});
|
|
1554
|
+
} finally {
|
|
1555
|
+
refreshInFlightRef.current = false;
|
|
1556
|
+
}
|
|
1518
1557
|
}, [load]);
|
|
1519
1558
|
(0, import_react9.useEffect)(() => {
|
|
1520
1559
|
if (gated) return;
|
|
@@ -1535,6 +1574,12 @@ function ConnectorPicker({
|
|
|
1535
1574
|
}, [gated, refreshAfterReturn]);
|
|
1536
1575
|
const handleConnect = (0, import_react9.useCallback)(
|
|
1537
1576
|
async (entry, kind) => {
|
|
1577
|
+
if (isUnhealthy(entry)) {
|
|
1578
|
+
reconnectingRef.current.add(entry.slug);
|
|
1579
|
+
} else {
|
|
1580
|
+
reconnectingRef.current.delete(entry.slug);
|
|
1581
|
+
}
|
|
1582
|
+
reconnectChecksRef.current.delete(entry.slug);
|
|
1538
1583
|
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "connecting" } }));
|
|
1539
1584
|
try {
|
|
1540
1585
|
const result = await clientRef.current.connect(entry, { kind });
|
|
@@ -1544,6 +1589,8 @@ function ConnectorPicker({
|
|
|
1544
1589
|
}
|
|
1545
1590
|
const list = await load();
|
|
1546
1591
|
const updated = list?.find((e) => e.slug === entry.slug) ?? null;
|
|
1592
|
+
reconnectingRef.current.delete(entry.slug);
|
|
1593
|
+
reconnectChecksRef.current.delete(entry.slug);
|
|
1547
1594
|
setUiState((prev) => {
|
|
1548
1595
|
const next = { ...prev };
|
|
1549
1596
|
delete next[entry.slug];
|
|
@@ -1553,6 +1600,8 @@ function ConnectorPicker({
|
|
|
1553
1600
|
onConnectedRef.current?.(updated);
|
|
1554
1601
|
}
|
|
1555
1602
|
} catch (err) {
|
|
1603
|
+
reconnectingRef.current.delete(entry.slug);
|
|
1604
|
+
reconnectChecksRef.current.delete(entry.slug);
|
|
1556
1605
|
const message = errorMessageFrom(err, DEFAULT_CONNECT_ERROR);
|
|
1557
1606
|
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "error", message } }));
|
|
1558
1607
|
}
|
|
@@ -1721,6 +1770,7 @@ function ConnectorTile({
|
|
|
1721
1770
|
const isConnecting = ui?.kind === "connecting";
|
|
1722
1771
|
const isDisconnecting = ui?.kind === "disconnecting";
|
|
1723
1772
|
const errorMessage = ui?.kind === "error" ? ui.message : null;
|
|
1773
|
+
const unhealthy = isUnhealthy(entry);
|
|
1724
1774
|
const [kind, setKind] = (0, import_react9.useState)("personal");
|
|
1725
1775
|
const [sharingOpen, setSharingOpen] = (0, import_react9.useState)(false);
|
|
1726
1776
|
const [setupOpen, setSetupOpen] = (0, import_react9.useState)(false);
|
|
@@ -1770,6 +1820,34 @@ function ConnectorTile({
|
|
|
1770
1820
|
children: entry.connection.since ? `Connected since ${formatSince(entry.connection.since)}` : "Connected"
|
|
1771
1821
|
}
|
|
1772
1822
|
),
|
|
1823
|
+
isConnected && !shared && entry.connection.account_label && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
1824
|
+
"p",
|
|
1825
|
+
{
|
|
1826
|
+
"data-testid": `connector-account-${entry.slug}`,
|
|
1827
|
+
className: "text-xs text-muted-foreground",
|
|
1828
|
+
children: [
|
|
1829
|
+
"Connected as ",
|
|
1830
|
+
entry.connection.account_label
|
|
1831
|
+
]
|
|
1832
|
+
}
|
|
1833
|
+
),
|
|
1834
|
+
isConnected && entry.access_summary && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1835
|
+
"p",
|
|
1836
|
+
{
|
|
1837
|
+
"data-testid": `connector-access-summary-${entry.slug}`,
|
|
1838
|
+
className: "text-xs text-muted-foreground",
|
|
1839
|
+
children: entry.access_summary
|
|
1840
|
+
}
|
|
1841
|
+
),
|
|
1842
|
+
unhealthy && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1843
|
+
"p",
|
|
1844
|
+
{
|
|
1845
|
+
role: "alert",
|
|
1846
|
+
"data-testid": `connector-unhealthy-${entry.slug}`,
|
|
1847
|
+
className: "text-xs font-medium text-destructive",
|
|
1848
|
+
children: DEFAULT_RECONNECT_MESSAGE
|
|
1849
|
+
}
|
|
1850
|
+
),
|
|
1773
1851
|
attribution && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1774
1852
|
"p",
|
|
1775
1853
|
{
|
|
@@ -1832,17 +1910,46 @@ function ConnectorTile({
|
|
|
1832
1910
|
errorMessage && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { role: "alert", "data-testid": `connector-error-${entry.slug}`, className: "text-xs text-destructive", children: errorMessage })
|
|
1833
1911
|
] }),
|
|
1834
1912
|
isConnected ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex shrink-0 flex-col items-end gap-1", children: [
|
|
1913
|
+
unhealthy && !shared && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1914
|
+
"button",
|
|
1915
|
+
{
|
|
1916
|
+
type: "button",
|
|
1917
|
+
"data-testid": `connector-reconnect-${entry.slug}`,
|
|
1918
|
+
onClick: () => onConnect(entry, entry.connection.kind ?? "personal"),
|
|
1919
|
+
disabled: isConnecting || isDisconnecting,
|
|
1920
|
+
className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
|
|
1921
|
+
children: isConnecting ? "Reconnecting\u2026" : "Reconnect"
|
|
1922
|
+
}
|
|
1923
|
+
),
|
|
1835
1924
|
canDisconnect && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1836
1925
|
"button",
|
|
1837
1926
|
{
|
|
1838
1927
|
type: "button",
|
|
1839
1928
|
"data-testid": `connector-disconnect-${entry.slug}`,
|
|
1929
|
+
title: DISCONNECT_REVOKES_HINT,
|
|
1840
1930
|
onClick: () => onDisconnect(entry),
|
|
1841
|
-
disabled: isDisconnecting,
|
|
1931
|
+
disabled: isDisconnecting || isConnecting,
|
|
1842
1932
|
className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-destructive hover:bg-destructive/10 disabled:opacity-50",
|
|
1843
1933
|
children: isDisconnecting ? "Disconnecting\u2026" : "Disconnect"
|
|
1844
1934
|
}
|
|
1845
1935
|
),
|
|
1936
|
+
canDisconnect && // Outcome-eval finding (DEV-7873): with no width cap, this
|
|
1937
|
+
// paragraph's one long sentence forced the WHOLE shrink-0
|
|
1938
|
+
// button column to grow to its natural (unwrapped) width,
|
|
1939
|
+
// squeezing the sibling `min-w-0 flex-1` text column down to
|
|
1940
|
+
// one word per line at any realistic tile width. RTL's jsdom
|
|
1941
|
+
// tests never caught it — jsdom doesn't lay out real flexbox
|
|
1942
|
+
// sizing, so every textContent assertion still passed. A
|
|
1943
|
+
// narrow max-width forces the hint to wrap within the same
|
|
1944
|
+
// footprint as the button above it instead.
|
|
1945
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1946
|
+
"p",
|
|
1947
|
+
{
|
|
1948
|
+
"data-testid": `connector-disconnect-hint-${entry.slug}`,
|
|
1949
|
+
className: "max-w-28 text-right text-[11px] text-muted-foreground",
|
|
1950
|
+
children: DISCONNECT_REVOKES_HINT
|
|
1951
|
+
}
|
|
1952
|
+
),
|
|
1846
1953
|
canManageSharing && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1847
1954
|
"button",
|
|
1848
1955
|
{
|
package/dist/index.mjs
CHANGED
|
@@ -1273,7 +1273,7 @@ function DropdownMenuLabel({
|
|
|
1273
1273
|
}
|
|
1274
1274
|
|
|
1275
1275
|
// src/context-switcher.tsx
|
|
1276
|
-
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1276
|
+
import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1277
1277
|
function groupByOrg(items) {
|
|
1278
1278
|
return groupItemsByOrg(items, {
|
|
1279
1279
|
orgId: (it) => it.orgId,
|
|
@@ -1348,6 +1348,10 @@ function ContextSwitcher({
|
|
|
1348
1348
|
children: [
|
|
1349
1349
|
/* @__PURE__ */ jsx6(BrandGlyph, { brand: toBrand(item), subtle: true }),
|
|
1350
1350
|
/* @__PURE__ */ jsx6("span", { className: "flex-1 truncate text-left text-foreground/90", children: item.name }),
|
|
1351
|
+
item.kindLabel && /* @__PURE__ */ jsxs5(Fragment3, { children: [
|
|
1352
|
+
" ",
|
|
1353
|
+
/* @__PURE__ */ jsx6("span", { className: "shrink-0 text-[11px] text-muted-foreground", children: item.kindLabel })
|
|
1354
|
+
] }),
|
|
1351
1355
|
isActive && /* @__PURE__ */ jsx6(Check, { className: "size-3.5 text-emerald-500" })
|
|
1352
1356
|
]
|
|
1353
1357
|
},
|
|
@@ -1364,8 +1368,10 @@ function ContextSwitcher({
|
|
|
1364
1368
|
|
|
1365
1369
|
// src/connector-picker.tsx
|
|
1366
1370
|
import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef5, useState as useState7 } from "react";
|
|
1367
|
-
import { Fragment as
|
|
1371
|
+
import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1368
1372
|
var DEFAULT_CONNECT_ERROR = "Couldn't start the connection. Please try again.";
|
|
1373
|
+
var MAX_RECONNECT_REFOCUS_CHECKS = 3;
|
|
1374
|
+
var RECONNECT_UNCONFIRMED_ERROR = "Couldn't confirm the reconnect \u2014 the connection still reports unhealthy. Try again, or disconnect and reconnect.";
|
|
1369
1375
|
var DEFAULT_DISCONNECT_ERROR = "Couldn't disconnect. Please try again.";
|
|
1370
1376
|
var DEFAULT_LOAD_ERROR = "Couldn't load connections. Please try again.";
|
|
1371
1377
|
var DEFAULT_GRANTS_LOAD_ERROR = "Couldn't load sharing. Please try again.";
|
|
@@ -1374,6 +1380,8 @@ var DEFAULT_REVOKE_ERROR = "Couldn't revoke access. Please try again.";
|
|
|
1374
1380
|
var DEFAULT_ORG_DISABLED_REASON = "Org-wide connections aren't available to you.";
|
|
1375
1381
|
var DEFAULT_CREDENTIAL_ERROR = "Couldn't save that credential. Please try again.";
|
|
1376
1382
|
var DEFAULT_CREDENTIAL_TEST_ERROR = "Couldn't test that credential. Please try again.";
|
|
1383
|
+
var DEFAULT_RECONNECT_MESSAGE = "This connection needs to be reconnected to keep working.";
|
|
1384
|
+
var DISCONNECT_REVOKES_HINT = "Disconnecting revokes Reeve's access to this account.";
|
|
1377
1385
|
var UNVERIFIABLE_CREDENTIAL_NOTE = "We can't verify this automatically \u2014 saving it is safe, but nothing has checked it yet.";
|
|
1378
1386
|
var SKELETON_COUNT = 3;
|
|
1379
1387
|
var UNGROUNDED_PLACEHOLDER = /\{\{[^}]*\}\}/;
|
|
@@ -1407,6 +1415,9 @@ function ownerLabel(entry) {
|
|
|
1407
1415
|
function isSharedWithViewer(entry) {
|
|
1408
1416
|
return entry.connection.status === "connected" && Boolean(entry.connection.shared_by) && !entry.connection.owned_by_viewer;
|
|
1409
1417
|
}
|
|
1418
|
+
function isUnhealthy(entry) {
|
|
1419
|
+
return entry.connection.status === "connected" && entry.connection.health?.status === "unhealthy";
|
|
1420
|
+
}
|
|
1410
1421
|
function ConnectorPicker({
|
|
1411
1422
|
client,
|
|
1412
1423
|
purpose,
|
|
@@ -1418,6 +1429,9 @@ function ConnectorPicker({
|
|
|
1418
1429
|
const [entries, setEntries] = useState7(null);
|
|
1419
1430
|
const [loadError, setLoadError] = useState7(null);
|
|
1420
1431
|
const [uiState, setUiState] = useState7({});
|
|
1432
|
+
const reconnectingRef = useRef5(/* @__PURE__ */ new Set());
|
|
1433
|
+
const reconnectChecksRef = useRef5(/* @__PURE__ */ new Map());
|
|
1434
|
+
const refreshInFlightRef = useRef5(false);
|
|
1421
1435
|
const grantsSupported = typeof client.listGrants === "function" && typeof client.grant === "function" && typeof client.revokeGrant === "function";
|
|
1422
1436
|
const credentialsSupported = typeof client.submitCredentials === "function";
|
|
1423
1437
|
const credentialTestSupported = typeof client.testCredentials === "function";
|
|
@@ -1446,20 +1460,45 @@ function ConnectorPicker({
|
|
|
1446
1460
|
void load();
|
|
1447
1461
|
}, [gated, load, purpose]);
|
|
1448
1462
|
const refreshAfterReturn = useCallback4(async () => {
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1463
|
+
if (refreshInFlightRef.current) return;
|
|
1464
|
+
refreshInFlightRef.current = true;
|
|
1465
|
+
try {
|
|
1466
|
+
const list = await load();
|
|
1467
|
+
if (!list) return;
|
|
1468
|
+
const stillUnhealthyChecks = /* @__PURE__ */ new Map();
|
|
1454
1469
|
for (const entry of list) {
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1470
|
+
const stillUnhealthy = reconnectingRef.current.has(entry.slug) && entry.connection.health?.status === "unhealthy";
|
|
1471
|
+
if (!stillUnhealthy) continue;
|
|
1472
|
+
const checks = (reconnectChecksRef.current.get(entry.slug) ?? 0) + 1;
|
|
1473
|
+
reconnectChecksRef.current.set(entry.slug, checks);
|
|
1474
|
+
stillUnhealthyChecks.set(entry.slug, checks);
|
|
1460
1475
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1476
|
+
setUiState((prev) => {
|
|
1477
|
+
let changed = false;
|
|
1478
|
+
const next = { ...prev };
|
|
1479
|
+
for (const entry of list) {
|
|
1480
|
+
if (next[entry.slug]?.kind !== "connecting" || entry.connection.status !== "connected") {
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
const checks = stillUnhealthyChecks.get(entry.slug);
|
|
1484
|
+
if (checks !== void 0 && checks >= MAX_RECONNECT_REFOCUS_CHECKS) {
|
|
1485
|
+
changed = true;
|
|
1486
|
+
reconnectingRef.current.delete(entry.slug);
|
|
1487
|
+
reconnectChecksRef.current.delete(entry.slug);
|
|
1488
|
+
next[entry.slug] = { kind: "error", message: RECONNECT_UNCONFIRMED_ERROR };
|
|
1489
|
+
} else if (checks === void 0) {
|
|
1490
|
+
changed = true;
|
|
1491
|
+
delete next[entry.slug];
|
|
1492
|
+
reconnectingRef.current.delete(entry.slug);
|
|
1493
|
+
reconnectChecksRef.current.delete(entry.slug);
|
|
1494
|
+
onConnectedRef.current?.(entry);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return changed ? next : prev;
|
|
1498
|
+
});
|
|
1499
|
+
} finally {
|
|
1500
|
+
refreshInFlightRef.current = false;
|
|
1501
|
+
}
|
|
1463
1502
|
}, [load]);
|
|
1464
1503
|
useEffect6(() => {
|
|
1465
1504
|
if (gated) return;
|
|
@@ -1480,6 +1519,12 @@ function ConnectorPicker({
|
|
|
1480
1519
|
}, [gated, refreshAfterReturn]);
|
|
1481
1520
|
const handleConnect = useCallback4(
|
|
1482
1521
|
async (entry, kind) => {
|
|
1522
|
+
if (isUnhealthy(entry)) {
|
|
1523
|
+
reconnectingRef.current.add(entry.slug);
|
|
1524
|
+
} else {
|
|
1525
|
+
reconnectingRef.current.delete(entry.slug);
|
|
1526
|
+
}
|
|
1527
|
+
reconnectChecksRef.current.delete(entry.slug);
|
|
1483
1528
|
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "connecting" } }));
|
|
1484
1529
|
try {
|
|
1485
1530
|
const result = await clientRef.current.connect(entry, { kind });
|
|
@@ -1489,6 +1534,8 @@ function ConnectorPicker({
|
|
|
1489
1534
|
}
|
|
1490
1535
|
const list = await load();
|
|
1491
1536
|
const updated = list?.find((e) => e.slug === entry.slug) ?? null;
|
|
1537
|
+
reconnectingRef.current.delete(entry.slug);
|
|
1538
|
+
reconnectChecksRef.current.delete(entry.slug);
|
|
1492
1539
|
setUiState((prev) => {
|
|
1493
1540
|
const next = { ...prev };
|
|
1494
1541
|
delete next[entry.slug];
|
|
@@ -1498,6 +1545,8 @@ function ConnectorPicker({
|
|
|
1498
1545
|
onConnectedRef.current?.(updated);
|
|
1499
1546
|
}
|
|
1500
1547
|
} catch (err) {
|
|
1548
|
+
reconnectingRef.current.delete(entry.slug);
|
|
1549
|
+
reconnectChecksRef.current.delete(entry.slug);
|
|
1501
1550
|
const message = errorMessageFrom(err, DEFAULT_CONNECT_ERROR);
|
|
1502
1551
|
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "error", message } }));
|
|
1503
1552
|
}
|
|
@@ -1666,6 +1715,7 @@ function ConnectorTile({
|
|
|
1666
1715
|
const isConnecting = ui?.kind === "connecting";
|
|
1667
1716
|
const isDisconnecting = ui?.kind === "disconnecting";
|
|
1668
1717
|
const errorMessage = ui?.kind === "error" ? ui.message : null;
|
|
1718
|
+
const unhealthy = isUnhealthy(entry);
|
|
1669
1719
|
const [kind, setKind] = useState7("personal");
|
|
1670
1720
|
const [sharingOpen, setSharingOpen] = useState7(false);
|
|
1671
1721
|
const [setupOpen, setSetupOpen] = useState7(false);
|
|
@@ -1702,7 +1752,7 @@ function ConnectorTile({
|
|
|
1702
1752
|
] }),
|
|
1703
1753
|
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
1704
1754
|
/* @__PURE__ */ jsx7("span", { "data-testid": `connector-capability-${entry.slug}`, children: capabilityLabel(entry.capability) }),
|
|
1705
|
-
entry.category && /* @__PURE__ */ jsxs6(
|
|
1755
|
+
entry.category && /* @__PURE__ */ jsxs6(Fragment4, { children: [
|
|
1706
1756
|
/* @__PURE__ */ jsx7("span", { "aria-hidden": "true", children: "\xB7" }),
|
|
1707
1757
|
/* @__PURE__ */ jsx7("span", { className: "truncate", children: entry.category })
|
|
1708
1758
|
] })
|
|
@@ -1715,6 +1765,34 @@ function ConnectorTile({
|
|
|
1715
1765
|
children: entry.connection.since ? `Connected since ${formatSince(entry.connection.since)}` : "Connected"
|
|
1716
1766
|
}
|
|
1717
1767
|
),
|
|
1768
|
+
isConnected && !shared && entry.connection.account_label && /* @__PURE__ */ jsxs6(
|
|
1769
|
+
"p",
|
|
1770
|
+
{
|
|
1771
|
+
"data-testid": `connector-account-${entry.slug}`,
|
|
1772
|
+
className: "text-xs text-muted-foreground",
|
|
1773
|
+
children: [
|
|
1774
|
+
"Connected as ",
|
|
1775
|
+
entry.connection.account_label
|
|
1776
|
+
]
|
|
1777
|
+
}
|
|
1778
|
+
),
|
|
1779
|
+
isConnected && entry.access_summary && /* @__PURE__ */ jsx7(
|
|
1780
|
+
"p",
|
|
1781
|
+
{
|
|
1782
|
+
"data-testid": `connector-access-summary-${entry.slug}`,
|
|
1783
|
+
className: "text-xs text-muted-foreground",
|
|
1784
|
+
children: entry.access_summary
|
|
1785
|
+
}
|
|
1786
|
+
),
|
|
1787
|
+
unhealthy && /* @__PURE__ */ jsx7(
|
|
1788
|
+
"p",
|
|
1789
|
+
{
|
|
1790
|
+
role: "alert",
|
|
1791
|
+
"data-testid": `connector-unhealthy-${entry.slug}`,
|
|
1792
|
+
className: "text-xs font-medium text-destructive",
|
|
1793
|
+
children: DEFAULT_RECONNECT_MESSAGE
|
|
1794
|
+
}
|
|
1795
|
+
),
|
|
1718
1796
|
attribution && /* @__PURE__ */ jsx7(
|
|
1719
1797
|
"p",
|
|
1720
1798
|
{
|
|
@@ -1777,17 +1855,46 @@ function ConnectorTile({
|
|
|
1777
1855
|
errorMessage && /* @__PURE__ */ jsx7("p", { role: "alert", "data-testid": `connector-error-${entry.slug}`, className: "text-xs text-destructive", children: errorMessage })
|
|
1778
1856
|
] }),
|
|
1779
1857
|
isConnected ? /* @__PURE__ */ jsxs6("div", { className: "flex shrink-0 flex-col items-end gap-1", children: [
|
|
1858
|
+
unhealthy && !shared && /* @__PURE__ */ jsx7(
|
|
1859
|
+
"button",
|
|
1860
|
+
{
|
|
1861
|
+
type: "button",
|
|
1862
|
+
"data-testid": `connector-reconnect-${entry.slug}`,
|
|
1863
|
+
onClick: () => onConnect(entry, entry.connection.kind ?? "personal"),
|
|
1864
|
+
disabled: isConnecting || isDisconnecting,
|
|
1865
|
+
className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
|
|
1866
|
+
children: isConnecting ? "Reconnecting\u2026" : "Reconnect"
|
|
1867
|
+
}
|
|
1868
|
+
),
|
|
1780
1869
|
canDisconnect && /* @__PURE__ */ jsx7(
|
|
1781
1870
|
"button",
|
|
1782
1871
|
{
|
|
1783
1872
|
type: "button",
|
|
1784
1873
|
"data-testid": `connector-disconnect-${entry.slug}`,
|
|
1874
|
+
title: DISCONNECT_REVOKES_HINT,
|
|
1785
1875
|
onClick: () => onDisconnect(entry),
|
|
1786
|
-
disabled: isDisconnecting,
|
|
1876
|
+
disabled: isDisconnecting || isConnecting,
|
|
1787
1877
|
className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-destructive hover:bg-destructive/10 disabled:opacity-50",
|
|
1788
1878
|
children: isDisconnecting ? "Disconnecting\u2026" : "Disconnect"
|
|
1789
1879
|
}
|
|
1790
1880
|
),
|
|
1881
|
+
canDisconnect && // Outcome-eval finding (DEV-7873): with no width cap, this
|
|
1882
|
+
// paragraph's one long sentence forced the WHOLE shrink-0
|
|
1883
|
+
// button column to grow to its natural (unwrapped) width,
|
|
1884
|
+
// squeezing the sibling `min-w-0 flex-1` text column down to
|
|
1885
|
+
// one word per line at any realistic tile width. RTL's jsdom
|
|
1886
|
+
// tests never caught it — jsdom doesn't lay out real flexbox
|
|
1887
|
+
// sizing, so every textContent assertion still passed. A
|
|
1888
|
+
// narrow max-width forces the hint to wrap within the same
|
|
1889
|
+
// footprint as the button above it instead.
|
|
1890
|
+
/* @__PURE__ */ jsx7(
|
|
1891
|
+
"p",
|
|
1892
|
+
{
|
|
1893
|
+
"data-testid": `connector-disconnect-hint-${entry.slug}`,
|
|
1894
|
+
className: "max-w-28 text-right text-[11px] text-muted-foreground",
|
|
1895
|
+
children: DISCONNECT_REVOKES_HINT
|
|
1896
|
+
}
|
|
1897
|
+
),
|
|
1791
1898
|
canManageSharing && /* @__PURE__ */ jsx7(
|
|
1792
1899
|
"button",
|
|
1793
1900
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meetreeve/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|