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