@meetreeve/ui 0.7.1 → 0.8.1
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 +68 -1
- package/dist/index.d.ts +68 -1
- package/dist/index.js +292 -18
- package/dist/index.mjs +291 -18
- package/package.json +3 -2
package/dist/index.d.mts
CHANGED
|
@@ -584,4 +584,71 @@ interface ContextSwitcherProps {
|
|
|
584
584
|
}
|
|
585
585
|
declare function ContextSwitcher({ items, activeRefId, onSelect, footer, placeholder, collapsed, side, align, contentClassName, }: ContextSwitcherProps): react.JSX.Element;
|
|
586
586
|
|
|
587
|
-
|
|
587
|
+
/**
|
|
588
|
+
* ConnectorPicker (DEV-5578 / CONN-3) — the one data-driven connect
|
|
589
|
+
* component in @meetreeve/ui. Renders a catalog of connectable entries from
|
|
590
|
+
* an injected client, owning the whole per-entry state machine
|
|
591
|
+
* (not-connected -> connecting -> connected, disconnect, inline per-tile
|
|
592
|
+
* errors, and a return-from-OAuth refresh). Every label is derived from the
|
|
593
|
+
* entry data the client returns — this file must never hardcode a specific
|
|
594
|
+
* provider's name (an explicit acceptance criterion); a host app supplies
|
|
595
|
+
* that entirely through `ConnectableEntry.name` / `.logo` / `.category`.
|
|
596
|
+
*
|
|
597
|
+
* Transport-injection precedent: `linkComponent` (DEV-4429/DEV-4391) —
|
|
598
|
+
* inject the transport, keep behavior in the package. Here the "transport"
|
|
599
|
+
* is the whole backend round-trip (list/connect/disconnect), injected as
|
|
600
|
+
* `client` (Matt's decision, 2026-07-25): Reeve passes one built on its own
|
|
601
|
+
* fetch client; a tenant clone passes ~10 lines of its own.
|
|
602
|
+
*/
|
|
603
|
+
type ConnectableTransport = "composio" | "native";
|
|
604
|
+
type ConnectableCapability = "read" | "write" | "readwrite";
|
|
605
|
+
interface ConnectableEntry {
|
|
606
|
+
slug: string;
|
|
607
|
+
name: string;
|
|
608
|
+
logo: string | null;
|
|
609
|
+
category: string | null;
|
|
610
|
+
transport: ConnectableTransport;
|
|
611
|
+
capability: ConnectableCapability;
|
|
612
|
+
purposes: string[];
|
|
613
|
+
connection: {
|
|
614
|
+
status: "connected" | "not_connected";
|
|
615
|
+
connected_account_id: string | null;
|
|
616
|
+
since: string | null;
|
|
617
|
+
};
|
|
618
|
+
/** true = tenant has a live connection but the entry left the allowlist. */
|
|
619
|
+
deprecated: boolean;
|
|
620
|
+
}
|
|
621
|
+
interface ConnectorPickerClient {
|
|
622
|
+
listConnectable(opts: {
|
|
623
|
+
purpose?: string;
|
|
624
|
+
}): Promise<ConnectableEntry[]>;
|
|
625
|
+
/** composio -> open the returned connect_url; native may resolve with none. */
|
|
626
|
+
connect(entry: ConnectableEntry): Promise<{
|
|
627
|
+
connect_url?: string;
|
|
628
|
+
}>;
|
|
629
|
+
disconnect(entry: ConnectableEntry): Promise<void>;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Structurally identical to the app's own `DnaLens` (src/lib/dna-lens.ts) —
|
|
633
|
+
* this package must never import app code, so it declares its own copy
|
|
634
|
+
* rather than sharing the type.
|
|
635
|
+
*/
|
|
636
|
+
type ConnectorPickerLens = "visitor" | "member" | "owner";
|
|
637
|
+
interface ConnectorPickerProps {
|
|
638
|
+
/** The injected transport adapter — required. */
|
|
639
|
+
client: ConnectorPickerClient;
|
|
640
|
+
/** Optional purpose slice, forwarded to `client.listConnectable`. */
|
|
641
|
+
purpose?: string;
|
|
642
|
+
/**
|
|
643
|
+
* Default "owner". "visitor"/"member" render nothing and make ZERO
|
|
644
|
+
* client calls — some host surfaces (e.g. a structurally owner-only
|
|
645
|
+
* knowledge page) have no visitor/member concept at all, and forcing them
|
|
646
|
+
* to synthesize a lens would import dead branches.
|
|
647
|
+
*/
|
|
648
|
+
lens?: ConnectorPickerLens;
|
|
649
|
+
layout?: "tiles" | "list";
|
|
650
|
+
onConnected?: (entry: ConnectableEntry) => void;
|
|
651
|
+
}
|
|
652
|
+
declare function ConnectorPicker({ client, purpose, lens, layout, onConnected, }: ConnectorPickerProps): react.JSX.Element | null;
|
|
653
|
+
|
|
654
|
+
export { type AppRungItem, AppSidebar, type AppSidebarBrand, type AppSidebarModuleDef, type AppSidebarProps, type AppSidebarTopItem, type Brand, BrandGlyph, type BrandGlyphProps, type ConnectableCapability, type ConnectableEntry, type ConnectableTransport, ConnectorPicker, type ConnectorPickerClient, type ConnectorPickerLens, type ConnectorPickerProps, 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, 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
|
@@ -584,4 +584,71 @@ interface ContextSwitcherProps {
|
|
|
584
584
|
}
|
|
585
585
|
declare function ContextSwitcher({ items, activeRefId, onSelect, footer, placeholder, collapsed, side, align, contentClassName, }: ContextSwitcherProps): react.JSX.Element;
|
|
586
586
|
|
|
587
|
-
|
|
587
|
+
/**
|
|
588
|
+
* ConnectorPicker (DEV-5578 / CONN-3) — the one data-driven connect
|
|
589
|
+
* component in @meetreeve/ui. Renders a catalog of connectable entries from
|
|
590
|
+
* an injected client, owning the whole per-entry state machine
|
|
591
|
+
* (not-connected -> connecting -> connected, disconnect, inline per-tile
|
|
592
|
+
* errors, and a return-from-OAuth refresh). Every label is derived from the
|
|
593
|
+
* entry data the client returns — this file must never hardcode a specific
|
|
594
|
+
* provider's name (an explicit acceptance criterion); a host app supplies
|
|
595
|
+
* that entirely through `ConnectableEntry.name` / `.logo` / `.category`.
|
|
596
|
+
*
|
|
597
|
+
* Transport-injection precedent: `linkComponent` (DEV-4429/DEV-4391) —
|
|
598
|
+
* inject the transport, keep behavior in the package. Here the "transport"
|
|
599
|
+
* is the whole backend round-trip (list/connect/disconnect), injected as
|
|
600
|
+
* `client` (Matt's decision, 2026-07-25): Reeve passes one built on its own
|
|
601
|
+
* fetch client; a tenant clone passes ~10 lines of its own.
|
|
602
|
+
*/
|
|
603
|
+
type ConnectableTransport = "composio" | "native";
|
|
604
|
+
type ConnectableCapability = "read" | "write" | "readwrite";
|
|
605
|
+
interface ConnectableEntry {
|
|
606
|
+
slug: string;
|
|
607
|
+
name: string;
|
|
608
|
+
logo: string | null;
|
|
609
|
+
category: string | null;
|
|
610
|
+
transport: ConnectableTransport;
|
|
611
|
+
capability: ConnectableCapability;
|
|
612
|
+
purposes: string[];
|
|
613
|
+
connection: {
|
|
614
|
+
status: "connected" | "not_connected";
|
|
615
|
+
connected_account_id: string | null;
|
|
616
|
+
since: string | null;
|
|
617
|
+
};
|
|
618
|
+
/** true = tenant has a live connection but the entry left the allowlist. */
|
|
619
|
+
deprecated: boolean;
|
|
620
|
+
}
|
|
621
|
+
interface ConnectorPickerClient {
|
|
622
|
+
listConnectable(opts: {
|
|
623
|
+
purpose?: string;
|
|
624
|
+
}): Promise<ConnectableEntry[]>;
|
|
625
|
+
/** composio -> open the returned connect_url; native may resolve with none. */
|
|
626
|
+
connect(entry: ConnectableEntry): Promise<{
|
|
627
|
+
connect_url?: string;
|
|
628
|
+
}>;
|
|
629
|
+
disconnect(entry: ConnectableEntry): Promise<void>;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Structurally identical to the app's own `DnaLens` (src/lib/dna-lens.ts) —
|
|
633
|
+
* this package must never import app code, so it declares its own copy
|
|
634
|
+
* rather than sharing the type.
|
|
635
|
+
*/
|
|
636
|
+
type ConnectorPickerLens = "visitor" | "member" | "owner";
|
|
637
|
+
interface ConnectorPickerProps {
|
|
638
|
+
/** The injected transport adapter — required. */
|
|
639
|
+
client: ConnectorPickerClient;
|
|
640
|
+
/** Optional purpose slice, forwarded to `client.listConnectable`. */
|
|
641
|
+
purpose?: string;
|
|
642
|
+
/**
|
|
643
|
+
* Default "owner". "visitor"/"member" render nothing and make ZERO
|
|
644
|
+
* client calls — some host surfaces (e.g. a structurally owner-only
|
|
645
|
+
* knowledge page) have no visitor/member concept at all, and forcing them
|
|
646
|
+
* to synthesize a lens would import dead branches.
|
|
647
|
+
*/
|
|
648
|
+
lens?: ConnectorPickerLens;
|
|
649
|
+
layout?: "tiles" | "list";
|
|
650
|
+
onConnected?: (entry: ConnectableEntry) => void;
|
|
651
|
+
}
|
|
652
|
+
declare function ConnectorPicker({ client, purpose, lens, layout, onConnected, }: ConnectorPickerProps): react.JSX.Element | null;
|
|
653
|
+
|
|
654
|
+
export { type AppRungItem, AppSidebar, type AppSidebarBrand, type AppSidebarModuleDef, type AppSidebarProps, type AppSidebarTopItem, type Brand, BrandGlyph, type BrandGlyphProps, type ConnectableCapability, type ConnectableEntry, type ConnectableTransport, ConnectorPicker, type ConnectorPickerClient, type ConnectorPickerLens, type ConnectorPickerProps, 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, 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
|
@@ -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,
|
|
@@ -838,13 +839,23 @@ function Flyout({
|
|
|
838
839
|
className: cn(
|
|
839
840
|
"group flex w-full items-center gap-2.5 rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
840
841
|
collapsed && "justify-center px-0",
|
|
841
|
-
|
|
842
|
+
// DEV-6586: this trigger renders as a row inside AppSidebar's own
|
|
843
|
+
// `<nav>` (a `bg-sidebar` surface that stays dark navy in both
|
|
844
|
+
// themes) — same fix as NavLink, same measured ratios (default
|
|
845
|
+
// 8.05:1, hover 13.65:1, active 10.95:1). The flyout PANEL below
|
|
846
|
+
// is intentionally left on document tokens: it renders via
|
|
847
|
+
// `position: fixed`, floating over the main content area on
|
|
848
|
+
// `bg-background`, not over the sidebar.
|
|
849
|
+
active ? "bg-sidebar-accent text-sidebar-accent-foreground" : "text-sidebar-foreground/70 hover:bg-sidebar-foreground/5 hover:text-sidebar-foreground"
|
|
842
850
|
),
|
|
843
851
|
children: [
|
|
844
852
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
845
853
|
Icon,
|
|
846
854
|
{
|
|
847
|
-
className: cn(
|
|
855
|
+
className: cn(
|
|
856
|
+
"size-4 shrink-0",
|
|
857
|
+
active ? "text-sidebar-accent-foreground" : "text-sidebar-foreground/70"
|
|
858
|
+
),
|
|
848
859
|
strokeWidth: 1.5,
|
|
849
860
|
"aria-hidden": "true"
|
|
850
861
|
}
|
|
@@ -927,13 +938,23 @@ function NavLink({
|
|
|
927
938
|
className: cn(
|
|
928
939
|
"group flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
929
940
|
collapsed && "justify-center px-0",
|
|
930
|
-
|
|
941
|
+
// DEV-6586: the sidebar is a permanently dark-navy surface in BOTH
|
|
942
|
+
// app themes (see `--sidebar-*` in globals.css) — styling from
|
|
943
|
+
// document-theme tokens (text-foreground/text-muted-foreground) put
|
|
944
|
+
// dark-on-dark text here in light mode. `--sidebar-foreground` /
|
|
945
|
+
// `--sidebar-accent(-foreground)` are theme-invariant, so hover/
|
|
946
|
+
// active reliably INCREASE legibility instead of erasing it:
|
|
947
|
+
// default 8.05:1, hover 13.65:1, active 10.95:1 (all >= 4.5:1 AA).
|
|
948
|
+
active ? "bg-sidebar-accent text-sidebar-accent-foreground" : "text-sidebar-foreground/70 hover:bg-sidebar-foreground/5 hover:text-sidebar-foreground"
|
|
931
949
|
),
|
|
932
950
|
children: [
|
|
933
951
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
934
952
|
Icon,
|
|
935
953
|
{
|
|
936
|
-
className: cn(
|
|
954
|
+
className: cn(
|
|
955
|
+
"size-4 shrink-0",
|
|
956
|
+
active ? "text-sidebar-accent-foreground" : "text-sidebar-foreground/70"
|
|
957
|
+
),
|
|
937
958
|
strokeWidth: 1.5,
|
|
938
959
|
"aria-hidden": "true"
|
|
939
960
|
}
|
|
@@ -1058,11 +1079,11 @@ function AppSidebar({
|
|
|
1058
1079
|
label: item.label,
|
|
1059
1080
|
icon: item.icon,
|
|
1060
1081
|
active,
|
|
1061
|
-
children,
|
|
1062
1082
|
linkComponent,
|
|
1063
1083
|
collapsed: collapsedRow,
|
|
1064
1084
|
forceClosed: openFlyoutId !== null && openFlyoutId !== item.id,
|
|
1065
|
-
onOpenChange: (isOpen) => handleFlyoutOpenChange(item.id, isOpen)
|
|
1085
|
+
onOpenChange: (isOpen) => handleFlyoutOpenChange(item.id, isOpen),
|
|
1086
|
+
children
|
|
1066
1087
|
},
|
|
1067
1088
|
item.id
|
|
1068
1089
|
);
|
|
@@ -1076,7 +1097,7 @@ function AppSidebar({
|
|
|
1076
1097
|
href: brand.href,
|
|
1077
1098
|
"data-testid": "app-sidebar-brand",
|
|
1078
1099
|
title: brand.label,
|
|
1079
|
-
className: "flex items-center justify-center border-b border-border px-1 py-4",
|
|
1100
|
+
className: "flex items-center justify-center border-b border-sidebar-border px-1 py-4",
|
|
1080
1101
|
children: [
|
|
1081
1102
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "sr-only", children: brand.label }),
|
|
1082
1103
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BrandGlyph, { brand: { name: brand.label }, size: "xl" })
|
|
@@ -1089,9 +1110,9 @@ function AppSidebar({
|
|
|
1089
1110
|
{
|
|
1090
1111
|
href: brand.href,
|
|
1091
1112
|
"data-testid": "app-sidebar-brand",
|
|
1092
|
-
className: "flex items-center gap-2.5 border-b border-border px-5 py-4",
|
|
1113
|
+
className: "flex items-center gap-2.5 border-b border-sidebar-border px-5 py-4 text-sidebar-foreground",
|
|
1093
1114
|
children: [
|
|
1094
|
-
/* @__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() }) }),
|
|
1115
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "grid size-7 place-items-center rounded-md bg-sidebar-primary text-sidebar-primary-foreground", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "font-mono text-sm font-bold", children: (brand.label[0] ?? "r").toUpperCase() }) }),
|
|
1095
1116
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "truncate text-sm font-semibold tracking-tight", children: brand.label })
|
|
1096
1117
|
]
|
|
1097
1118
|
}
|
|
@@ -1147,7 +1168,7 @@ function AppSidebar({
|
|
|
1147
1168
|
"aria-modal": "true",
|
|
1148
1169
|
"aria-label": "Navigation",
|
|
1149
1170
|
"data-testid": "app-sidebar-mobile-drawer",
|
|
1150
|
-
className: "absolute inset-y-0 left-0 flex w-72 flex-col overflow-y-auto border-r border-border bg-sidebar",
|
|
1171
|
+
className: "absolute inset-y-0 left-0 flex w-72 flex-col overflow-y-auto border-r border-sidebar-border bg-sidebar",
|
|
1151
1172
|
children: [
|
|
1152
1173
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "sticky top-0 z-10 flex justify-end bg-sidebar p-2", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1153
1174
|
"button",
|
|
@@ -1158,7 +1179,7 @@ function AppSidebar({
|
|
|
1158
1179
|
"aria-label": `Close ${menuLabel.toLowerCase()}`,
|
|
1159
1180
|
onClick: () => setMobileOpen(false),
|
|
1160
1181
|
style: TOUCH_TARGET_STYLE,
|
|
1161
|
-
className: "flex items-center justify-center rounded-md p-2 text-foreground hover:bg-foreground/5",
|
|
1182
|
+
className: "flex items-center justify-center rounded-md p-2 text-sidebar-foreground/70 hover:bg-sidebar-foreground/5 hover:text-sidebar-foreground",
|
|
1162
1183
|
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react2.X, { className: "h-5 w-5", "aria-hidden": "true" })
|
|
1163
1184
|
}
|
|
1164
1185
|
) }),
|
|
@@ -1176,7 +1197,7 @@ function AppSidebar({
|
|
|
1176
1197
|
{
|
|
1177
1198
|
"data-testid": "app-sidebar-desktop",
|
|
1178
1199
|
className: cn(
|
|
1179
|
-
"hidden shrink-0 flex-col border-r border-border bg-sidebar md:flex",
|
|
1200
|
+
"hidden shrink-0 flex-col border-r border-sidebar-border bg-sidebar md:flex",
|
|
1180
1201
|
collapsed ? COLLAPSED_WIDTH_CLASS : EXPANDED_WIDTH_CLASS,
|
|
1181
1202
|
// DEV-5331: a plain CSS width transition is enough — no new deps
|
|
1182
1203
|
// for a spring/JS animation. Respect prefers-reduced-motion.
|
|
@@ -1197,7 +1218,7 @@ function AppSidebar({
|
|
|
1197
1218
|
) })
|
|
1198
1219
|
}
|
|
1199
1220
|
),
|
|
1200
|
-
collapsible && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "border-t border-border px-2 py-2", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
1221
|
+
collapsible && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "border-t border-sidebar-border px-2 py-2", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
1201
1222
|
"button",
|
|
1202
1223
|
{
|
|
1203
1224
|
type: "button",
|
|
@@ -1207,7 +1228,7 @@ function AppSidebar({
|
|
|
1207
1228
|
title: collapsed ? "Expand sidebar" : "Collapse sidebar",
|
|
1208
1229
|
onClick: handleToggleCollapsed,
|
|
1209
1230
|
className: cn(
|
|
1210
|
-
"flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm text-
|
|
1231
|
+
"flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm text-sidebar-foreground/70 transition-colors hover:bg-sidebar-foreground/5 hover:text-sidebar-foreground",
|
|
1211
1232
|
collapsed && "justify-center px-0"
|
|
1212
1233
|
),
|
|
1213
1234
|
children: [
|
|
@@ -1345,18 +1366,18 @@ function ContextSwitcher({
|
|
|
1345
1366
|
type: "button",
|
|
1346
1367
|
title: active?.name || placeholder,
|
|
1347
1368
|
"aria-label": active?.name || placeholder,
|
|
1348
|
-
className: "flex h-10 w-10 items-center justify-center rounded-lg text-
|
|
1369
|
+
className: "flex h-10 w-10 items-center justify-center rounded-lg text-sidebar-foreground/70 transition-colors hover:bg-sidebar-foreground/5 hover:text-sidebar-foreground",
|
|
1349
1370
|
children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BrandGlyph, { brand: toBrand(active ?? { name: "" }), size: "lg" })
|
|
1350
1371
|
}
|
|
1351
1372
|
) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1352
1373
|
"button",
|
|
1353
1374
|
{
|
|
1354
1375
|
type: "button",
|
|
1355
|
-
className: "group flex items-center gap-2 rounded-md border border-border bg-
|
|
1376
|
+
className: "group flex items-center gap-2 rounded-md border border-sidebar-border bg-sidebar-accent/60 px-2.5 py-1.5 text-sm font-medium transition-colors hover:bg-sidebar",
|
|
1356
1377
|
children: [
|
|
1357
1378
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BrandGlyph, { brand: toBrand(active ?? { name: "" }) }),
|
|
1358
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "max-w-[140px] truncate text-foreground
|
|
1359
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.ChevronDown, { className: "h-3.5 w-3.5 text-
|
|
1379
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "max-w-[140px] truncate text-sidebar-accent-foreground", children: active?.name || placeholder }),
|
|
1380
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.ChevronDown, { className: "h-3.5 w-3.5 text-sidebar-foreground/70 transition-transform group-data-[state=open]:rotate-180" })
|
|
1360
1381
|
]
|
|
1361
1382
|
}
|
|
1362
1383
|
) }),
|
|
@@ -1395,10 +1416,263 @@ function ContextSwitcher({
|
|
|
1395
1416
|
)
|
|
1396
1417
|
] });
|
|
1397
1418
|
}
|
|
1419
|
+
|
|
1420
|
+
// src/connector-picker.tsx
|
|
1421
|
+
var import_react9 = require("react");
|
|
1422
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
1423
|
+
var DEFAULT_CONNECT_ERROR = "Couldn't start the connection. Please try again.";
|
|
1424
|
+
var DEFAULT_DISCONNECT_ERROR = "Couldn't disconnect. Please try again.";
|
|
1425
|
+
var DEFAULT_LOAD_ERROR = "Couldn't load connections. Please try again.";
|
|
1426
|
+
var SKELETON_COUNT = 3;
|
|
1427
|
+
function capabilityLabel(capability) {
|
|
1428
|
+
if (capability === "read") return "Read only";
|
|
1429
|
+
if (capability === "write") return "Write only";
|
|
1430
|
+
return "Read & write";
|
|
1431
|
+
}
|
|
1432
|
+
function formatSince(since) {
|
|
1433
|
+
const parsed = new Date(since);
|
|
1434
|
+
if (Number.isNaN(parsed.getTime())) return since;
|
|
1435
|
+
return parsed.toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
|
|
1436
|
+
}
|
|
1437
|
+
function errorMessageFrom(err, fallback) {
|
|
1438
|
+
return err instanceof Error && err.message ? err.message : fallback;
|
|
1439
|
+
}
|
|
1440
|
+
function ConnectorPicker({
|
|
1441
|
+
client,
|
|
1442
|
+
purpose,
|
|
1443
|
+
lens = "owner",
|
|
1444
|
+
layout = "tiles",
|
|
1445
|
+
onConnected
|
|
1446
|
+
}) {
|
|
1447
|
+
const gated = lens !== "owner";
|
|
1448
|
+
const [entries, setEntries] = (0, import_react9.useState)(null);
|
|
1449
|
+
const [loadError, setLoadError] = (0, import_react9.useState)(null);
|
|
1450
|
+
const [uiState, setUiState] = (0, import_react9.useState)({});
|
|
1451
|
+
const clientRef = (0, import_react9.useRef)(client);
|
|
1452
|
+
clientRef.current = client;
|
|
1453
|
+
const purposeRef = (0, import_react9.useRef)(purpose);
|
|
1454
|
+
purposeRef.current = purpose;
|
|
1455
|
+
const onConnectedRef = (0, import_react9.useRef)(onConnected);
|
|
1456
|
+
onConnectedRef.current = onConnected;
|
|
1457
|
+
const uiStateRef = (0, import_react9.useRef)(uiState);
|
|
1458
|
+
uiStateRef.current = uiState;
|
|
1459
|
+
const load = (0, import_react9.useCallback)(async () => {
|
|
1460
|
+
if (gated) return null;
|
|
1461
|
+
try {
|
|
1462
|
+
const list = await clientRef.current.listConnectable({ purpose: purposeRef.current });
|
|
1463
|
+
setEntries(list);
|
|
1464
|
+
setLoadError(null);
|
|
1465
|
+
return list;
|
|
1466
|
+
} catch (err) {
|
|
1467
|
+
setLoadError(errorMessageFrom(err, DEFAULT_LOAD_ERROR));
|
|
1468
|
+
return null;
|
|
1469
|
+
}
|
|
1470
|
+
}, [gated]);
|
|
1471
|
+
(0, import_react9.useEffect)(() => {
|
|
1472
|
+
if (gated) return;
|
|
1473
|
+
void load();
|
|
1474
|
+
}, [gated, load, purpose]);
|
|
1475
|
+
const refreshAfterReturn = (0, import_react9.useCallback)(async () => {
|
|
1476
|
+
const list = await load();
|
|
1477
|
+
if (!list) return;
|
|
1478
|
+
setUiState((prev) => {
|
|
1479
|
+
let changed = false;
|
|
1480
|
+
const next = { ...prev };
|
|
1481
|
+
for (const entry of list) {
|
|
1482
|
+
if (next[entry.slug]?.kind === "connecting" && entry.connection.status === "connected") {
|
|
1483
|
+
changed = true;
|
|
1484
|
+
delete next[entry.slug];
|
|
1485
|
+
onConnectedRef.current?.(entry);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
return changed ? next : prev;
|
|
1489
|
+
});
|
|
1490
|
+
}, [load]);
|
|
1491
|
+
(0, import_react9.useEffect)(() => {
|
|
1492
|
+
if (gated) return;
|
|
1493
|
+
function handleReturn() {
|
|
1494
|
+
if (document.visibilityState === "hidden") return;
|
|
1495
|
+
const hasPendingConnect = Object.values(uiStateRef.current).some(
|
|
1496
|
+
(s) => s.kind === "connecting"
|
|
1497
|
+
);
|
|
1498
|
+
if (!hasPendingConnect) return;
|
|
1499
|
+
void refreshAfterReturn();
|
|
1500
|
+
}
|
|
1501
|
+
window.addEventListener("focus", handleReturn);
|
|
1502
|
+
document.addEventListener("visibilitychange", handleReturn);
|
|
1503
|
+
return () => {
|
|
1504
|
+
window.removeEventListener("focus", handleReturn);
|
|
1505
|
+
document.removeEventListener("visibilitychange", handleReturn);
|
|
1506
|
+
};
|
|
1507
|
+
}, [gated, refreshAfterReturn]);
|
|
1508
|
+
const handleConnect = (0, import_react9.useCallback)(
|
|
1509
|
+
async (entry) => {
|
|
1510
|
+
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "connecting" } }));
|
|
1511
|
+
try {
|
|
1512
|
+
const result = await clientRef.current.connect(entry);
|
|
1513
|
+
if (result?.connect_url) {
|
|
1514
|
+
window.open(result.connect_url, "_blank", "noopener,noreferrer");
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
const list = await load();
|
|
1518
|
+
const updated = list?.find((e) => e.slug === entry.slug) ?? null;
|
|
1519
|
+
setUiState((prev) => {
|
|
1520
|
+
const next = { ...prev };
|
|
1521
|
+
delete next[entry.slug];
|
|
1522
|
+
return next;
|
|
1523
|
+
});
|
|
1524
|
+
if (updated?.connection.status === "connected") {
|
|
1525
|
+
onConnectedRef.current?.(updated);
|
|
1526
|
+
}
|
|
1527
|
+
} catch (err) {
|
|
1528
|
+
const message = errorMessageFrom(err, DEFAULT_CONNECT_ERROR);
|
|
1529
|
+
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "error", message } }));
|
|
1530
|
+
}
|
|
1531
|
+
},
|
|
1532
|
+
[load]
|
|
1533
|
+
);
|
|
1534
|
+
const handleDisconnect = (0, import_react9.useCallback)(
|
|
1535
|
+
async (entry) => {
|
|
1536
|
+
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "disconnecting" } }));
|
|
1537
|
+
try {
|
|
1538
|
+
await clientRef.current.disconnect(entry);
|
|
1539
|
+
await load();
|
|
1540
|
+
setUiState((prev) => {
|
|
1541
|
+
const next = { ...prev };
|
|
1542
|
+
delete next[entry.slug];
|
|
1543
|
+
return next;
|
|
1544
|
+
});
|
|
1545
|
+
} catch (err) {
|
|
1546
|
+
const message = errorMessageFrom(err, DEFAULT_DISCONNECT_ERROR);
|
|
1547
|
+
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "error", message } }));
|
|
1548
|
+
}
|
|
1549
|
+
},
|
|
1550
|
+
[load]
|
|
1551
|
+
);
|
|
1552
|
+
if (gated) return null;
|
|
1553
|
+
const containerClassName = cn(
|
|
1554
|
+
"flex flex-col",
|
|
1555
|
+
layout === "tiles" ? "gap-3 sm:grid sm:grid-cols-2 lg:grid-cols-3" : "divide-y divide-border"
|
|
1556
|
+
);
|
|
1557
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { "data-testid": "connector-picker", "data-layout": layout, className: containerClassName, children: [
|
|
1558
|
+
entries === null && !loadError && Array.from({ length: SKELETON_COUNT }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
1559
|
+
"div",
|
|
1560
|
+
{
|
|
1561
|
+
"data-testid": "connector-picker-skeleton-item",
|
|
1562
|
+
"aria-hidden": "true",
|
|
1563
|
+
className: "flex items-center gap-3 rounded-lg border border-border p-3",
|
|
1564
|
+
children: [
|
|
1565
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "h-8 w-8 shrink-0 animate-pulse rounded bg-foreground/10" }),
|
|
1566
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "h-3 flex-1 animate-pulse rounded bg-foreground/10" })
|
|
1567
|
+
]
|
|
1568
|
+
},
|
|
1569
|
+
i
|
|
1570
|
+
)),
|
|
1571
|
+
loadError && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
1572
|
+
"div",
|
|
1573
|
+
{
|
|
1574
|
+
role: "alert",
|
|
1575
|
+
"data-testid": "connector-picker-load-error",
|
|
1576
|
+
className: "flex items-center justify-between gap-3 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive",
|
|
1577
|
+
children: [
|
|
1578
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: loadError }),
|
|
1579
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", onClick: () => void load(), className: "shrink-0 underline", children: "Retry" })
|
|
1580
|
+
]
|
|
1581
|
+
}
|
|
1582
|
+
),
|
|
1583
|
+
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." }),
|
|
1584
|
+
entries?.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1585
|
+
ConnectorTile,
|
|
1586
|
+
{
|
|
1587
|
+
entry,
|
|
1588
|
+
ui: uiState[entry.slug],
|
|
1589
|
+
layout,
|
|
1590
|
+
onConnect: handleConnect,
|
|
1591
|
+
onDisconnect: handleDisconnect
|
|
1592
|
+
},
|
|
1593
|
+
entry.slug
|
|
1594
|
+
))
|
|
1595
|
+
] });
|
|
1596
|
+
}
|
|
1597
|
+
function ConnectorTile({
|
|
1598
|
+
entry,
|
|
1599
|
+
ui,
|
|
1600
|
+
layout,
|
|
1601
|
+
onConnect,
|
|
1602
|
+
onDisconnect
|
|
1603
|
+
}) {
|
|
1604
|
+
const isConnected = entry.connection.status === "connected";
|
|
1605
|
+
const isConnecting = ui?.kind === "connecting";
|
|
1606
|
+
const isDisconnecting = ui?.kind === "disconnecting";
|
|
1607
|
+
const errorMessage = ui?.kind === "error" ? ui.message : null;
|
|
1608
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
1609
|
+
"div",
|
|
1610
|
+
{
|
|
1611
|
+
"data-testid": `connector-tile-${entry.slug}`,
|
|
1612
|
+
"data-connected": isConnected || void 0,
|
|
1613
|
+
"data-deprecated": entry.deprecated || void 0,
|
|
1614
|
+
className: cn("flex items-center gap-3 p-3", layout === "tiles" && "rounded-lg border border-border"),
|
|
1615
|
+
children: [
|
|
1616
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(BrandGlyph, { brand: { name: entry.name, logo: entry.logo }, size: "lg" }),
|
|
1617
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
1618
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
1619
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "truncate text-sm font-medium", children: entry.name }),
|
|
1620
|
+
entry.deprecated && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1621
|
+
"span",
|
|
1622
|
+
{
|
|
1623
|
+
"data-testid": `connector-deprecated-${entry.slug}`,
|
|
1624
|
+
className: "shrink-0 rounded-full bg-foreground/10 px-2 py-0.5 text-[11px] text-muted-foreground",
|
|
1625
|
+
children: "No longer available"
|
|
1626
|
+
}
|
|
1627
|
+
)
|
|
1628
|
+
] }),
|
|
1629
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
1630
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { "data-testid": `connector-capability-${entry.slug}`, children: capabilityLabel(entry.capability) }),
|
|
1631
|
+
entry.category && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
|
|
1632
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { "aria-hidden": "true", children: "\xB7" }),
|
|
1633
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "truncate", children: entry.category })
|
|
1634
|
+
] })
|
|
1635
|
+
] }),
|
|
1636
|
+
isConnected && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1637
|
+
"p",
|
|
1638
|
+
{
|
|
1639
|
+
"data-testid": `connector-connected-since-${entry.slug}`,
|
|
1640
|
+
className: "text-xs text-muted-foreground",
|
|
1641
|
+
children: entry.connection.since ? `Connected since ${formatSince(entry.connection.since)}` : "Connected"
|
|
1642
|
+
}
|
|
1643
|
+
),
|
|
1644
|
+
errorMessage && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { role: "alert", "data-testid": `connector-error-${entry.slug}`, className: "text-xs text-destructive", children: errorMessage })
|
|
1645
|
+
] }),
|
|
1646
|
+
isConnected ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1647
|
+
"button",
|
|
1648
|
+
{
|
|
1649
|
+
type: "button",
|
|
1650
|
+
"data-testid": `connector-disconnect-${entry.slug}`,
|
|
1651
|
+
onClick: () => onDisconnect(entry),
|
|
1652
|
+
disabled: isDisconnecting,
|
|
1653
|
+
className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-destructive hover:bg-destructive/10 disabled:opacity-50",
|
|
1654
|
+
children: isDisconnecting ? "Disconnecting\u2026" : "Disconnect"
|
|
1655
|
+
}
|
|
1656
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1657
|
+
"button",
|
|
1658
|
+
{
|
|
1659
|
+
type: "button",
|
|
1660
|
+
"data-testid": `connector-connect-${entry.slug}`,
|
|
1661
|
+
onClick: () => onConnect(entry),
|
|
1662
|
+
disabled: isConnecting,
|
|
1663
|
+
className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
|
|
1664
|
+
children: isConnecting ? "Connecting\u2026" : "Connect"
|
|
1665
|
+
}
|
|
1666
|
+
)
|
|
1667
|
+
]
|
|
1668
|
+
}
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1398
1671
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1399
1672
|
0 && (module.exports = {
|
|
1400
1673
|
AppSidebar,
|
|
1401
1674
|
BrandGlyph,
|
|
1675
|
+
ConnectorPicker,
|
|
1402
1676
|
ContextSwitcher,
|
|
1403
1677
|
Flyout,
|
|
1404
1678
|
GLYPH_CANVAS_FILL,
|
package/dist/index.mjs
CHANGED
|
@@ -784,13 +784,23 @@ function Flyout({
|
|
|
784
784
|
className: cn(
|
|
785
785
|
"group flex w-full items-center gap-2.5 rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
786
786
|
collapsed && "justify-center px-0",
|
|
787
|
-
|
|
787
|
+
// DEV-6586: this trigger renders as a row inside AppSidebar's own
|
|
788
|
+
// `<nav>` (a `bg-sidebar` surface that stays dark navy in both
|
|
789
|
+
// themes) — same fix as NavLink, same measured ratios (default
|
|
790
|
+
// 8.05:1, hover 13.65:1, active 10.95:1). The flyout PANEL below
|
|
791
|
+
// is intentionally left on document tokens: it renders via
|
|
792
|
+
// `position: fixed`, floating over the main content area on
|
|
793
|
+
// `bg-background`, not over the sidebar.
|
|
794
|
+
active ? "bg-sidebar-accent text-sidebar-accent-foreground" : "text-sidebar-foreground/70 hover:bg-sidebar-foreground/5 hover:text-sidebar-foreground"
|
|
788
795
|
),
|
|
789
796
|
children: [
|
|
790
797
|
/* @__PURE__ */ jsx3(
|
|
791
798
|
Icon,
|
|
792
799
|
{
|
|
793
|
-
className: cn(
|
|
800
|
+
className: cn(
|
|
801
|
+
"size-4 shrink-0",
|
|
802
|
+
active ? "text-sidebar-accent-foreground" : "text-sidebar-foreground/70"
|
|
803
|
+
),
|
|
794
804
|
strokeWidth: 1.5,
|
|
795
805
|
"aria-hidden": "true"
|
|
796
806
|
}
|
|
@@ -873,13 +883,23 @@ function NavLink({
|
|
|
873
883
|
className: cn(
|
|
874
884
|
"group flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
875
885
|
collapsed && "justify-center px-0",
|
|
876
|
-
|
|
886
|
+
// DEV-6586: the sidebar is a permanently dark-navy surface in BOTH
|
|
887
|
+
// app themes (see `--sidebar-*` in globals.css) — styling from
|
|
888
|
+
// document-theme tokens (text-foreground/text-muted-foreground) put
|
|
889
|
+
// dark-on-dark text here in light mode. `--sidebar-foreground` /
|
|
890
|
+
// `--sidebar-accent(-foreground)` are theme-invariant, so hover/
|
|
891
|
+
// active reliably INCREASE legibility instead of erasing it:
|
|
892
|
+
// default 8.05:1, hover 13.65:1, active 10.95:1 (all >= 4.5:1 AA).
|
|
893
|
+
active ? "bg-sidebar-accent text-sidebar-accent-foreground" : "text-sidebar-foreground/70 hover:bg-sidebar-foreground/5 hover:text-sidebar-foreground"
|
|
877
894
|
),
|
|
878
895
|
children: [
|
|
879
896
|
/* @__PURE__ */ jsx4(
|
|
880
897
|
Icon,
|
|
881
898
|
{
|
|
882
|
-
className: cn(
|
|
899
|
+
className: cn(
|
|
900
|
+
"size-4 shrink-0",
|
|
901
|
+
active ? "text-sidebar-accent-foreground" : "text-sidebar-foreground/70"
|
|
902
|
+
),
|
|
883
903
|
strokeWidth: 1.5,
|
|
884
904
|
"aria-hidden": "true"
|
|
885
905
|
}
|
|
@@ -1004,11 +1024,11 @@ function AppSidebar({
|
|
|
1004
1024
|
label: item.label,
|
|
1005
1025
|
icon: item.icon,
|
|
1006
1026
|
active,
|
|
1007
|
-
children,
|
|
1008
1027
|
linkComponent,
|
|
1009
1028
|
collapsed: collapsedRow,
|
|
1010
1029
|
forceClosed: openFlyoutId !== null && openFlyoutId !== item.id,
|
|
1011
|
-
onOpenChange: (isOpen) => handleFlyoutOpenChange(item.id, isOpen)
|
|
1030
|
+
onOpenChange: (isOpen) => handleFlyoutOpenChange(item.id, isOpen),
|
|
1031
|
+
children
|
|
1012
1032
|
},
|
|
1013
1033
|
item.id
|
|
1014
1034
|
);
|
|
@@ -1022,7 +1042,7 @@ function AppSidebar({
|
|
|
1022
1042
|
href: brand.href,
|
|
1023
1043
|
"data-testid": "app-sidebar-brand",
|
|
1024
1044
|
title: brand.label,
|
|
1025
|
-
className: "flex items-center justify-center border-b border-border px-1 py-4",
|
|
1045
|
+
className: "flex items-center justify-center border-b border-sidebar-border px-1 py-4",
|
|
1026
1046
|
children: [
|
|
1027
1047
|
/* @__PURE__ */ jsx4("span", { className: "sr-only", children: brand.label }),
|
|
1028
1048
|
/* @__PURE__ */ jsx4(BrandGlyph, { brand: { name: brand.label }, size: "xl" })
|
|
@@ -1035,9 +1055,9 @@ function AppSidebar({
|
|
|
1035
1055
|
{
|
|
1036
1056
|
href: brand.href,
|
|
1037
1057
|
"data-testid": "app-sidebar-brand",
|
|
1038
|
-
className: "flex items-center gap-2.5 border-b border-border px-5 py-4",
|
|
1058
|
+
className: "flex items-center gap-2.5 border-b border-sidebar-border px-5 py-4 text-sidebar-foreground",
|
|
1039
1059
|
children: [
|
|
1040
|
-
/* @__PURE__ */ jsx4("span", { className: "grid size-7 place-items-center rounded-md bg-primary text-primary-foreground", children: /* @__PURE__ */ jsx4("span", { className: "font-mono text-sm font-bold", children: (brand.label[0] ?? "r").toUpperCase() }) }),
|
|
1060
|
+
/* @__PURE__ */ jsx4("span", { className: "grid size-7 place-items-center rounded-md bg-sidebar-primary text-sidebar-primary-foreground", children: /* @__PURE__ */ jsx4("span", { className: "font-mono text-sm font-bold", children: (brand.label[0] ?? "r").toUpperCase() }) }),
|
|
1041
1061
|
/* @__PURE__ */ jsx4("span", { className: "truncate text-sm font-semibold tracking-tight", children: brand.label })
|
|
1042
1062
|
]
|
|
1043
1063
|
}
|
|
@@ -1093,7 +1113,7 @@ function AppSidebar({
|
|
|
1093
1113
|
"aria-modal": "true",
|
|
1094
1114
|
"aria-label": "Navigation",
|
|
1095
1115
|
"data-testid": "app-sidebar-mobile-drawer",
|
|
1096
|
-
className: "absolute inset-y-0 left-0 flex w-72 flex-col overflow-y-auto border-r border-border bg-sidebar",
|
|
1116
|
+
className: "absolute inset-y-0 left-0 flex w-72 flex-col overflow-y-auto border-r border-sidebar-border bg-sidebar",
|
|
1097
1117
|
children: [
|
|
1098
1118
|
/* @__PURE__ */ jsx4("div", { className: "sticky top-0 z-10 flex justify-end bg-sidebar p-2", children: /* @__PURE__ */ jsx4(
|
|
1099
1119
|
"button",
|
|
@@ -1104,7 +1124,7 @@ function AppSidebar({
|
|
|
1104
1124
|
"aria-label": `Close ${menuLabel.toLowerCase()}`,
|
|
1105
1125
|
onClick: () => setMobileOpen(false),
|
|
1106
1126
|
style: TOUCH_TARGET_STYLE,
|
|
1107
|
-
className: "flex items-center justify-center rounded-md p-2 text-foreground hover:bg-foreground/5",
|
|
1127
|
+
className: "flex items-center justify-center rounded-md p-2 text-sidebar-foreground/70 hover:bg-sidebar-foreground/5 hover:text-sidebar-foreground",
|
|
1108
1128
|
children: /* @__PURE__ */ jsx4(X2, { className: "h-5 w-5", "aria-hidden": "true" })
|
|
1109
1129
|
}
|
|
1110
1130
|
) }),
|
|
@@ -1122,7 +1142,7 @@ function AppSidebar({
|
|
|
1122
1142
|
{
|
|
1123
1143
|
"data-testid": "app-sidebar-desktop",
|
|
1124
1144
|
className: cn(
|
|
1125
|
-
"hidden shrink-0 flex-col border-r border-border bg-sidebar md:flex",
|
|
1145
|
+
"hidden shrink-0 flex-col border-r border-sidebar-border bg-sidebar md:flex",
|
|
1126
1146
|
collapsed ? COLLAPSED_WIDTH_CLASS : EXPANDED_WIDTH_CLASS,
|
|
1127
1147
|
// DEV-5331: a plain CSS width transition is enough — no new deps
|
|
1128
1148
|
// for a spring/JS animation. Respect prefers-reduced-motion.
|
|
@@ -1143,7 +1163,7 @@ function AppSidebar({
|
|
|
1143
1163
|
) })
|
|
1144
1164
|
}
|
|
1145
1165
|
),
|
|
1146
|
-
collapsible && /* @__PURE__ */ jsx4("div", { className: "border-t border-border px-2 py-2", children: /* @__PURE__ */ jsxs4(
|
|
1166
|
+
collapsible && /* @__PURE__ */ jsx4("div", { className: "border-t border-sidebar-border px-2 py-2", children: /* @__PURE__ */ jsxs4(
|
|
1147
1167
|
"button",
|
|
1148
1168
|
{
|
|
1149
1169
|
type: "button",
|
|
@@ -1153,7 +1173,7 @@ function AppSidebar({
|
|
|
1153
1173
|
title: collapsed ? "Expand sidebar" : "Collapse sidebar",
|
|
1154
1174
|
onClick: handleToggleCollapsed,
|
|
1155
1175
|
className: cn(
|
|
1156
|
-
"flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm text-
|
|
1176
|
+
"flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm text-sidebar-foreground/70 transition-colors hover:bg-sidebar-foreground/5 hover:text-sidebar-foreground",
|
|
1157
1177
|
collapsed && "justify-center px-0"
|
|
1158
1178
|
),
|
|
1159
1179
|
children: [
|
|
@@ -1291,18 +1311,18 @@ function ContextSwitcher({
|
|
|
1291
1311
|
type: "button",
|
|
1292
1312
|
title: active?.name || placeholder,
|
|
1293
1313
|
"aria-label": active?.name || placeholder,
|
|
1294
|
-
className: "flex h-10 w-10 items-center justify-center rounded-lg text-
|
|
1314
|
+
className: "flex h-10 w-10 items-center justify-center rounded-lg text-sidebar-foreground/70 transition-colors hover:bg-sidebar-foreground/5 hover:text-sidebar-foreground",
|
|
1295
1315
|
children: /* @__PURE__ */ jsx6(BrandGlyph, { brand: toBrand(active ?? { name: "" }), size: "lg" })
|
|
1296
1316
|
}
|
|
1297
1317
|
) : /* @__PURE__ */ jsxs5(
|
|
1298
1318
|
"button",
|
|
1299
1319
|
{
|
|
1300
1320
|
type: "button",
|
|
1301
|
-
className: "group flex items-center gap-2 rounded-md border border-border bg-
|
|
1321
|
+
className: "group flex items-center gap-2 rounded-md border border-sidebar-border bg-sidebar-accent/60 px-2.5 py-1.5 text-sm font-medium transition-colors hover:bg-sidebar",
|
|
1302
1322
|
children: [
|
|
1303
1323
|
/* @__PURE__ */ jsx6(BrandGlyph, { brand: toBrand(active ?? { name: "" }) }),
|
|
1304
|
-
/* @__PURE__ */ jsx6("span", { className: "max-w-[140px] truncate text-foreground
|
|
1305
|
-
/* @__PURE__ */ jsx6(ChevronDown, { className: "h-3.5 w-3.5 text-
|
|
1324
|
+
/* @__PURE__ */ jsx6("span", { className: "max-w-[140px] truncate text-sidebar-accent-foreground", children: active?.name || placeholder }),
|
|
1325
|
+
/* @__PURE__ */ jsx6(ChevronDown, { className: "h-3.5 w-3.5 text-sidebar-foreground/70 transition-transform group-data-[state=open]:rotate-180" })
|
|
1306
1326
|
]
|
|
1307
1327
|
}
|
|
1308
1328
|
) }),
|
|
@@ -1341,9 +1361,262 @@ function ContextSwitcher({
|
|
|
1341
1361
|
)
|
|
1342
1362
|
] });
|
|
1343
1363
|
}
|
|
1364
|
+
|
|
1365
|
+
// src/connector-picker.tsx
|
|
1366
|
+
import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef5, useState as useState7 } from "react";
|
|
1367
|
+
import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1368
|
+
var DEFAULT_CONNECT_ERROR = "Couldn't start the connection. Please try again.";
|
|
1369
|
+
var DEFAULT_DISCONNECT_ERROR = "Couldn't disconnect. Please try again.";
|
|
1370
|
+
var DEFAULT_LOAD_ERROR = "Couldn't load connections. Please try again.";
|
|
1371
|
+
var SKELETON_COUNT = 3;
|
|
1372
|
+
function capabilityLabel(capability) {
|
|
1373
|
+
if (capability === "read") return "Read only";
|
|
1374
|
+
if (capability === "write") return "Write only";
|
|
1375
|
+
return "Read & write";
|
|
1376
|
+
}
|
|
1377
|
+
function formatSince(since) {
|
|
1378
|
+
const parsed = new Date(since);
|
|
1379
|
+
if (Number.isNaN(parsed.getTime())) return since;
|
|
1380
|
+
return parsed.toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
|
|
1381
|
+
}
|
|
1382
|
+
function errorMessageFrom(err, fallback) {
|
|
1383
|
+
return err instanceof Error && err.message ? err.message : fallback;
|
|
1384
|
+
}
|
|
1385
|
+
function ConnectorPicker({
|
|
1386
|
+
client,
|
|
1387
|
+
purpose,
|
|
1388
|
+
lens = "owner",
|
|
1389
|
+
layout = "tiles",
|
|
1390
|
+
onConnected
|
|
1391
|
+
}) {
|
|
1392
|
+
const gated = lens !== "owner";
|
|
1393
|
+
const [entries, setEntries] = useState7(null);
|
|
1394
|
+
const [loadError, setLoadError] = useState7(null);
|
|
1395
|
+
const [uiState, setUiState] = useState7({});
|
|
1396
|
+
const clientRef = useRef5(client);
|
|
1397
|
+
clientRef.current = client;
|
|
1398
|
+
const purposeRef = useRef5(purpose);
|
|
1399
|
+
purposeRef.current = purpose;
|
|
1400
|
+
const onConnectedRef = useRef5(onConnected);
|
|
1401
|
+
onConnectedRef.current = onConnected;
|
|
1402
|
+
const uiStateRef = useRef5(uiState);
|
|
1403
|
+
uiStateRef.current = uiState;
|
|
1404
|
+
const load = useCallback4(async () => {
|
|
1405
|
+
if (gated) return null;
|
|
1406
|
+
try {
|
|
1407
|
+
const list = await clientRef.current.listConnectable({ purpose: purposeRef.current });
|
|
1408
|
+
setEntries(list);
|
|
1409
|
+
setLoadError(null);
|
|
1410
|
+
return list;
|
|
1411
|
+
} catch (err) {
|
|
1412
|
+
setLoadError(errorMessageFrom(err, DEFAULT_LOAD_ERROR));
|
|
1413
|
+
return null;
|
|
1414
|
+
}
|
|
1415
|
+
}, [gated]);
|
|
1416
|
+
useEffect6(() => {
|
|
1417
|
+
if (gated) return;
|
|
1418
|
+
void load();
|
|
1419
|
+
}, [gated, load, purpose]);
|
|
1420
|
+
const refreshAfterReturn = useCallback4(async () => {
|
|
1421
|
+
const list = await load();
|
|
1422
|
+
if (!list) return;
|
|
1423
|
+
setUiState((prev) => {
|
|
1424
|
+
let changed = false;
|
|
1425
|
+
const next = { ...prev };
|
|
1426
|
+
for (const entry of list) {
|
|
1427
|
+
if (next[entry.slug]?.kind === "connecting" && entry.connection.status === "connected") {
|
|
1428
|
+
changed = true;
|
|
1429
|
+
delete next[entry.slug];
|
|
1430
|
+
onConnectedRef.current?.(entry);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
return changed ? next : prev;
|
|
1434
|
+
});
|
|
1435
|
+
}, [load]);
|
|
1436
|
+
useEffect6(() => {
|
|
1437
|
+
if (gated) return;
|
|
1438
|
+
function handleReturn() {
|
|
1439
|
+
if (document.visibilityState === "hidden") return;
|
|
1440
|
+
const hasPendingConnect = Object.values(uiStateRef.current).some(
|
|
1441
|
+
(s) => s.kind === "connecting"
|
|
1442
|
+
);
|
|
1443
|
+
if (!hasPendingConnect) return;
|
|
1444
|
+
void refreshAfterReturn();
|
|
1445
|
+
}
|
|
1446
|
+
window.addEventListener("focus", handleReturn);
|
|
1447
|
+
document.addEventListener("visibilitychange", handleReturn);
|
|
1448
|
+
return () => {
|
|
1449
|
+
window.removeEventListener("focus", handleReturn);
|
|
1450
|
+
document.removeEventListener("visibilitychange", handleReturn);
|
|
1451
|
+
};
|
|
1452
|
+
}, [gated, refreshAfterReturn]);
|
|
1453
|
+
const handleConnect = useCallback4(
|
|
1454
|
+
async (entry) => {
|
|
1455
|
+
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "connecting" } }));
|
|
1456
|
+
try {
|
|
1457
|
+
const result = await clientRef.current.connect(entry);
|
|
1458
|
+
if (result?.connect_url) {
|
|
1459
|
+
window.open(result.connect_url, "_blank", "noopener,noreferrer");
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
const list = await load();
|
|
1463
|
+
const updated = list?.find((e) => e.slug === entry.slug) ?? null;
|
|
1464
|
+
setUiState((prev) => {
|
|
1465
|
+
const next = { ...prev };
|
|
1466
|
+
delete next[entry.slug];
|
|
1467
|
+
return next;
|
|
1468
|
+
});
|
|
1469
|
+
if (updated?.connection.status === "connected") {
|
|
1470
|
+
onConnectedRef.current?.(updated);
|
|
1471
|
+
}
|
|
1472
|
+
} catch (err) {
|
|
1473
|
+
const message = errorMessageFrom(err, DEFAULT_CONNECT_ERROR);
|
|
1474
|
+
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "error", message } }));
|
|
1475
|
+
}
|
|
1476
|
+
},
|
|
1477
|
+
[load]
|
|
1478
|
+
);
|
|
1479
|
+
const handleDisconnect = useCallback4(
|
|
1480
|
+
async (entry) => {
|
|
1481
|
+
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "disconnecting" } }));
|
|
1482
|
+
try {
|
|
1483
|
+
await clientRef.current.disconnect(entry);
|
|
1484
|
+
await load();
|
|
1485
|
+
setUiState((prev) => {
|
|
1486
|
+
const next = { ...prev };
|
|
1487
|
+
delete next[entry.slug];
|
|
1488
|
+
return next;
|
|
1489
|
+
});
|
|
1490
|
+
} catch (err) {
|
|
1491
|
+
const message = errorMessageFrom(err, DEFAULT_DISCONNECT_ERROR);
|
|
1492
|
+
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "error", message } }));
|
|
1493
|
+
}
|
|
1494
|
+
},
|
|
1495
|
+
[load]
|
|
1496
|
+
);
|
|
1497
|
+
if (gated) return null;
|
|
1498
|
+
const containerClassName = cn(
|
|
1499
|
+
"flex flex-col",
|
|
1500
|
+
layout === "tiles" ? "gap-3 sm:grid sm:grid-cols-2 lg:grid-cols-3" : "divide-y divide-border"
|
|
1501
|
+
);
|
|
1502
|
+
return /* @__PURE__ */ jsxs6("div", { "data-testid": "connector-picker", "data-layout": layout, className: containerClassName, children: [
|
|
1503
|
+
entries === null && !loadError && Array.from({ length: SKELETON_COUNT }).map((_, i) => /* @__PURE__ */ jsxs6(
|
|
1504
|
+
"div",
|
|
1505
|
+
{
|
|
1506
|
+
"data-testid": "connector-picker-skeleton-item",
|
|
1507
|
+
"aria-hidden": "true",
|
|
1508
|
+
className: "flex items-center gap-3 rounded-lg border border-border p-3",
|
|
1509
|
+
children: [
|
|
1510
|
+
/* @__PURE__ */ jsx7("span", { className: "h-8 w-8 shrink-0 animate-pulse rounded bg-foreground/10" }),
|
|
1511
|
+
/* @__PURE__ */ jsx7("span", { className: "h-3 flex-1 animate-pulse rounded bg-foreground/10" })
|
|
1512
|
+
]
|
|
1513
|
+
},
|
|
1514
|
+
i
|
|
1515
|
+
)),
|
|
1516
|
+
loadError && /* @__PURE__ */ jsxs6(
|
|
1517
|
+
"div",
|
|
1518
|
+
{
|
|
1519
|
+
role: "alert",
|
|
1520
|
+
"data-testid": "connector-picker-load-error",
|
|
1521
|
+
className: "flex items-center justify-between gap-3 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive",
|
|
1522
|
+
children: [
|
|
1523
|
+
/* @__PURE__ */ jsx7("span", { children: loadError }),
|
|
1524
|
+
/* @__PURE__ */ jsx7("button", { type: "button", onClick: () => void load(), className: "shrink-0 underline", children: "Retry" })
|
|
1525
|
+
]
|
|
1526
|
+
}
|
|
1527
|
+
),
|
|
1528
|
+
entries !== null && entries.length === 0 && !loadError && /* @__PURE__ */ jsx7("p", { "data-testid": "connector-picker-empty", className: "text-sm text-muted-foreground", children: "No connections available." }),
|
|
1529
|
+
entries?.map((entry) => /* @__PURE__ */ jsx7(
|
|
1530
|
+
ConnectorTile,
|
|
1531
|
+
{
|
|
1532
|
+
entry,
|
|
1533
|
+
ui: uiState[entry.slug],
|
|
1534
|
+
layout,
|
|
1535
|
+
onConnect: handleConnect,
|
|
1536
|
+
onDisconnect: handleDisconnect
|
|
1537
|
+
},
|
|
1538
|
+
entry.slug
|
|
1539
|
+
))
|
|
1540
|
+
] });
|
|
1541
|
+
}
|
|
1542
|
+
function ConnectorTile({
|
|
1543
|
+
entry,
|
|
1544
|
+
ui,
|
|
1545
|
+
layout,
|
|
1546
|
+
onConnect,
|
|
1547
|
+
onDisconnect
|
|
1548
|
+
}) {
|
|
1549
|
+
const isConnected = entry.connection.status === "connected";
|
|
1550
|
+
const isConnecting = ui?.kind === "connecting";
|
|
1551
|
+
const isDisconnecting = ui?.kind === "disconnecting";
|
|
1552
|
+
const errorMessage = ui?.kind === "error" ? ui.message : null;
|
|
1553
|
+
return /* @__PURE__ */ jsxs6(
|
|
1554
|
+
"div",
|
|
1555
|
+
{
|
|
1556
|
+
"data-testid": `connector-tile-${entry.slug}`,
|
|
1557
|
+
"data-connected": isConnected || void 0,
|
|
1558
|
+
"data-deprecated": entry.deprecated || void 0,
|
|
1559
|
+
className: cn("flex items-center gap-3 p-3", layout === "tiles" && "rounded-lg border border-border"),
|
|
1560
|
+
children: [
|
|
1561
|
+
/* @__PURE__ */ jsx7(BrandGlyph, { brand: { name: entry.name, logo: entry.logo }, size: "lg" }),
|
|
1562
|
+
/* @__PURE__ */ jsxs6("div", { className: "min-w-0 flex-1", children: [
|
|
1563
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", children: [
|
|
1564
|
+
/* @__PURE__ */ jsx7("p", { className: "truncate text-sm font-medium", children: entry.name }),
|
|
1565
|
+
entry.deprecated && /* @__PURE__ */ jsx7(
|
|
1566
|
+
"span",
|
|
1567
|
+
{
|
|
1568
|
+
"data-testid": `connector-deprecated-${entry.slug}`,
|
|
1569
|
+
className: "shrink-0 rounded-full bg-foreground/10 px-2 py-0.5 text-[11px] text-muted-foreground",
|
|
1570
|
+
children: "No longer available"
|
|
1571
|
+
}
|
|
1572
|
+
)
|
|
1573
|
+
] }),
|
|
1574
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
1575
|
+
/* @__PURE__ */ jsx7("span", { "data-testid": `connector-capability-${entry.slug}`, children: capabilityLabel(entry.capability) }),
|
|
1576
|
+
entry.category && /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
1577
|
+
/* @__PURE__ */ jsx7("span", { "aria-hidden": "true", children: "\xB7" }),
|
|
1578
|
+
/* @__PURE__ */ jsx7("span", { className: "truncate", children: entry.category })
|
|
1579
|
+
] })
|
|
1580
|
+
] }),
|
|
1581
|
+
isConnected && /* @__PURE__ */ jsx7(
|
|
1582
|
+
"p",
|
|
1583
|
+
{
|
|
1584
|
+
"data-testid": `connector-connected-since-${entry.slug}`,
|
|
1585
|
+
className: "text-xs text-muted-foreground",
|
|
1586
|
+
children: entry.connection.since ? `Connected since ${formatSince(entry.connection.since)}` : "Connected"
|
|
1587
|
+
}
|
|
1588
|
+
),
|
|
1589
|
+
errorMessage && /* @__PURE__ */ jsx7("p", { role: "alert", "data-testid": `connector-error-${entry.slug}`, className: "text-xs text-destructive", children: errorMessage })
|
|
1590
|
+
] }),
|
|
1591
|
+
isConnected ? /* @__PURE__ */ jsx7(
|
|
1592
|
+
"button",
|
|
1593
|
+
{
|
|
1594
|
+
type: "button",
|
|
1595
|
+
"data-testid": `connector-disconnect-${entry.slug}`,
|
|
1596
|
+
onClick: () => onDisconnect(entry),
|
|
1597
|
+
disabled: isDisconnecting,
|
|
1598
|
+
className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-destructive hover:bg-destructive/10 disabled:opacity-50",
|
|
1599
|
+
children: isDisconnecting ? "Disconnecting\u2026" : "Disconnect"
|
|
1600
|
+
}
|
|
1601
|
+
) : /* @__PURE__ */ jsx7(
|
|
1602
|
+
"button",
|
|
1603
|
+
{
|
|
1604
|
+
type: "button",
|
|
1605
|
+
"data-testid": `connector-connect-${entry.slug}`,
|
|
1606
|
+
onClick: () => onConnect(entry),
|
|
1607
|
+
disabled: isConnecting,
|
|
1608
|
+
className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
|
|
1609
|
+
children: isConnecting ? "Connecting\u2026" : "Connect"
|
|
1610
|
+
}
|
|
1611
|
+
)
|
|
1612
|
+
]
|
|
1613
|
+
}
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1344
1616
|
export {
|
|
1345
1617
|
AppSidebar,
|
|
1346
1618
|
BrandGlyph,
|
|
1619
|
+
ConnectorPicker,
|
|
1347
1620
|
ContextSwitcher,
|
|
1348
1621
|
Flyout,
|
|
1349
1622
|
GLYPH_CANVAS_FILL,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meetreeve/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
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",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"dev": "tsup --watch",
|
|
36
36
|
"test": "vitest run",
|
|
37
37
|
"typecheck": "tsc --noEmit",
|
|
38
|
-
"check:esm": "node scripts/check-esm-import.mjs",
|
|
38
|
+
"check:esm": "node ../../scripts/check-esm-import.mjs .",
|
|
39
39
|
"prepublishOnly": "npm run build"
|
|
40
40
|
},
|
|
41
41
|
"publishConfig": {
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"@testing-library/dom": "^10.4.1",
|
|
56
56
|
"@testing-library/react": "^16.3.2",
|
|
57
57
|
"@testing-library/user-event": "^14.6.1",
|
|
58
|
+
"@types/node": "^20.19.43",
|
|
58
59
|
"@types/react": "^19",
|
|
59
60
|
"@types/react-dom": "^19",
|
|
60
61
|
"@vitejs/plugin-react": "^5.1.3",
|