@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.mjs
CHANGED
|
@@ -36,6 +36,7 @@ var WORDMARK_WEIGHT = 800;
|
|
|
36
36
|
var WORDMARK_WEIGHT_CLASS = "font-extrabold";
|
|
37
37
|
var WORDMARK_TEXT_LG = "text-base";
|
|
38
38
|
var MONOGRAM_TEXT_LG = "text-[17px]";
|
|
39
|
+
var MONOGRAM_TEXT_XL = "text-[26px]";
|
|
39
40
|
var LOCKUP_GAP = "gap-2";
|
|
40
41
|
var LOCKUP_ROW = "flex items-baseline";
|
|
41
42
|
var GLYPH_CANVAS_FILL = 0.65;
|
|
@@ -75,9 +76,9 @@ function faviconUrl(domain) {
|
|
|
75
76
|
return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(host)}&sz=128`;
|
|
76
77
|
}
|
|
77
78
|
function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
|
|
78
|
-
const box = size === "lg" ? "h-6 w-6" : "h-5 w-5";
|
|
79
|
-
const img = size === "lg" ? "h-4 w-4" : "h-3.5 w-3.5";
|
|
80
|
-
const dim = size === "lg" ? 16 : 14;
|
|
79
|
+
const box = size === "xl" ? "h-10 w-10" : size === "lg" ? "h-6 w-6" : "h-5 w-5";
|
|
80
|
+
const img = size === "xl" ? "h-7 w-7" : size === "lg" ? "h-4 w-4" : "h-3.5 w-3.5";
|
|
81
|
+
const dim = size === "xl" ? 28 : size === "lg" ? 16 : 14;
|
|
81
82
|
const candidates = [];
|
|
82
83
|
if (brand.favicon) candidates.push(brand.favicon);
|
|
83
84
|
if (brand.logo) candidates.push(brand.logo);
|
|
@@ -130,8 +131,10 @@ function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
|
|
|
130
131
|
// DEV-5139 rule 2: at "lg" the letter runs 17px = the text-base
|
|
131
132
|
// wordmark (16px) × MARK_CAP_RATIO (1.08) — a lone round glyph at
|
|
132
133
|
// exact parity optically reads small. "sm" has no wordmark partner
|
|
133
|
-
// in its surfaces;
|
|
134
|
-
|
|
134
|
+
// in its surfaces; unchanged pending a lockup audit there. "xl"
|
|
135
|
+
// (DEV-5331) is a standalone glyph with no wordmark at all — rule 5
|
|
136
|
+
// (GLYPH_CANVAS_FILL) governs it instead of rule 2.
|
|
137
|
+
size === "xl" ? MONOGRAM_TEXT_XL : size === "lg" ? MONOGRAM_TEXT_LG : "text-[11px]",
|
|
135
138
|
box,
|
|
136
139
|
className
|
|
137
140
|
),
|
|
@@ -415,8 +418,8 @@ function useResponsivePlaceholder(desktop, mobile) {
|
|
|
415
418
|
}
|
|
416
419
|
|
|
417
420
|
// src/app-sidebar.tsx
|
|
418
|
-
import { useEffect as useEffect5, useId as useId3, useMemo, useRef as useRef4, useState as useState6 } from "react";
|
|
419
|
-
import { Menu as Menu2, X as X2 } from "lucide-react";
|
|
421
|
+
import { useCallback as useCallback3, useEffect as useEffect5, useId as useId3, useMemo, useRef as useRef4, useState as useState6 } from "react";
|
|
422
|
+
import { Menu as Menu2, PanelLeftClose, PanelLeftOpen, X as X2 } from "lucide-react";
|
|
420
423
|
|
|
421
424
|
// src/lib/resolve-icon.ts
|
|
422
425
|
import * as LucideIcons from "lucide-react";
|
|
@@ -698,11 +701,22 @@ function useLadderRung(ladder, containerRef, options = {}) {
|
|
|
698
701
|
import { useCallback as useCallback2, useEffect as useEffect4, useId as useId2, useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState5 } from "react";
|
|
699
702
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
700
703
|
var PANEL_GAP = 4;
|
|
701
|
-
function Flyout({
|
|
704
|
+
function Flyout({
|
|
705
|
+
label,
|
|
706
|
+
icon,
|
|
707
|
+
active,
|
|
708
|
+
children,
|
|
709
|
+
linkComponent,
|
|
710
|
+
collapsed = false,
|
|
711
|
+
onOpenChange,
|
|
712
|
+
forceClosed = false,
|
|
713
|
+
className
|
|
714
|
+
}) {
|
|
702
715
|
const [hoverOpen, setHoverOpen] = useState5(false);
|
|
703
716
|
const [clickOpen, setClickOpen] = useState5(false);
|
|
704
717
|
const [panelPosition, setPanelPosition] = useState5(null);
|
|
705
|
-
const
|
|
718
|
+
const wantsOpen = hoverOpen || clickOpen;
|
|
719
|
+
const open = wantsOpen && !forceClosed;
|
|
706
720
|
const panelId = useId2();
|
|
707
721
|
const triggerRef = useRef3(null);
|
|
708
722
|
const panelRef = useRef3(null);
|
|
@@ -714,6 +728,17 @@ function Flyout({ label, icon, active, children, linkComponent, className }) {
|
|
|
714
728
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
715
729
|
if (rect) setPanelPosition({ top: rect.top, left: rect.right + PANEL_GAP });
|
|
716
730
|
}, []);
|
|
731
|
+
const onOpenChangeRef = useRef3(onOpenChange);
|
|
732
|
+
onOpenChangeRef.current = onOpenChange;
|
|
733
|
+
useEffect4(() => {
|
|
734
|
+
onOpenChangeRef.current?.(wantsOpen);
|
|
735
|
+
}, [wantsOpen]);
|
|
736
|
+
useEffect4(() => {
|
|
737
|
+
if (forceClosed) {
|
|
738
|
+
setHoverOpen(false);
|
|
739
|
+
setClickOpen(false);
|
|
740
|
+
}
|
|
741
|
+
}, [forceClosed]);
|
|
717
742
|
useLayoutEffect2(() => {
|
|
718
743
|
if (open) reposition();
|
|
719
744
|
}, [open, reposition]);
|
|
@@ -754,9 +779,11 @@ function Flyout({ label, icon, active, children, linkComponent, className }) {
|
|
|
754
779
|
"data-testid": "ladder-flyout-trigger",
|
|
755
780
|
"aria-expanded": open,
|
|
756
781
|
"aria-controls": panelId,
|
|
782
|
+
title: collapsed ? label : void 0,
|
|
757
783
|
onClick: () => setClickOpen((v) => !v),
|
|
758
784
|
className: cn(
|
|
759
785
|
"group flex w-full items-center gap-2.5 rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
786
|
+
collapsed && "justify-center px-0",
|
|
760
787
|
active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
|
|
761
788
|
),
|
|
762
789
|
children: [
|
|
@@ -768,7 +795,7 @@ function Flyout({ label, icon, active, children, linkComponent, className }) {
|
|
|
768
795
|
"aria-hidden": "true"
|
|
769
796
|
}
|
|
770
797
|
),
|
|
771
|
-
/* @__PURE__ */ jsx3("span", { className: "truncate", children: label })
|
|
798
|
+
/* @__PURE__ */ jsx3("span", { className: cn("truncate", collapsed && "sr-only"), children: label })
|
|
772
799
|
]
|
|
773
800
|
}
|
|
774
801
|
),
|
|
@@ -819,6 +846,9 @@ function Flyout({ label, icon, active, children, linkComponent, className }) {
|
|
|
819
846
|
|
|
820
847
|
// src/app-sidebar.tsx
|
|
821
848
|
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
849
|
+
var DEFAULT_COLLAPSE_STORAGE_KEY = "reeve-sidebar-collapsed";
|
|
850
|
+
var COLLAPSED_WIDTH_CLASS = "w-14";
|
|
851
|
+
var EXPANDED_WIDTH_CLASS = "w-64";
|
|
822
852
|
function isHrefActive(href, activePath) {
|
|
823
853
|
return activePath === href || activePath.startsWith(`${href}/`);
|
|
824
854
|
}
|
|
@@ -828,7 +858,8 @@ function NavLink({
|
|
|
828
858
|
icon,
|
|
829
859
|
label,
|
|
830
860
|
testId,
|
|
831
|
-
linkComponent
|
|
861
|
+
linkComponent,
|
|
862
|
+
collapsed = false
|
|
832
863
|
}) {
|
|
833
864
|
const Icon = resolveIcon(icon);
|
|
834
865
|
const LinkEl = linkComponent ?? "a";
|
|
@@ -838,8 +869,10 @@ function NavLink({
|
|
|
838
869
|
href,
|
|
839
870
|
"data-testid": testId,
|
|
840
871
|
"aria-current": active ? "page" : void 0,
|
|
872
|
+
title: collapsed ? label : void 0,
|
|
841
873
|
className: cn(
|
|
842
874
|
"group flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
875
|
+
collapsed && "justify-center px-0",
|
|
843
876
|
active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
|
|
844
877
|
),
|
|
845
878
|
children: [
|
|
@@ -851,7 +884,7 @@ function NavLink({
|
|
|
851
884
|
"aria-hidden": "true"
|
|
852
885
|
}
|
|
853
886
|
),
|
|
854
|
-
/* @__PURE__ */ jsx4("span", { className: "truncate", children: label })
|
|
887
|
+
/* @__PURE__ */ jsx4("span", { className: cn("truncate", collapsed && "sr-only"), children: label })
|
|
855
888
|
]
|
|
856
889
|
}
|
|
857
890
|
);
|
|
@@ -867,16 +900,54 @@ function AppSidebar({
|
|
|
867
900
|
ladderOptions,
|
|
868
901
|
linkComponent,
|
|
869
902
|
isActive,
|
|
903
|
+
collapsible = true,
|
|
904
|
+
defaultCollapsed = false,
|
|
905
|
+
onCollapsedChange,
|
|
906
|
+
storageKey = DEFAULT_COLLAPSE_STORAGE_KEY,
|
|
870
907
|
className
|
|
871
908
|
}) {
|
|
872
909
|
const isActiveFn = isActive ?? isHrefActive;
|
|
873
910
|
const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINTS.md}px)`);
|
|
911
|
+
const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
|
|
874
912
|
const [mobileOpen, setMobileOpen] = useState6(false);
|
|
875
913
|
const containerRef = useRef4(null);
|
|
876
914
|
const drawerId = useId3();
|
|
877
915
|
const mobileTriggerRef = useRef4(null);
|
|
878
916
|
const mobileDrawerRef = useRef4(null);
|
|
879
917
|
const mobileCloseRef = useRef4(null);
|
|
918
|
+
const [collapsed, setCollapsed] = useState6(defaultCollapsed);
|
|
919
|
+
const [collapseHydrated, setCollapseHydrated] = useState6(false);
|
|
920
|
+
useEffect5(() => {
|
|
921
|
+
if (!collapsible) return;
|
|
922
|
+
try {
|
|
923
|
+
const stored = localStorage.getItem(storageKey);
|
|
924
|
+
if (stored === "true") setCollapsed(true);
|
|
925
|
+
else if (stored === "false") setCollapsed(false);
|
|
926
|
+
} catch {
|
|
927
|
+
}
|
|
928
|
+
setCollapseHydrated(true);
|
|
929
|
+
}, []);
|
|
930
|
+
useEffect5(() => {
|
|
931
|
+
if (!collapsible || !collapseHydrated) return;
|
|
932
|
+
try {
|
|
933
|
+
localStorage.setItem(storageKey, String(collapsed));
|
|
934
|
+
} catch {
|
|
935
|
+
}
|
|
936
|
+
}, [collapsed, collapseHydrated, collapsible, storageKey]);
|
|
937
|
+
function handleToggleCollapsed() {
|
|
938
|
+
setCollapsed((prev) => {
|
|
939
|
+
const next = !prev;
|
|
940
|
+
onCollapsedChange?.(next);
|
|
941
|
+
return next;
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
const [openFlyoutId, setOpenFlyoutId] = useState6(null);
|
|
945
|
+
const handleFlyoutOpenChange = useCallback3((id, isOpen) => {
|
|
946
|
+
setOpenFlyoutId((current) => {
|
|
947
|
+
if (isOpen) return id;
|
|
948
|
+
return current === id ? null : current;
|
|
949
|
+
});
|
|
950
|
+
}, []);
|
|
880
951
|
const moduleMap = useMemo(() => new Map(modules.map((m) => [m.moduleId, m])), [modules]);
|
|
881
952
|
const registryForLadder = useMemo(
|
|
882
953
|
() => modules.map((m) => ({ moduleId: m.moduleId, group: m.group })),
|
|
@@ -898,7 +969,7 @@ function AppSidebar({
|
|
|
898
969
|
triggerRef: mobileTriggerRef,
|
|
899
970
|
initialFocusRef: mobileCloseRef
|
|
900
971
|
});
|
|
901
|
-
function renderAppItem(item, testIdPrefix) {
|
|
972
|
+
function renderAppItem(item, testIdPrefix, collapsedRow = false) {
|
|
902
973
|
const def = moduleMap.get(item.moduleId);
|
|
903
974
|
if (!def) return null;
|
|
904
975
|
return /* @__PURE__ */ jsx4(
|
|
@@ -909,12 +980,13 @@ function AppSidebar({
|
|
|
909
980
|
icon: def.icon,
|
|
910
981
|
label: def.label,
|
|
911
982
|
testId: `${testIdPrefix}-${item.moduleId}`,
|
|
912
|
-
linkComponent
|
|
983
|
+
linkComponent,
|
|
984
|
+
collapsed: collapsedRow
|
|
913
985
|
},
|
|
914
986
|
item.moduleId
|
|
915
987
|
);
|
|
916
988
|
}
|
|
917
|
-
function renderGroupItem(item) {
|
|
989
|
+
function renderGroupItem(item, collapsedRow = false) {
|
|
918
990
|
const children = item.children.map((moduleId) => {
|
|
919
991
|
const def = moduleMap.get(moduleId);
|
|
920
992
|
const href = def?.href ?? "#";
|
|
@@ -932,27 +1004,48 @@ function AppSidebar({
|
|
|
932
1004
|
label: item.label,
|
|
933
1005
|
icon: item.icon,
|
|
934
1006
|
active,
|
|
935
|
-
|
|
936
|
-
|
|
1007
|
+
linkComponent,
|
|
1008
|
+
collapsed: collapsedRow,
|
|
1009
|
+
forceClosed: openFlyoutId !== null && openFlyoutId !== item.id,
|
|
1010
|
+
onOpenChange: (isOpen) => handleFlyoutOpenChange(item.id, isOpen),
|
|
1011
|
+
children
|
|
937
1012
|
},
|
|
938
1013
|
item.id
|
|
939
1014
|
);
|
|
940
1015
|
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
{
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1016
|
+
function brandBlockFor(collapsedMode) {
|
|
1017
|
+
if (!brand) return null;
|
|
1018
|
+
if (collapsedMode) {
|
|
1019
|
+
return /* @__PURE__ */ jsxs4(
|
|
1020
|
+
"a",
|
|
1021
|
+
{
|
|
1022
|
+
href: brand.href,
|
|
1023
|
+
"data-testid": "app-sidebar-brand",
|
|
1024
|
+
title: brand.label,
|
|
1025
|
+
className: "flex items-center justify-center border-b border-border px-1 py-4",
|
|
1026
|
+
children: [
|
|
1027
|
+
/* @__PURE__ */ jsx4("span", { className: "sr-only", children: brand.label }),
|
|
1028
|
+
/* @__PURE__ */ jsx4(BrandGlyph, { brand: { name: brand.label }, size: "xl" })
|
|
1029
|
+
]
|
|
1030
|
+
}
|
|
1031
|
+
);
|
|
951
1032
|
}
|
|
952
|
-
|
|
953
|
-
|
|
1033
|
+
return /* @__PURE__ */ jsxs4(
|
|
1034
|
+
"a",
|
|
1035
|
+
{
|
|
1036
|
+
href: brand.href,
|
|
1037
|
+
"data-testid": "app-sidebar-brand",
|
|
1038
|
+
className: "flex items-center gap-2.5 border-b border-border px-5 py-4",
|
|
1039
|
+
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() }) }),
|
|
1041
|
+
/* @__PURE__ */ jsx4("span", { className: "truncate text-sm font-semibold tracking-tight", children: brand.label })
|
|
1042
|
+
]
|
|
1043
|
+
}
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
function topItemBlock(testId, collapsedMode = false) {
|
|
954
1047
|
if (!topItem) return null;
|
|
955
|
-
return /* @__PURE__ */ jsx4("div", { className: "px-3 pt-4", children: /* @__PURE__ */ jsx4(
|
|
1048
|
+
return /* @__PURE__ */ jsx4("div", { className: cn("px-3 pt-4", collapsedMode && "flex justify-center px-0"), children: /* @__PURE__ */ jsx4(
|
|
956
1049
|
NavLink,
|
|
957
1050
|
{
|
|
958
1051
|
href: topItem.href,
|
|
@@ -960,7 +1053,8 @@ function AppSidebar({
|
|
|
960
1053
|
icon: topItem.icon,
|
|
961
1054
|
label: topItem.label,
|
|
962
1055
|
testId,
|
|
963
|
-
linkComponent
|
|
1056
|
+
linkComponent,
|
|
1057
|
+
collapsed: collapsedMode
|
|
964
1058
|
}
|
|
965
1059
|
) });
|
|
966
1060
|
}
|
|
@@ -1014,7 +1108,7 @@ function AppSidebar({
|
|
|
1014
1108
|
children: /* @__PURE__ */ jsx4(X2, { className: "h-5 w-5", "aria-hidden": "true" })
|
|
1015
1109
|
}
|
|
1016
1110
|
) }),
|
|
1017
|
-
|
|
1111
|
+
brandBlockFor(false),
|
|
1018
1112
|
topItemBlock("app-sidebar-mobile-top-item"),
|
|
1019
1113
|
/* @__PURE__ */ jsx4("nav", { className: "flex-1 px-3 py-4", children: /* @__PURE__ */ jsx4("div", { className: "flex flex-col gap-0.5", children: rung0.items.map(
|
|
1020
1114
|
(item) => item.type === "app" ? renderAppItem(item, "app-sidebar-mobile-navlink") : null
|
|
@@ -1027,19 +1121,47 @@ function AppSidebar({
|
|
|
1027
1121
|
"aside",
|
|
1028
1122
|
{
|
|
1029
1123
|
"data-testid": "app-sidebar-desktop",
|
|
1030
|
-
className: cn(
|
|
1124
|
+
className: cn(
|
|
1125
|
+
"hidden shrink-0 flex-col border-r border-border bg-sidebar md:flex",
|
|
1126
|
+
collapsed ? COLLAPSED_WIDTH_CLASS : EXPANDED_WIDTH_CLASS,
|
|
1127
|
+
// DEV-5331: a plain CSS width transition is enough — no new deps
|
|
1128
|
+
// for a spring/JS animation. Respect prefers-reduced-motion.
|
|
1129
|
+
!prefersReducedMotion && "transition-[width] duration-200 ease-out",
|
|
1130
|
+
className
|
|
1131
|
+
),
|
|
1031
1132
|
children: [
|
|
1032
|
-
|
|
1033
|
-
topItemBlock("app-sidebar-top-item"),
|
|
1133
|
+
brandBlockFor(collapsed),
|
|
1134
|
+
topItemBlock("app-sidebar-top-item", collapsed),
|
|
1034
1135
|
/* @__PURE__ */ jsx4(
|
|
1035
1136
|
"nav",
|
|
1036
1137
|
{
|
|
1037
1138
|
ref: containerRef,
|
|
1038
1139
|
"data-testid": "app-sidebar-nav",
|
|
1039
1140
|
className: "flex-1 overflow-y-auto px-3 py-4",
|
|
1040
|
-
children: /* @__PURE__ */ jsx4("div", { className: "flex flex-col gap-0.5", children: rung.items.map(
|
|
1141
|
+
children: /* @__PURE__ */ jsx4("div", { className: "flex flex-col gap-0.5", children: rung.items.map(
|
|
1142
|
+
(item) => item.type === "app" ? renderAppItem(item, "app-sidebar-navlink", collapsed) : renderGroupItem(item, collapsed)
|
|
1143
|
+
) })
|
|
1041
1144
|
}
|
|
1042
|
-
)
|
|
1145
|
+
),
|
|
1146
|
+
collapsible && /* @__PURE__ */ jsx4("div", { className: "border-t border-border px-2 py-2", children: /* @__PURE__ */ jsxs4(
|
|
1147
|
+
"button",
|
|
1148
|
+
{
|
|
1149
|
+
type: "button",
|
|
1150
|
+
"data-testid": "app-sidebar-collapse-toggle",
|
|
1151
|
+
"aria-pressed": collapsed,
|
|
1152
|
+
"aria-label": collapsed ? "Expand sidebar" : "Collapse sidebar",
|
|
1153
|
+
title: collapsed ? "Expand sidebar" : "Collapse sidebar",
|
|
1154
|
+
onClick: handleToggleCollapsed,
|
|
1155
|
+
className: cn(
|
|
1156
|
+
"flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-foreground/5 hover:text-foreground",
|
|
1157
|
+
collapsed && "justify-center px-0"
|
|
1158
|
+
),
|
|
1159
|
+
children: [
|
|
1160
|
+
collapsed ? /* @__PURE__ */ jsx4(PanelLeftOpen, { className: "size-4 shrink-0", strokeWidth: 1.5, "aria-hidden": "true" }) : /* @__PURE__ */ jsx4(PanelLeftClose, { className: "size-4 shrink-0", strokeWidth: 1.5, "aria-hidden": "true" }),
|
|
1161
|
+
/* @__PURE__ */ jsx4("span", { className: cn("truncate", collapsed && "sr-only"), children: collapsed ? "Expand sidebar" : "Collapse sidebar" })
|
|
1162
|
+
]
|
|
1163
|
+
}
|
|
1164
|
+
) })
|
|
1043
1165
|
]
|
|
1044
1166
|
}
|
|
1045
1167
|
)
|
|
@@ -1219,9 +1341,262 @@ function ContextSwitcher({
|
|
|
1219
1341
|
)
|
|
1220
1342
|
] });
|
|
1221
1343
|
}
|
|
1344
|
+
|
|
1345
|
+
// src/connector-picker.tsx
|
|
1346
|
+
import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef5, useState as useState7 } from "react";
|
|
1347
|
+
import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1348
|
+
var DEFAULT_CONNECT_ERROR = "Couldn't start the connection. Please try again.";
|
|
1349
|
+
var DEFAULT_DISCONNECT_ERROR = "Couldn't disconnect. Please try again.";
|
|
1350
|
+
var DEFAULT_LOAD_ERROR = "Couldn't load connections. Please try again.";
|
|
1351
|
+
var SKELETON_COUNT = 3;
|
|
1352
|
+
function capabilityLabel(capability) {
|
|
1353
|
+
if (capability === "read") return "Read only";
|
|
1354
|
+
if (capability === "write") return "Write only";
|
|
1355
|
+
return "Read & write";
|
|
1356
|
+
}
|
|
1357
|
+
function formatSince(since) {
|
|
1358
|
+
const parsed = new Date(since);
|
|
1359
|
+
if (Number.isNaN(parsed.getTime())) return since;
|
|
1360
|
+
return parsed.toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
|
|
1361
|
+
}
|
|
1362
|
+
function errorMessageFrom(err, fallback) {
|
|
1363
|
+
return err instanceof Error && err.message ? err.message : fallback;
|
|
1364
|
+
}
|
|
1365
|
+
function ConnectorPicker({
|
|
1366
|
+
client,
|
|
1367
|
+
purpose,
|
|
1368
|
+
lens = "owner",
|
|
1369
|
+
layout = "tiles",
|
|
1370
|
+
onConnected
|
|
1371
|
+
}) {
|
|
1372
|
+
const gated = lens !== "owner";
|
|
1373
|
+
const [entries, setEntries] = useState7(null);
|
|
1374
|
+
const [loadError, setLoadError] = useState7(null);
|
|
1375
|
+
const [uiState, setUiState] = useState7({});
|
|
1376
|
+
const clientRef = useRef5(client);
|
|
1377
|
+
clientRef.current = client;
|
|
1378
|
+
const purposeRef = useRef5(purpose);
|
|
1379
|
+
purposeRef.current = purpose;
|
|
1380
|
+
const onConnectedRef = useRef5(onConnected);
|
|
1381
|
+
onConnectedRef.current = onConnected;
|
|
1382
|
+
const uiStateRef = useRef5(uiState);
|
|
1383
|
+
uiStateRef.current = uiState;
|
|
1384
|
+
const load = useCallback4(async () => {
|
|
1385
|
+
if (gated) return null;
|
|
1386
|
+
try {
|
|
1387
|
+
const list = await clientRef.current.listConnectable({ purpose: purposeRef.current });
|
|
1388
|
+
setEntries(list);
|
|
1389
|
+
setLoadError(null);
|
|
1390
|
+
return list;
|
|
1391
|
+
} catch (err) {
|
|
1392
|
+
setLoadError(errorMessageFrom(err, DEFAULT_LOAD_ERROR));
|
|
1393
|
+
return null;
|
|
1394
|
+
}
|
|
1395
|
+
}, [gated]);
|
|
1396
|
+
useEffect6(() => {
|
|
1397
|
+
if (gated) return;
|
|
1398
|
+
void load();
|
|
1399
|
+
}, [gated, load, purpose]);
|
|
1400
|
+
const refreshAfterReturn = useCallback4(async () => {
|
|
1401
|
+
const list = await load();
|
|
1402
|
+
if (!list) return;
|
|
1403
|
+
setUiState((prev) => {
|
|
1404
|
+
let changed = false;
|
|
1405
|
+
const next = { ...prev };
|
|
1406
|
+
for (const entry of list) {
|
|
1407
|
+
if (next[entry.slug]?.kind === "connecting" && entry.connection.status === "connected") {
|
|
1408
|
+
changed = true;
|
|
1409
|
+
delete next[entry.slug];
|
|
1410
|
+
onConnectedRef.current?.(entry);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
return changed ? next : prev;
|
|
1414
|
+
});
|
|
1415
|
+
}, [load]);
|
|
1416
|
+
useEffect6(() => {
|
|
1417
|
+
if (gated) return;
|
|
1418
|
+
function handleReturn() {
|
|
1419
|
+
if (document.visibilityState === "hidden") return;
|
|
1420
|
+
const hasPendingConnect = Object.values(uiStateRef.current).some(
|
|
1421
|
+
(s) => s.kind === "connecting"
|
|
1422
|
+
);
|
|
1423
|
+
if (!hasPendingConnect) return;
|
|
1424
|
+
void refreshAfterReturn();
|
|
1425
|
+
}
|
|
1426
|
+
window.addEventListener("focus", handleReturn);
|
|
1427
|
+
document.addEventListener("visibilitychange", handleReturn);
|
|
1428
|
+
return () => {
|
|
1429
|
+
window.removeEventListener("focus", handleReturn);
|
|
1430
|
+
document.removeEventListener("visibilitychange", handleReturn);
|
|
1431
|
+
};
|
|
1432
|
+
}, [gated, refreshAfterReturn]);
|
|
1433
|
+
const handleConnect = useCallback4(
|
|
1434
|
+
async (entry) => {
|
|
1435
|
+
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "connecting" } }));
|
|
1436
|
+
try {
|
|
1437
|
+
const result = await clientRef.current.connect(entry);
|
|
1438
|
+
if (result?.connect_url) {
|
|
1439
|
+
window.open(result.connect_url, "_blank", "noopener,noreferrer");
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
const list = await load();
|
|
1443
|
+
const updated = list?.find((e) => e.slug === entry.slug) ?? null;
|
|
1444
|
+
setUiState((prev) => {
|
|
1445
|
+
const next = { ...prev };
|
|
1446
|
+
delete next[entry.slug];
|
|
1447
|
+
return next;
|
|
1448
|
+
});
|
|
1449
|
+
if (updated?.connection.status === "connected") {
|
|
1450
|
+
onConnectedRef.current?.(updated);
|
|
1451
|
+
}
|
|
1452
|
+
} catch (err) {
|
|
1453
|
+
const message = errorMessageFrom(err, DEFAULT_CONNECT_ERROR);
|
|
1454
|
+
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "error", message } }));
|
|
1455
|
+
}
|
|
1456
|
+
},
|
|
1457
|
+
[load]
|
|
1458
|
+
);
|
|
1459
|
+
const handleDisconnect = useCallback4(
|
|
1460
|
+
async (entry) => {
|
|
1461
|
+
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "disconnecting" } }));
|
|
1462
|
+
try {
|
|
1463
|
+
await clientRef.current.disconnect(entry);
|
|
1464
|
+
await load();
|
|
1465
|
+
setUiState((prev) => {
|
|
1466
|
+
const next = { ...prev };
|
|
1467
|
+
delete next[entry.slug];
|
|
1468
|
+
return next;
|
|
1469
|
+
});
|
|
1470
|
+
} catch (err) {
|
|
1471
|
+
const message = errorMessageFrom(err, DEFAULT_DISCONNECT_ERROR);
|
|
1472
|
+
setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "error", message } }));
|
|
1473
|
+
}
|
|
1474
|
+
},
|
|
1475
|
+
[load]
|
|
1476
|
+
);
|
|
1477
|
+
if (gated) return null;
|
|
1478
|
+
const containerClassName = cn(
|
|
1479
|
+
"flex flex-col",
|
|
1480
|
+
layout === "tiles" ? "gap-3 sm:grid sm:grid-cols-2 lg:grid-cols-3" : "divide-y divide-border"
|
|
1481
|
+
);
|
|
1482
|
+
return /* @__PURE__ */ jsxs6("div", { "data-testid": "connector-picker", "data-layout": layout, className: containerClassName, children: [
|
|
1483
|
+
entries === null && !loadError && Array.from({ length: SKELETON_COUNT }).map((_, i) => /* @__PURE__ */ jsxs6(
|
|
1484
|
+
"div",
|
|
1485
|
+
{
|
|
1486
|
+
"data-testid": "connector-picker-skeleton-item",
|
|
1487
|
+
"aria-hidden": "true",
|
|
1488
|
+
className: "flex items-center gap-3 rounded-lg border border-border p-3",
|
|
1489
|
+
children: [
|
|
1490
|
+
/* @__PURE__ */ jsx7("span", { className: "h-8 w-8 shrink-0 animate-pulse rounded bg-foreground/10" }),
|
|
1491
|
+
/* @__PURE__ */ jsx7("span", { className: "h-3 flex-1 animate-pulse rounded bg-foreground/10" })
|
|
1492
|
+
]
|
|
1493
|
+
},
|
|
1494
|
+
i
|
|
1495
|
+
)),
|
|
1496
|
+
loadError && /* @__PURE__ */ jsxs6(
|
|
1497
|
+
"div",
|
|
1498
|
+
{
|
|
1499
|
+
role: "alert",
|
|
1500
|
+
"data-testid": "connector-picker-load-error",
|
|
1501
|
+
className: "flex items-center justify-between gap-3 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive",
|
|
1502
|
+
children: [
|
|
1503
|
+
/* @__PURE__ */ jsx7("span", { children: loadError }),
|
|
1504
|
+
/* @__PURE__ */ jsx7("button", { type: "button", onClick: () => void load(), className: "shrink-0 underline", children: "Retry" })
|
|
1505
|
+
]
|
|
1506
|
+
}
|
|
1507
|
+
),
|
|
1508
|
+
entries !== null && entries.length === 0 && !loadError && /* @__PURE__ */ jsx7("p", { "data-testid": "connector-picker-empty", className: "text-sm text-muted-foreground", children: "No connections available." }),
|
|
1509
|
+
entries?.map((entry) => /* @__PURE__ */ jsx7(
|
|
1510
|
+
ConnectorTile,
|
|
1511
|
+
{
|
|
1512
|
+
entry,
|
|
1513
|
+
ui: uiState[entry.slug],
|
|
1514
|
+
layout,
|
|
1515
|
+
onConnect: handleConnect,
|
|
1516
|
+
onDisconnect: handleDisconnect
|
|
1517
|
+
},
|
|
1518
|
+
entry.slug
|
|
1519
|
+
))
|
|
1520
|
+
] });
|
|
1521
|
+
}
|
|
1522
|
+
function ConnectorTile({
|
|
1523
|
+
entry,
|
|
1524
|
+
ui,
|
|
1525
|
+
layout,
|
|
1526
|
+
onConnect,
|
|
1527
|
+
onDisconnect
|
|
1528
|
+
}) {
|
|
1529
|
+
const isConnected = entry.connection.status === "connected";
|
|
1530
|
+
const isConnecting = ui?.kind === "connecting";
|
|
1531
|
+
const isDisconnecting = ui?.kind === "disconnecting";
|
|
1532
|
+
const errorMessage = ui?.kind === "error" ? ui.message : null;
|
|
1533
|
+
return /* @__PURE__ */ jsxs6(
|
|
1534
|
+
"div",
|
|
1535
|
+
{
|
|
1536
|
+
"data-testid": `connector-tile-${entry.slug}`,
|
|
1537
|
+
"data-connected": isConnected || void 0,
|
|
1538
|
+
"data-deprecated": entry.deprecated || void 0,
|
|
1539
|
+
className: cn("flex items-center gap-3 p-3", layout === "tiles" && "rounded-lg border border-border"),
|
|
1540
|
+
children: [
|
|
1541
|
+
/* @__PURE__ */ jsx7(BrandGlyph, { brand: { name: entry.name, logo: entry.logo }, size: "lg" }),
|
|
1542
|
+
/* @__PURE__ */ jsxs6("div", { className: "min-w-0 flex-1", children: [
|
|
1543
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", children: [
|
|
1544
|
+
/* @__PURE__ */ jsx7("p", { className: "truncate text-sm font-medium", children: entry.name }),
|
|
1545
|
+
entry.deprecated && /* @__PURE__ */ jsx7(
|
|
1546
|
+
"span",
|
|
1547
|
+
{
|
|
1548
|
+
"data-testid": `connector-deprecated-${entry.slug}`,
|
|
1549
|
+
className: "shrink-0 rounded-full bg-foreground/10 px-2 py-0.5 text-[11px] text-muted-foreground",
|
|
1550
|
+
children: "No longer available"
|
|
1551
|
+
}
|
|
1552
|
+
)
|
|
1553
|
+
] }),
|
|
1554
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
1555
|
+
/* @__PURE__ */ jsx7("span", { "data-testid": `connector-capability-${entry.slug}`, children: capabilityLabel(entry.capability) }),
|
|
1556
|
+
entry.category && /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
1557
|
+
/* @__PURE__ */ jsx7("span", { "aria-hidden": "true", children: "\xB7" }),
|
|
1558
|
+
/* @__PURE__ */ jsx7("span", { className: "truncate", children: entry.category })
|
|
1559
|
+
] })
|
|
1560
|
+
] }),
|
|
1561
|
+
isConnected && /* @__PURE__ */ jsx7(
|
|
1562
|
+
"p",
|
|
1563
|
+
{
|
|
1564
|
+
"data-testid": `connector-connected-since-${entry.slug}`,
|
|
1565
|
+
className: "text-xs text-muted-foreground",
|
|
1566
|
+
children: entry.connection.since ? `Connected since ${formatSince(entry.connection.since)}` : "Connected"
|
|
1567
|
+
}
|
|
1568
|
+
),
|
|
1569
|
+
errorMessage && /* @__PURE__ */ jsx7("p", { role: "alert", "data-testid": `connector-error-${entry.slug}`, className: "text-xs text-destructive", children: errorMessage })
|
|
1570
|
+
] }),
|
|
1571
|
+
isConnected ? /* @__PURE__ */ jsx7(
|
|
1572
|
+
"button",
|
|
1573
|
+
{
|
|
1574
|
+
type: "button",
|
|
1575
|
+
"data-testid": `connector-disconnect-${entry.slug}`,
|
|
1576
|
+
onClick: () => onDisconnect(entry),
|
|
1577
|
+
disabled: isDisconnecting,
|
|
1578
|
+
className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-destructive hover:bg-destructive/10 disabled:opacity-50",
|
|
1579
|
+
children: isDisconnecting ? "Disconnecting\u2026" : "Disconnect"
|
|
1580
|
+
}
|
|
1581
|
+
) : /* @__PURE__ */ jsx7(
|
|
1582
|
+
"button",
|
|
1583
|
+
{
|
|
1584
|
+
type: "button",
|
|
1585
|
+
"data-testid": `connector-connect-${entry.slug}`,
|
|
1586
|
+
onClick: () => onConnect(entry),
|
|
1587
|
+
disabled: isConnecting,
|
|
1588
|
+
className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
|
|
1589
|
+
children: isConnecting ? "Connecting\u2026" : "Connect"
|
|
1590
|
+
}
|
|
1591
|
+
)
|
|
1592
|
+
]
|
|
1593
|
+
}
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1222
1596
|
export {
|
|
1223
1597
|
AppSidebar,
|
|
1224
1598
|
BrandGlyph,
|
|
1599
|
+
ConnectorPicker,
|
|
1225
1600
|
ContextSwitcher,
|
|
1226
1601
|
Flyout,
|
|
1227
1602
|
GLYPH_CANVAS_FILL,
|
package/dist/lockup/index.d.mts
CHANGED
|
@@ -51,6 +51,17 @@ declare const WORDMARK_WEIGHT_CLASS = "font-extrabold";
|
|
|
51
51
|
*/
|
|
52
52
|
declare const WORDMARK_TEXT_LG = "text-base";
|
|
53
53
|
declare const MONOGRAM_TEXT_LG = "text-[17px]";
|
|
54
|
+
/**
|
|
55
|
+
* "xl" — a bare, STANDALONE glyph slot with no adjacent wordmark at all
|
|
56
|
+
* (e.g. AppSidebar's collapsed 56px icon rail, DEV-5331 — Matt's live
|
|
57
|
+
* review found the "lg" monogram too small there). No wordmark to pair
|
|
58
|
+
* with means rule 2 (MARK_CAP_RATIO) doesn't apply; rule 5
|
|
59
|
+
* (GLYPH_CANVAS_FILL) does instead — same governing rule as a favicon/
|
|
60
|
+
* avatar tile. Canvas = 40px (`h-10 w-10`); letter cap height = 40 ×
|
|
61
|
+
* GLYPH_CANVAS_FILL (0.65) = 26px, landing in Matt's requested ~24-28px
|
|
62
|
+
* range for a rail comfortably wider than the glyph itself.
|
|
63
|
+
*/
|
|
64
|
+
declare const MONOGRAM_TEXT_XL = "text-[26px]";
|
|
54
65
|
/**
|
|
55
66
|
* Rule 3, applied: 8px gap ≈ 0.70× the wordmark's ~11.5px cap height (at
|
|
56
67
|
* text-base) — inside the 0.5–0.8 band. Named so nobody "fixes" it
|
|
@@ -65,4 +76,4 @@ declare const LOCKUP_ROW = "flex items-baseline";
|
|
|
65
76
|
/** Rule 5 — standalone glyph cap height as a fraction of the canvas edge. */
|
|
66
77
|
declare const GLYPH_CANVAS_FILL = 0.65;
|
|
67
78
|
|
|
68
|
-
export { GLYPH_CANVAS_FILL, LOCKUP_GAP, LOCKUP_ROW, MARK_CAP_RATIO, MARK_WEIGHT, MARK_WEIGHT_CLASS, MONOGRAM_TEXT_LG, WORDMARK_TEXT_LG, WORDMARK_WEIGHT, WORDMARK_WEIGHT_CLASS };
|
|
79
|
+
export { GLYPH_CANVAS_FILL, LOCKUP_GAP, LOCKUP_ROW, MARK_CAP_RATIO, MARK_WEIGHT, MARK_WEIGHT_CLASS, MONOGRAM_TEXT_LG, MONOGRAM_TEXT_XL, WORDMARK_TEXT_LG, WORDMARK_WEIGHT, WORDMARK_WEIGHT_CLASS };
|
package/dist/lockup/index.d.ts
CHANGED
|
@@ -51,6 +51,17 @@ declare const WORDMARK_WEIGHT_CLASS = "font-extrabold";
|
|
|
51
51
|
*/
|
|
52
52
|
declare const WORDMARK_TEXT_LG = "text-base";
|
|
53
53
|
declare const MONOGRAM_TEXT_LG = "text-[17px]";
|
|
54
|
+
/**
|
|
55
|
+
* "xl" — a bare, STANDALONE glyph slot with no adjacent wordmark at all
|
|
56
|
+
* (e.g. AppSidebar's collapsed 56px icon rail, DEV-5331 — Matt's live
|
|
57
|
+
* review found the "lg" monogram too small there). No wordmark to pair
|
|
58
|
+
* with means rule 2 (MARK_CAP_RATIO) doesn't apply; rule 5
|
|
59
|
+
* (GLYPH_CANVAS_FILL) does instead — same governing rule as a favicon/
|
|
60
|
+
* avatar tile. Canvas = 40px (`h-10 w-10`); letter cap height = 40 ×
|
|
61
|
+
* GLYPH_CANVAS_FILL (0.65) = 26px, landing in Matt's requested ~24-28px
|
|
62
|
+
* range for a rail comfortably wider than the glyph itself.
|
|
63
|
+
*/
|
|
64
|
+
declare const MONOGRAM_TEXT_XL = "text-[26px]";
|
|
54
65
|
/**
|
|
55
66
|
* Rule 3, applied: 8px gap ≈ 0.70× the wordmark's ~11.5px cap height (at
|
|
56
67
|
* text-base) — inside the 0.5–0.8 band. Named so nobody "fixes" it
|
|
@@ -65,4 +76,4 @@ declare const LOCKUP_ROW = "flex items-baseline";
|
|
|
65
76
|
/** Rule 5 — standalone glyph cap height as a fraction of the canvas edge. */
|
|
66
77
|
declare const GLYPH_CANVAS_FILL = 0.65;
|
|
67
78
|
|
|
68
|
-
export { GLYPH_CANVAS_FILL, LOCKUP_GAP, LOCKUP_ROW, MARK_CAP_RATIO, MARK_WEIGHT, MARK_WEIGHT_CLASS, MONOGRAM_TEXT_LG, WORDMARK_TEXT_LG, WORDMARK_WEIGHT, WORDMARK_WEIGHT_CLASS };
|
|
79
|
+
export { GLYPH_CANVAS_FILL, LOCKUP_GAP, LOCKUP_ROW, MARK_CAP_RATIO, MARK_WEIGHT, MARK_WEIGHT_CLASS, MONOGRAM_TEXT_LG, MONOGRAM_TEXT_XL, WORDMARK_TEXT_LG, WORDMARK_WEIGHT, WORDMARK_WEIGHT_CLASS };
|