@meetreeve/ui 0.6.2 → 0.7.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 +47 -3
- package/dist/index.d.ts +47 -3
- package/dist/index.js +156 -34
- package/dist/index.mjs +158 -36
- package/dist/lockup/index.d.mts +79 -0
- package/dist/lockup/index.d.ts +79 -0
- package/dist/lockup/index.js +60 -0
- package/dist/lockup/index.mjs +25 -0
- package/package.json +6 -1
package/dist/index.d.mts
CHANGED
|
@@ -42,7 +42,14 @@ interface Brand {
|
|
|
42
42
|
declare function faviconUrl(domain: string | null | undefined): string | null;
|
|
43
43
|
interface BrandGlyphProps {
|
|
44
44
|
brand: Brand;
|
|
45
|
-
|
|
45
|
+
/**
|
|
46
|
+
* "sm"/"lg" are lockup sizes (a wordmark sits alongside the glyph
|
|
47
|
+
* elsewhere in the same surface — see MARK_CAP_RATIO). "xl" is for a
|
|
48
|
+
* bare, standalone glyph slot with no adjacent wordmark at all (e.g. a
|
|
49
|
+
* collapsed 56px icon rail, DEV-5331) — governed by GLYPH_CANVAS_FILL
|
|
50
|
+
* instead, same rule a favicon/avatar tile follows.
|
|
51
|
+
*/
|
|
52
|
+
size?: "sm" | "lg" | "xl";
|
|
46
53
|
/** Slightly lighter tint behind image sources (e.g. dropdown rows). */
|
|
47
54
|
subtle?: boolean;
|
|
48
55
|
className?: string;
|
|
@@ -341,6 +348,20 @@ interface AppSidebarProps {
|
|
|
341
348
|
* `/studio` should NOT light up on `/studio/photo`).
|
|
342
349
|
*/
|
|
343
350
|
isActive?: (href: string, activePath: string) => boolean;
|
|
351
|
+
/**
|
|
352
|
+
* Whether the desktop rail can collapse to a 56px icon rail (DEV-5331,
|
|
353
|
+
* porting AppNavRail's click-to-pin model into the package). Default
|
|
354
|
+
* `true`. `false` hides the toggle entirely — the rail then just stays at
|
|
355
|
+
* whatever `defaultCollapsed` says, with no way for the user to change it.
|
|
356
|
+
* Mobile is never affected either way (collapse is desktop-only).
|
|
357
|
+
*/
|
|
358
|
+
collapsible?: boolean;
|
|
359
|
+
/** Initial collapsed state before any persisted value is read. Default `false`. */
|
|
360
|
+
defaultCollapsed?: boolean;
|
|
361
|
+
/** Fires with the new collapsed value whenever the user toggles the rail. */
|
|
362
|
+
onCollapsedChange?: (collapsed: boolean) => void;
|
|
363
|
+
/** localStorage key for the persisted collapsed preference. Default `"reeve-sidebar-collapsed"`. */
|
|
364
|
+
storageKey?: string;
|
|
344
365
|
/** Merged onto the desktop `<aside>` itself — there is no extra wrapping
|
|
345
366
|
* element (see the render note below on why that matters for height). */
|
|
346
367
|
className?: string;
|
|
@@ -355,7 +376,7 @@ interface AppSidebarProps {
|
|
|
355
376
|
* dependency, matching ResponsiveHeader's existing disclosure pattern in
|
|
356
377
|
* this same package.
|
|
357
378
|
*/
|
|
358
|
-
declare function AppSidebar({ ladder, modules, groupOrder, activePath, brand, topItem, menuLabel, ladderOptions, linkComponent, isActive, className, }: AppSidebarProps): react.JSX.Element;
|
|
379
|
+
declare function AppSidebar({ ladder, modules, groupOrder, activePath, brand, topItem, menuLabel, ladderOptions, linkComponent, isActive, collapsible, defaultCollapsed, onCollapsedChange, storageKey, className, }: AppSidebarProps): react.JSX.Element;
|
|
359
380
|
|
|
360
381
|
/**
|
|
361
382
|
* Minimal shape AppSidebar's fallback derivation needs from a module registry
|
|
@@ -436,6 +457,29 @@ interface FlyoutProps {
|
|
|
436
457
|
* mirrors ResponsiveHeader's `linkComponent` prop (DEV-4429) exactly.
|
|
437
458
|
*/
|
|
438
459
|
linkComponent?: ElementType;
|
|
460
|
+
/**
|
|
461
|
+
* Icon-only trigger for AppSidebar's collapsed (56px) rail (DEV-5331): the
|
|
462
|
+
* label collapses to a `sr-only` span (kept in the a11y tree) plus a native
|
|
463
|
+
* `title` tooltip — the package has no tooltip primitive and adding one
|
|
464
|
+
* (a Radix dep) is explicitly out of scope. The panel's own child rows are
|
|
465
|
+
* unaffected — they always show full labels regardless of this flag.
|
|
466
|
+
*/
|
|
467
|
+
collapsed?: boolean;
|
|
468
|
+
/**
|
|
469
|
+
* Sibling-coordination hooks (DEV-5331 fix — Matt's live review found two
|
|
470
|
+
* folded-group flyouts open + overlapping at once, since each Flyout had
|
|
471
|
+
* zero awareness of its siblings). `onOpenChange` reports every open/
|
|
472
|
+
* closed transition of THIS instance's own hover/click intent — reported
|
|
473
|
+
* even while `forceClosed` is currently suppressing it, so a parent
|
|
474
|
+
* coordinator can learn "this one wants to open" and hand it the single
|
|
475
|
+
* active slot. `forceClosed` is set by that coordinator to immediately
|
|
476
|
+
* hide THIS instance (and reset its internal hover/click state) the
|
|
477
|
+
* instant a different sibling becomes the active one. Both optional and
|
|
478
|
+
* independent of each other — omitted, Flyout is fully standalone (its
|
|
479
|
+
* own state, unaware of any siblings), exactly as before.
|
|
480
|
+
*/
|
|
481
|
+
onOpenChange?: (open: boolean) => void;
|
|
482
|
+
forceClosed?: boolean;
|
|
439
483
|
className?: string;
|
|
440
484
|
}
|
|
441
485
|
/**
|
|
@@ -473,7 +517,7 @@ interface FlyoutProps {
|
|
|
473
517
|
* than working around it. jsdom never computes real scrollable-overflow
|
|
474
518
|
* geometry, so no unit test caught this — only an actual browser render did.
|
|
475
519
|
*/
|
|
476
|
-
declare function Flyout({ label, icon, active, children, linkComponent, className }: FlyoutProps): react.JSX.Element;
|
|
520
|
+
declare function Flyout({ label, icon, active, children, linkComponent, collapsed, onOpenChange, forceClosed, className, }: FlyoutProps): react.JSX.Element;
|
|
477
521
|
|
|
478
522
|
/**
|
|
479
523
|
* ContextSwitcher (DEV-4408) — promoted out of reeve-frontend's
|
package/dist/index.d.ts
CHANGED
|
@@ -42,7 +42,14 @@ interface Brand {
|
|
|
42
42
|
declare function faviconUrl(domain: string | null | undefined): string | null;
|
|
43
43
|
interface BrandGlyphProps {
|
|
44
44
|
brand: Brand;
|
|
45
|
-
|
|
45
|
+
/**
|
|
46
|
+
* "sm"/"lg" are lockup sizes (a wordmark sits alongside the glyph
|
|
47
|
+
* elsewhere in the same surface — see MARK_CAP_RATIO). "xl" is for a
|
|
48
|
+
* bare, standalone glyph slot with no adjacent wordmark at all (e.g. a
|
|
49
|
+
* collapsed 56px icon rail, DEV-5331) — governed by GLYPH_CANVAS_FILL
|
|
50
|
+
* instead, same rule a favicon/avatar tile follows.
|
|
51
|
+
*/
|
|
52
|
+
size?: "sm" | "lg" | "xl";
|
|
46
53
|
/** Slightly lighter tint behind image sources (e.g. dropdown rows). */
|
|
47
54
|
subtle?: boolean;
|
|
48
55
|
className?: string;
|
|
@@ -341,6 +348,20 @@ interface AppSidebarProps {
|
|
|
341
348
|
* `/studio` should NOT light up on `/studio/photo`).
|
|
342
349
|
*/
|
|
343
350
|
isActive?: (href: string, activePath: string) => boolean;
|
|
351
|
+
/**
|
|
352
|
+
* Whether the desktop rail can collapse to a 56px icon rail (DEV-5331,
|
|
353
|
+
* porting AppNavRail's click-to-pin model into the package). Default
|
|
354
|
+
* `true`. `false` hides the toggle entirely — the rail then just stays at
|
|
355
|
+
* whatever `defaultCollapsed` says, with no way for the user to change it.
|
|
356
|
+
* Mobile is never affected either way (collapse is desktop-only).
|
|
357
|
+
*/
|
|
358
|
+
collapsible?: boolean;
|
|
359
|
+
/** Initial collapsed state before any persisted value is read. Default `false`. */
|
|
360
|
+
defaultCollapsed?: boolean;
|
|
361
|
+
/** Fires with the new collapsed value whenever the user toggles the rail. */
|
|
362
|
+
onCollapsedChange?: (collapsed: boolean) => void;
|
|
363
|
+
/** localStorage key for the persisted collapsed preference. Default `"reeve-sidebar-collapsed"`. */
|
|
364
|
+
storageKey?: string;
|
|
344
365
|
/** Merged onto the desktop `<aside>` itself — there is no extra wrapping
|
|
345
366
|
* element (see the render note below on why that matters for height). */
|
|
346
367
|
className?: string;
|
|
@@ -355,7 +376,7 @@ interface AppSidebarProps {
|
|
|
355
376
|
* dependency, matching ResponsiveHeader's existing disclosure pattern in
|
|
356
377
|
* this same package.
|
|
357
378
|
*/
|
|
358
|
-
declare function AppSidebar({ ladder, modules, groupOrder, activePath, brand, topItem, menuLabel, ladderOptions, linkComponent, isActive, className, }: AppSidebarProps): react.JSX.Element;
|
|
379
|
+
declare function AppSidebar({ ladder, modules, groupOrder, activePath, brand, topItem, menuLabel, ladderOptions, linkComponent, isActive, collapsible, defaultCollapsed, onCollapsedChange, storageKey, className, }: AppSidebarProps): react.JSX.Element;
|
|
359
380
|
|
|
360
381
|
/**
|
|
361
382
|
* Minimal shape AppSidebar's fallback derivation needs from a module registry
|
|
@@ -436,6 +457,29 @@ interface FlyoutProps {
|
|
|
436
457
|
* mirrors ResponsiveHeader's `linkComponent` prop (DEV-4429) exactly.
|
|
437
458
|
*/
|
|
438
459
|
linkComponent?: ElementType;
|
|
460
|
+
/**
|
|
461
|
+
* Icon-only trigger for AppSidebar's collapsed (56px) rail (DEV-5331): the
|
|
462
|
+
* label collapses to a `sr-only` span (kept in the a11y tree) plus a native
|
|
463
|
+
* `title` tooltip — the package has no tooltip primitive and adding one
|
|
464
|
+
* (a Radix dep) is explicitly out of scope. The panel's own child rows are
|
|
465
|
+
* unaffected — they always show full labels regardless of this flag.
|
|
466
|
+
*/
|
|
467
|
+
collapsed?: boolean;
|
|
468
|
+
/**
|
|
469
|
+
* Sibling-coordination hooks (DEV-5331 fix — Matt's live review found two
|
|
470
|
+
* folded-group flyouts open + overlapping at once, since each Flyout had
|
|
471
|
+
* zero awareness of its siblings). `onOpenChange` reports every open/
|
|
472
|
+
* closed transition of THIS instance's own hover/click intent — reported
|
|
473
|
+
* even while `forceClosed` is currently suppressing it, so a parent
|
|
474
|
+
* coordinator can learn "this one wants to open" and hand it the single
|
|
475
|
+
* active slot. `forceClosed` is set by that coordinator to immediately
|
|
476
|
+
* hide THIS instance (and reset its internal hover/click state) the
|
|
477
|
+
* instant a different sibling becomes the active one. Both optional and
|
|
478
|
+
* independent of each other — omitted, Flyout is fully standalone (its
|
|
479
|
+
* own state, unaware of any siblings), exactly as before.
|
|
480
|
+
*/
|
|
481
|
+
onOpenChange?: (open: boolean) => void;
|
|
482
|
+
forceClosed?: boolean;
|
|
439
483
|
className?: string;
|
|
440
484
|
}
|
|
441
485
|
/**
|
|
@@ -473,7 +517,7 @@ interface FlyoutProps {
|
|
|
473
517
|
* than working around it. jsdom never computes real scrollable-overflow
|
|
474
518
|
* geometry, so no unit test caught this — only an actual browser render did.
|
|
475
519
|
*/
|
|
476
|
-
declare function Flyout({ label, icon, active, children, linkComponent, className }: FlyoutProps): react.JSX.Element;
|
|
520
|
+
declare function Flyout({ label, icon, active, children, linkComponent, collapsed, onOpenChange, forceClosed, className, }: FlyoutProps): react.JSX.Element;
|
|
477
521
|
|
|
478
522
|
/**
|
|
479
523
|
* ContextSwitcher (DEV-4408) — promoted out of reeve-frontend's
|
package/dist/index.js
CHANGED
|
@@ -90,6 +90,7 @@ var WORDMARK_WEIGHT = 800;
|
|
|
90
90
|
var WORDMARK_WEIGHT_CLASS = "font-extrabold";
|
|
91
91
|
var WORDMARK_TEXT_LG = "text-base";
|
|
92
92
|
var MONOGRAM_TEXT_LG = "text-[17px]";
|
|
93
|
+
var MONOGRAM_TEXT_XL = "text-[26px]";
|
|
93
94
|
var LOCKUP_GAP = "gap-2";
|
|
94
95
|
var LOCKUP_ROW = "flex items-baseline";
|
|
95
96
|
var GLYPH_CANVAS_FILL = 0.65;
|
|
@@ -129,9 +130,9 @@ function faviconUrl(domain) {
|
|
|
129
130
|
return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(host)}&sz=128`;
|
|
130
131
|
}
|
|
131
132
|
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;
|
|
133
|
+
const box = size === "xl" ? "h-10 w-10" : size === "lg" ? "h-6 w-6" : "h-5 w-5";
|
|
134
|
+
const img = size === "xl" ? "h-7 w-7" : size === "lg" ? "h-4 w-4" : "h-3.5 w-3.5";
|
|
135
|
+
const dim = size === "xl" ? 28 : size === "lg" ? 16 : 14;
|
|
135
136
|
const candidates = [];
|
|
136
137
|
if (brand.favicon) candidates.push(brand.favicon);
|
|
137
138
|
if (brand.logo) candidates.push(brand.logo);
|
|
@@ -184,8 +185,10 @@ function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
|
|
|
184
185
|
// DEV-5139 rule 2: at "lg" the letter runs 17px = the text-base
|
|
185
186
|
// wordmark (16px) × MARK_CAP_RATIO (1.08) — a lone round glyph at
|
|
186
187
|
// exact parity optically reads small. "sm" has no wordmark partner
|
|
187
|
-
// in its surfaces;
|
|
188
|
-
|
|
188
|
+
// in its surfaces; unchanged pending a lockup audit there. "xl"
|
|
189
|
+
// (DEV-5331) is a standalone glyph with no wordmark at all — rule 5
|
|
190
|
+
// (GLYPH_CANVAS_FILL) governs it instead of rule 2.
|
|
191
|
+
size === "xl" ? MONOGRAM_TEXT_XL : size === "lg" ? MONOGRAM_TEXT_LG : "text-[11px]",
|
|
189
192
|
box,
|
|
190
193
|
className
|
|
191
194
|
),
|
|
@@ -752,11 +755,22 @@ function useLadderRung(ladder, containerRef, options = {}) {
|
|
|
752
755
|
var import_react7 = require("react");
|
|
753
756
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
754
757
|
var PANEL_GAP = 4;
|
|
755
|
-
function Flyout({
|
|
758
|
+
function Flyout({
|
|
759
|
+
label,
|
|
760
|
+
icon,
|
|
761
|
+
active,
|
|
762
|
+
children,
|
|
763
|
+
linkComponent,
|
|
764
|
+
collapsed = false,
|
|
765
|
+
onOpenChange,
|
|
766
|
+
forceClosed = false,
|
|
767
|
+
className
|
|
768
|
+
}) {
|
|
756
769
|
const [hoverOpen, setHoverOpen] = (0, import_react7.useState)(false);
|
|
757
770
|
const [clickOpen, setClickOpen] = (0, import_react7.useState)(false);
|
|
758
771
|
const [panelPosition, setPanelPosition] = (0, import_react7.useState)(null);
|
|
759
|
-
const
|
|
772
|
+
const wantsOpen = hoverOpen || clickOpen;
|
|
773
|
+
const open = wantsOpen && !forceClosed;
|
|
760
774
|
const panelId = (0, import_react7.useId)();
|
|
761
775
|
const triggerRef = (0, import_react7.useRef)(null);
|
|
762
776
|
const panelRef = (0, import_react7.useRef)(null);
|
|
@@ -768,6 +782,17 @@ function Flyout({ label, icon, active, children, linkComponent, className }) {
|
|
|
768
782
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
769
783
|
if (rect) setPanelPosition({ top: rect.top, left: rect.right + PANEL_GAP });
|
|
770
784
|
}, []);
|
|
785
|
+
const onOpenChangeRef = (0, import_react7.useRef)(onOpenChange);
|
|
786
|
+
onOpenChangeRef.current = onOpenChange;
|
|
787
|
+
(0, import_react7.useEffect)(() => {
|
|
788
|
+
onOpenChangeRef.current?.(wantsOpen);
|
|
789
|
+
}, [wantsOpen]);
|
|
790
|
+
(0, import_react7.useEffect)(() => {
|
|
791
|
+
if (forceClosed) {
|
|
792
|
+
setHoverOpen(false);
|
|
793
|
+
setClickOpen(false);
|
|
794
|
+
}
|
|
795
|
+
}, [forceClosed]);
|
|
771
796
|
(0, import_react7.useLayoutEffect)(() => {
|
|
772
797
|
if (open) reposition();
|
|
773
798
|
}, [open, reposition]);
|
|
@@ -808,9 +833,11 @@ function Flyout({ label, icon, active, children, linkComponent, className }) {
|
|
|
808
833
|
"data-testid": "ladder-flyout-trigger",
|
|
809
834
|
"aria-expanded": open,
|
|
810
835
|
"aria-controls": panelId,
|
|
836
|
+
title: collapsed ? label : void 0,
|
|
811
837
|
onClick: () => setClickOpen((v) => !v),
|
|
812
838
|
className: cn(
|
|
813
839
|
"group flex w-full items-center gap-2.5 rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
840
|
+
collapsed && "justify-center px-0",
|
|
814
841
|
active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
|
|
815
842
|
),
|
|
816
843
|
children: [
|
|
@@ -822,7 +849,7 @@ function Flyout({ label, icon, active, children, linkComponent, className }) {
|
|
|
822
849
|
"aria-hidden": "true"
|
|
823
850
|
}
|
|
824
851
|
),
|
|
825
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "truncate", children: label })
|
|
852
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: cn("truncate", collapsed && "sr-only"), children: label })
|
|
826
853
|
]
|
|
827
854
|
}
|
|
828
855
|
),
|
|
@@ -873,6 +900,9 @@ function Flyout({ label, icon, active, children, linkComponent, className }) {
|
|
|
873
900
|
|
|
874
901
|
// src/app-sidebar.tsx
|
|
875
902
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
903
|
+
var DEFAULT_COLLAPSE_STORAGE_KEY = "reeve-sidebar-collapsed";
|
|
904
|
+
var COLLAPSED_WIDTH_CLASS = "w-14";
|
|
905
|
+
var EXPANDED_WIDTH_CLASS = "w-64";
|
|
876
906
|
function isHrefActive(href, activePath) {
|
|
877
907
|
return activePath === href || activePath.startsWith(`${href}/`);
|
|
878
908
|
}
|
|
@@ -882,7 +912,8 @@ function NavLink({
|
|
|
882
912
|
icon,
|
|
883
913
|
label,
|
|
884
914
|
testId,
|
|
885
|
-
linkComponent
|
|
915
|
+
linkComponent,
|
|
916
|
+
collapsed = false
|
|
886
917
|
}) {
|
|
887
918
|
const Icon = resolveIcon(icon);
|
|
888
919
|
const LinkEl = linkComponent ?? "a";
|
|
@@ -892,8 +923,10 @@ function NavLink({
|
|
|
892
923
|
href,
|
|
893
924
|
"data-testid": testId,
|
|
894
925
|
"aria-current": active ? "page" : void 0,
|
|
926
|
+
title: collapsed ? label : void 0,
|
|
895
927
|
className: cn(
|
|
896
928
|
"group flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
929
|
+
collapsed && "justify-center px-0",
|
|
897
930
|
active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
|
|
898
931
|
),
|
|
899
932
|
children: [
|
|
@@ -905,7 +938,7 @@ function NavLink({
|
|
|
905
938
|
"aria-hidden": "true"
|
|
906
939
|
}
|
|
907
940
|
),
|
|
908
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "truncate", children: label })
|
|
941
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: cn("truncate", collapsed && "sr-only"), children: label })
|
|
909
942
|
]
|
|
910
943
|
}
|
|
911
944
|
);
|
|
@@ -921,16 +954,54 @@ function AppSidebar({
|
|
|
921
954
|
ladderOptions,
|
|
922
955
|
linkComponent,
|
|
923
956
|
isActive,
|
|
957
|
+
collapsible = true,
|
|
958
|
+
defaultCollapsed = false,
|
|
959
|
+
onCollapsedChange,
|
|
960
|
+
storageKey = DEFAULT_COLLAPSE_STORAGE_KEY,
|
|
924
961
|
className
|
|
925
962
|
}) {
|
|
926
963
|
const isActiveFn = isActive ?? isHrefActive;
|
|
927
964
|
const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINTS.md}px)`);
|
|
965
|
+
const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
|
|
928
966
|
const [mobileOpen, setMobileOpen] = (0, import_react8.useState)(false);
|
|
929
967
|
const containerRef = (0, import_react8.useRef)(null);
|
|
930
968
|
const drawerId = (0, import_react8.useId)();
|
|
931
969
|
const mobileTriggerRef = (0, import_react8.useRef)(null);
|
|
932
970
|
const mobileDrawerRef = (0, import_react8.useRef)(null);
|
|
933
971
|
const mobileCloseRef = (0, import_react8.useRef)(null);
|
|
972
|
+
const [collapsed, setCollapsed] = (0, import_react8.useState)(defaultCollapsed);
|
|
973
|
+
const [collapseHydrated, setCollapseHydrated] = (0, import_react8.useState)(false);
|
|
974
|
+
(0, import_react8.useEffect)(() => {
|
|
975
|
+
if (!collapsible) return;
|
|
976
|
+
try {
|
|
977
|
+
const stored = localStorage.getItem(storageKey);
|
|
978
|
+
if (stored === "true") setCollapsed(true);
|
|
979
|
+
else if (stored === "false") setCollapsed(false);
|
|
980
|
+
} catch {
|
|
981
|
+
}
|
|
982
|
+
setCollapseHydrated(true);
|
|
983
|
+
}, []);
|
|
984
|
+
(0, import_react8.useEffect)(() => {
|
|
985
|
+
if (!collapsible || !collapseHydrated) return;
|
|
986
|
+
try {
|
|
987
|
+
localStorage.setItem(storageKey, String(collapsed));
|
|
988
|
+
} catch {
|
|
989
|
+
}
|
|
990
|
+
}, [collapsed, collapseHydrated, collapsible, storageKey]);
|
|
991
|
+
function handleToggleCollapsed() {
|
|
992
|
+
setCollapsed((prev) => {
|
|
993
|
+
const next = !prev;
|
|
994
|
+
onCollapsedChange?.(next);
|
|
995
|
+
return next;
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
const [openFlyoutId, setOpenFlyoutId] = (0, import_react8.useState)(null);
|
|
999
|
+
const handleFlyoutOpenChange = (0, import_react8.useCallback)((id, isOpen) => {
|
|
1000
|
+
setOpenFlyoutId((current) => {
|
|
1001
|
+
if (isOpen) return id;
|
|
1002
|
+
return current === id ? null : current;
|
|
1003
|
+
});
|
|
1004
|
+
}, []);
|
|
934
1005
|
const moduleMap = (0, import_react8.useMemo)(() => new Map(modules.map((m) => [m.moduleId, m])), [modules]);
|
|
935
1006
|
const registryForLadder = (0, import_react8.useMemo)(
|
|
936
1007
|
() => modules.map((m) => ({ moduleId: m.moduleId, group: m.group })),
|
|
@@ -952,7 +1023,7 @@ function AppSidebar({
|
|
|
952
1023
|
triggerRef: mobileTriggerRef,
|
|
953
1024
|
initialFocusRef: mobileCloseRef
|
|
954
1025
|
});
|
|
955
|
-
function renderAppItem(item, testIdPrefix) {
|
|
1026
|
+
function renderAppItem(item, testIdPrefix, collapsedRow = false) {
|
|
956
1027
|
const def = moduleMap.get(item.moduleId);
|
|
957
1028
|
if (!def) return null;
|
|
958
1029
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
@@ -963,12 +1034,13 @@ function AppSidebar({
|
|
|
963
1034
|
icon: def.icon,
|
|
964
1035
|
label: def.label,
|
|
965
1036
|
testId: `${testIdPrefix}-${item.moduleId}`,
|
|
966
|
-
linkComponent
|
|
1037
|
+
linkComponent,
|
|
1038
|
+
collapsed: collapsedRow
|
|
967
1039
|
},
|
|
968
1040
|
item.moduleId
|
|
969
1041
|
);
|
|
970
1042
|
}
|
|
971
|
-
function renderGroupItem(item) {
|
|
1043
|
+
function renderGroupItem(item, collapsedRow = false) {
|
|
972
1044
|
const children = item.children.map((moduleId) => {
|
|
973
1045
|
const def = moduleMap.get(moduleId);
|
|
974
1046
|
const href = def?.href ?? "#";
|
|
@@ -987,26 +1059,47 @@ function AppSidebar({
|
|
|
987
1059
|
icon: item.icon,
|
|
988
1060
|
active,
|
|
989
1061
|
children,
|
|
990
|
-
linkComponent
|
|
1062
|
+
linkComponent,
|
|
1063
|
+
collapsed: collapsedRow,
|
|
1064
|
+
forceClosed: openFlyoutId !== null && openFlyoutId !== item.id,
|
|
1065
|
+
onOpenChange: (isOpen) => handleFlyoutOpenChange(item.id, isOpen)
|
|
991
1066
|
},
|
|
992
1067
|
item.id
|
|
993
1068
|
);
|
|
994
1069
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
{
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1070
|
+
function brandBlockFor(collapsedMode) {
|
|
1071
|
+
if (!brand) return null;
|
|
1072
|
+
if (collapsedMode) {
|
|
1073
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
1074
|
+
"a",
|
|
1075
|
+
{
|
|
1076
|
+
href: brand.href,
|
|
1077
|
+
"data-testid": "app-sidebar-brand",
|
|
1078
|
+
title: brand.label,
|
|
1079
|
+
className: "flex items-center justify-center border-b border-border px-1 py-4",
|
|
1080
|
+
children: [
|
|
1081
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "sr-only", children: brand.label }),
|
|
1082
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BrandGlyph, { brand: { name: brand.label }, size: "xl" })
|
|
1083
|
+
]
|
|
1084
|
+
}
|
|
1085
|
+
);
|
|
1005
1086
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1087
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
1088
|
+
"a",
|
|
1089
|
+
{
|
|
1090
|
+
href: brand.href,
|
|
1091
|
+
"data-testid": "app-sidebar-brand",
|
|
1092
|
+
className: "flex items-center gap-2.5 border-b border-border px-5 py-4",
|
|
1093
|
+
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() }) }),
|
|
1095
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "truncate text-sm font-semibold tracking-tight", children: brand.label })
|
|
1096
|
+
]
|
|
1097
|
+
}
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
function topItemBlock(testId, collapsedMode = false) {
|
|
1008
1101
|
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)(
|
|
1102
|
+
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
1103
|
NavLink,
|
|
1011
1104
|
{
|
|
1012
1105
|
href: topItem.href,
|
|
@@ -1014,7 +1107,8 @@ function AppSidebar({
|
|
|
1014
1107
|
icon: topItem.icon,
|
|
1015
1108
|
label: topItem.label,
|
|
1016
1109
|
testId,
|
|
1017
|
-
linkComponent
|
|
1110
|
+
linkComponent,
|
|
1111
|
+
collapsed: collapsedMode
|
|
1018
1112
|
}
|
|
1019
1113
|
) });
|
|
1020
1114
|
}
|
|
@@ -1068,7 +1162,7 @@ function AppSidebar({
|
|
|
1068
1162
|
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react2.X, { className: "h-5 w-5", "aria-hidden": "true" })
|
|
1069
1163
|
}
|
|
1070
1164
|
) }),
|
|
1071
|
-
|
|
1165
|
+
brandBlockFor(false),
|
|
1072
1166
|
topItemBlock("app-sidebar-mobile-top-item"),
|
|
1073
1167
|
/* @__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
1168
|
(item) => item.type === "app" ? renderAppItem(item, "app-sidebar-mobile-navlink") : null
|
|
@@ -1081,19 +1175,47 @@ function AppSidebar({
|
|
|
1081
1175
|
"aside",
|
|
1082
1176
|
{
|
|
1083
1177
|
"data-testid": "app-sidebar-desktop",
|
|
1084
|
-
className: cn(
|
|
1178
|
+
className: cn(
|
|
1179
|
+
"hidden shrink-0 flex-col border-r border-border bg-sidebar md:flex",
|
|
1180
|
+
collapsed ? COLLAPSED_WIDTH_CLASS : EXPANDED_WIDTH_CLASS,
|
|
1181
|
+
// DEV-5331: a plain CSS width transition is enough — no new deps
|
|
1182
|
+
// for a spring/JS animation. Respect prefers-reduced-motion.
|
|
1183
|
+
!prefersReducedMotion && "transition-[width] duration-200 ease-out",
|
|
1184
|
+
className
|
|
1185
|
+
),
|
|
1085
1186
|
children: [
|
|
1086
|
-
|
|
1087
|
-
topItemBlock("app-sidebar-top-item"),
|
|
1187
|
+
brandBlockFor(collapsed),
|
|
1188
|
+
topItemBlock("app-sidebar-top-item", collapsed),
|
|
1088
1189
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1089
1190
|
"nav",
|
|
1090
1191
|
{
|
|
1091
1192
|
ref: containerRef,
|
|
1092
1193
|
"data-testid": "app-sidebar-nav",
|
|
1093
1194
|
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(
|
|
1195
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex flex-col gap-0.5", children: rung.items.map(
|
|
1196
|
+
(item) => item.type === "app" ? renderAppItem(item, "app-sidebar-navlink", collapsed) : renderGroupItem(item, collapsed)
|
|
1197
|
+
) })
|
|
1095
1198
|
}
|
|
1096
|
-
)
|
|
1199
|
+
),
|
|
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)(
|
|
1201
|
+
"button",
|
|
1202
|
+
{
|
|
1203
|
+
type: "button",
|
|
1204
|
+
"data-testid": "app-sidebar-collapse-toggle",
|
|
1205
|
+
"aria-pressed": collapsed,
|
|
1206
|
+
"aria-label": collapsed ? "Expand sidebar" : "Collapse sidebar",
|
|
1207
|
+
title: collapsed ? "Expand sidebar" : "Collapse sidebar",
|
|
1208
|
+
onClick: handleToggleCollapsed,
|
|
1209
|
+
className: cn(
|
|
1210
|
+
"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",
|
|
1211
|
+
collapsed && "justify-center px-0"
|
|
1212
|
+
),
|
|
1213
|
+
children: [
|
|
1214
|
+
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" }),
|
|
1215
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: cn("truncate", collapsed && "sr-only"), children: collapsed ? "Expand sidebar" : "Collapse sidebar" })
|
|
1216
|
+
]
|
|
1217
|
+
}
|
|
1218
|
+
) })
|
|
1097
1219
|
]
|
|
1098
1220
|
}
|
|
1099
1221
|
)
|
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 ?? "#";
|
|
@@ -933,26 +1005,47 @@ function AppSidebar({
|
|
|
933
1005
|
icon: item.icon,
|
|
934
1006
|
active,
|
|
935
1007
|
children,
|
|
936
|
-
linkComponent
|
|
1008
|
+
linkComponent,
|
|
1009
|
+
collapsed: collapsedRow,
|
|
1010
|
+
forceClosed: openFlyoutId !== null && openFlyoutId !== item.id,
|
|
1011
|
+
onOpenChange: (isOpen) => handleFlyoutOpenChange(item.id, isOpen)
|
|
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
|
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand lockup metric rules (DEV-5139) — the single source of truth for how a
|
|
3
|
+
* lettermark relates to its wordmark. Grounded in standard identity-system
|
|
4
|
+
* practice: the measurement unit for a lockup is the WORDMARK'S CAP HEIGHT,
|
|
5
|
+
* proportions are locked as ratios, and the pieces share one baseline.
|
|
6
|
+
*
|
|
7
|
+
* Rules v1:
|
|
8
|
+
* 1. Baseline lock — mark, accent dot, and wordmark sit on ONE shared
|
|
9
|
+
* baseline. Flex `items-baseline` in the lockup; never `items-center`
|
|
10
|
+
* for text marks (center-aligning boxes ≠ aligning baselines — measured
|
|
11
|
+
* 3px baseline float on cadasense, DEV-5139).
|
|
12
|
+
* 2. Cap-height parity + optical overshoot — the mark's cap height is
|
|
13
|
+
* MARK_CAP_RATIO × the wordmark's cap height. A lone round glyph
|
|
14
|
+
* (C/G/O/Q/S) optically undershoots flat caps, so exact parity reads
|
|
15
|
+
* small; 1.08 restores perceived equality (measured 0.93 before).
|
|
16
|
+
* 3. Lockup gap — mark→wordmark gap is 0.5–0.8× the wordmark cap height.
|
|
17
|
+
* 4. Clearspace — ≥1 cap height of empty space around the whole lockup
|
|
18
|
+
* (the header's own padding satisfies this; consumers embedding the
|
|
19
|
+
* lockup elsewhere own the rule).
|
|
20
|
+
* 5. Standalone glyph fill — on a square canvas (favicon, avatar), the
|
|
21
|
+
* letter's cap height is ~60–70% of the canvas edge; ghost-tile below
|
|
22
|
+
* 20px (DEV-4539).
|
|
23
|
+
* 6. Ratios are locked — scale the lockup uniformly; never resize one
|
|
24
|
+
* element of it. These constants are that lock.
|
|
25
|
+
*
|
|
26
|
+
* The generated-asset pipeline (reeve-services brand_identity
|
|
27
|
+
* svg_templates) mirrors these values — see DEV-5140. Change them together.
|
|
28
|
+
*/
|
|
29
|
+
/** Rule 2 — mark cap height ÷ wordmark cap height. */
|
|
30
|
+
declare const MARK_CAP_RATIO = 1.08;
|
|
31
|
+
/**
|
|
32
|
+
* Rule 7 (DEV-5340) — family construction weights, pixel-matched to the
|
|
33
|
+
* asset family (reeve-mark.svg, MindFortress "M." v2, reeve-services
|
|
34
|
+
* svg_templates): the mark is Black (900), the wordmark ExtraBold (800).
|
|
35
|
+
* The live-DOM lockup previously ran both at semibold (600) — Matt's
|
|
36
|
+
* measured "too thin" on cadasense. Numeric constants are the spec; the
|
|
37
|
+
* *_CLASS twins are the Tailwind application. The generated-asset pipeline
|
|
38
|
+
* mirrors the numeric values — see DEV-5140. Change them together.
|
|
39
|
+
*/
|
|
40
|
+
declare const MARK_WEIGHT = 900;
|
|
41
|
+
declare const MARK_WEIGHT_CLASS = "font-black";
|
|
42
|
+
declare const WORDMARK_WEIGHT = 800;
|
|
43
|
+
declare const WORDMARK_WEIGHT_CLASS = "font-extrabold";
|
|
44
|
+
/**
|
|
45
|
+
* Rule 2 + rule 6, applied (DEV-5340 scale-up): the header wordmark runs
|
|
46
|
+
* `text-base` (16px) — family scale; text-sm (14px) measured visibly under
|
|
47
|
+
* the MindFortress navbar mark — so the monogram letter renders at
|
|
48
|
+
* 16 × MARK_CAP_RATIO ≈ 17.3 → 17px. Same font family, and weight does not
|
|
49
|
+
* move cap height (900 vs 800 differ only in stroke), so equal font-size ⇒
|
|
50
|
+
* equal cap height; the ratio rides the font-size.
|
|
51
|
+
*/
|
|
52
|
+
declare const WORDMARK_TEXT_LG = "text-base";
|
|
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]";
|
|
65
|
+
/**
|
|
66
|
+
* Rule 3, applied: 8px gap ≈ 0.70× the wordmark's ~11.5px cap height (at
|
|
67
|
+
* text-base) — inside the 0.5–0.8 band. Named so nobody "fixes" it
|
|
68
|
+
* per-surface.
|
|
69
|
+
*/
|
|
70
|
+
declare const LOCKUP_GAP = "gap-2";
|
|
71
|
+
/**
|
|
72
|
+
* Rule 1, applied: the lockup is one locked unit — baseline-aligned inside,
|
|
73
|
+
* centered as a whole by whatever wraps it (e.g. a touch target).
|
|
74
|
+
*/
|
|
75
|
+
declare const LOCKUP_ROW = "flex items-baseline";
|
|
76
|
+
/** Rule 5 — standalone glyph cap height as a fraction of the canvas edge. */
|
|
77
|
+
declare const GLYPH_CANVAS_FILL = 0.65;
|
|
78
|
+
|
|
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 };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand lockup metric rules (DEV-5139) — the single source of truth for how a
|
|
3
|
+
* lettermark relates to its wordmark. Grounded in standard identity-system
|
|
4
|
+
* practice: the measurement unit for a lockup is the WORDMARK'S CAP HEIGHT,
|
|
5
|
+
* proportions are locked as ratios, and the pieces share one baseline.
|
|
6
|
+
*
|
|
7
|
+
* Rules v1:
|
|
8
|
+
* 1. Baseline lock — mark, accent dot, and wordmark sit on ONE shared
|
|
9
|
+
* baseline. Flex `items-baseline` in the lockup; never `items-center`
|
|
10
|
+
* for text marks (center-aligning boxes ≠ aligning baselines — measured
|
|
11
|
+
* 3px baseline float on cadasense, DEV-5139).
|
|
12
|
+
* 2. Cap-height parity + optical overshoot — the mark's cap height is
|
|
13
|
+
* MARK_CAP_RATIO × the wordmark's cap height. A lone round glyph
|
|
14
|
+
* (C/G/O/Q/S) optically undershoots flat caps, so exact parity reads
|
|
15
|
+
* small; 1.08 restores perceived equality (measured 0.93 before).
|
|
16
|
+
* 3. Lockup gap — mark→wordmark gap is 0.5–0.8× the wordmark cap height.
|
|
17
|
+
* 4. Clearspace — ≥1 cap height of empty space around the whole lockup
|
|
18
|
+
* (the header's own padding satisfies this; consumers embedding the
|
|
19
|
+
* lockup elsewhere own the rule).
|
|
20
|
+
* 5. Standalone glyph fill — on a square canvas (favicon, avatar), the
|
|
21
|
+
* letter's cap height is ~60–70% of the canvas edge; ghost-tile below
|
|
22
|
+
* 20px (DEV-4539).
|
|
23
|
+
* 6. Ratios are locked — scale the lockup uniformly; never resize one
|
|
24
|
+
* element of it. These constants are that lock.
|
|
25
|
+
*
|
|
26
|
+
* The generated-asset pipeline (reeve-services brand_identity
|
|
27
|
+
* svg_templates) mirrors these values — see DEV-5140. Change them together.
|
|
28
|
+
*/
|
|
29
|
+
/** Rule 2 — mark cap height ÷ wordmark cap height. */
|
|
30
|
+
declare const MARK_CAP_RATIO = 1.08;
|
|
31
|
+
/**
|
|
32
|
+
* Rule 7 (DEV-5340) — family construction weights, pixel-matched to the
|
|
33
|
+
* asset family (reeve-mark.svg, MindFortress "M." v2, reeve-services
|
|
34
|
+
* svg_templates): the mark is Black (900), the wordmark ExtraBold (800).
|
|
35
|
+
* The live-DOM lockup previously ran both at semibold (600) — Matt's
|
|
36
|
+
* measured "too thin" on cadasense. Numeric constants are the spec; the
|
|
37
|
+
* *_CLASS twins are the Tailwind application. The generated-asset pipeline
|
|
38
|
+
* mirrors the numeric values — see DEV-5140. Change them together.
|
|
39
|
+
*/
|
|
40
|
+
declare const MARK_WEIGHT = 900;
|
|
41
|
+
declare const MARK_WEIGHT_CLASS = "font-black";
|
|
42
|
+
declare const WORDMARK_WEIGHT = 800;
|
|
43
|
+
declare const WORDMARK_WEIGHT_CLASS = "font-extrabold";
|
|
44
|
+
/**
|
|
45
|
+
* Rule 2 + rule 6, applied (DEV-5340 scale-up): the header wordmark runs
|
|
46
|
+
* `text-base` (16px) — family scale; text-sm (14px) measured visibly under
|
|
47
|
+
* the MindFortress navbar mark — so the monogram letter renders at
|
|
48
|
+
* 16 × MARK_CAP_RATIO ≈ 17.3 → 17px. Same font family, and weight does not
|
|
49
|
+
* move cap height (900 vs 800 differ only in stroke), so equal font-size ⇒
|
|
50
|
+
* equal cap height; the ratio rides the font-size.
|
|
51
|
+
*/
|
|
52
|
+
declare const WORDMARK_TEXT_LG = "text-base";
|
|
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]";
|
|
65
|
+
/**
|
|
66
|
+
* Rule 3, applied: 8px gap ≈ 0.70× the wordmark's ~11.5px cap height (at
|
|
67
|
+
* text-base) — inside the 0.5–0.8 band. Named so nobody "fixes" it
|
|
68
|
+
* per-surface.
|
|
69
|
+
*/
|
|
70
|
+
declare const LOCKUP_GAP = "gap-2";
|
|
71
|
+
/**
|
|
72
|
+
* Rule 1, applied: the lockup is one locked unit — baseline-aligned inside,
|
|
73
|
+
* centered as a whole by whatever wraps it (e.g. a touch target).
|
|
74
|
+
*/
|
|
75
|
+
declare const LOCKUP_ROW = "flex items-baseline";
|
|
76
|
+
/** Rule 5 — standalone glyph cap height as a fraction of the canvas edge. */
|
|
77
|
+
declare const GLYPH_CANVAS_FILL = 0.65;
|
|
78
|
+
|
|
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 };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/lib/lockup.ts
|
|
21
|
+
var lockup_exports = {};
|
|
22
|
+
__export(lockup_exports, {
|
|
23
|
+
GLYPH_CANVAS_FILL: () => GLYPH_CANVAS_FILL,
|
|
24
|
+
LOCKUP_GAP: () => LOCKUP_GAP,
|
|
25
|
+
LOCKUP_ROW: () => LOCKUP_ROW,
|
|
26
|
+
MARK_CAP_RATIO: () => MARK_CAP_RATIO,
|
|
27
|
+
MARK_WEIGHT: () => MARK_WEIGHT,
|
|
28
|
+
MARK_WEIGHT_CLASS: () => MARK_WEIGHT_CLASS,
|
|
29
|
+
MONOGRAM_TEXT_LG: () => MONOGRAM_TEXT_LG,
|
|
30
|
+
MONOGRAM_TEXT_XL: () => MONOGRAM_TEXT_XL,
|
|
31
|
+
WORDMARK_TEXT_LG: () => WORDMARK_TEXT_LG,
|
|
32
|
+
WORDMARK_WEIGHT: () => WORDMARK_WEIGHT,
|
|
33
|
+
WORDMARK_WEIGHT_CLASS: () => WORDMARK_WEIGHT_CLASS
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(lockup_exports);
|
|
36
|
+
var MARK_CAP_RATIO = 1.08;
|
|
37
|
+
var MARK_WEIGHT = 900;
|
|
38
|
+
var MARK_WEIGHT_CLASS = "font-black";
|
|
39
|
+
var WORDMARK_WEIGHT = 800;
|
|
40
|
+
var WORDMARK_WEIGHT_CLASS = "font-extrabold";
|
|
41
|
+
var WORDMARK_TEXT_LG = "text-base";
|
|
42
|
+
var MONOGRAM_TEXT_LG = "text-[17px]";
|
|
43
|
+
var MONOGRAM_TEXT_XL = "text-[26px]";
|
|
44
|
+
var LOCKUP_GAP = "gap-2";
|
|
45
|
+
var LOCKUP_ROW = "flex items-baseline";
|
|
46
|
+
var GLYPH_CANVAS_FILL = 0.65;
|
|
47
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
48
|
+
0 && (module.exports = {
|
|
49
|
+
GLYPH_CANVAS_FILL,
|
|
50
|
+
LOCKUP_GAP,
|
|
51
|
+
LOCKUP_ROW,
|
|
52
|
+
MARK_CAP_RATIO,
|
|
53
|
+
MARK_WEIGHT,
|
|
54
|
+
MARK_WEIGHT_CLASS,
|
|
55
|
+
MONOGRAM_TEXT_LG,
|
|
56
|
+
MONOGRAM_TEXT_XL,
|
|
57
|
+
WORDMARK_TEXT_LG,
|
|
58
|
+
WORDMARK_WEIGHT,
|
|
59
|
+
WORDMARK_WEIGHT_CLASS
|
|
60
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/lib/lockup.ts
|
|
2
|
+
var MARK_CAP_RATIO = 1.08;
|
|
3
|
+
var MARK_WEIGHT = 900;
|
|
4
|
+
var MARK_WEIGHT_CLASS = "font-black";
|
|
5
|
+
var WORDMARK_WEIGHT = 800;
|
|
6
|
+
var WORDMARK_WEIGHT_CLASS = "font-extrabold";
|
|
7
|
+
var WORDMARK_TEXT_LG = "text-base";
|
|
8
|
+
var MONOGRAM_TEXT_LG = "text-[17px]";
|
|
9
|
+
var MONOGRAM_TEXT_XL = "text-[26px]";
|
|
10
|
+
var LOCKUP_GAP = "gap-2";
|
|
11
|
+
var LOCKUP_ROW = "flex items-baseline";
|
|
12
|
+
var GLYPH_CANVAS_FILL = 0.65;
|
|
13
|
+
export {
|
|
14
|
+
GLYPH_CANVAS_FILL,
|
|
15
|
+
LOCKUP_GAP,
|
|
16
|
+
LOCKUP_ROW,
|
|
17
|
+
MARK_CAP_RATIO,
|
|
18
|
+
MARK_WEIGHT,
|
|
19
|
+
MARK_WEIGHT_CLASS,
|
|
20
|
+
MONOGRAM_TEXT_LG,
|
|
21
|
+
MONOGRAM_TEXT_XL,
|
|
22
|
+
WORDMARK_TEXT_LG,
|
|
23
|
+
WORDMARK_WEIGHT,
|
|
24
|
+
WORDMARK_WEIGHT_CLASS
|
|
25
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meetreeve/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
"import": "./dist/tokens/index.mjs",
|
|
18
18
|
"require": "./dist/tokens/index.js"
|
|
19
19
|
},
|
|
20
|
+
"./lockup": {
|
|
21
|
+
"types": "./dist/lockup/index.d.ts",
|
|
22
|
+
"import": "./dist/lockup/index.mjs",
|
|
23
|
+
"require": "./dist/lockup/index.js"
|
|
24
|
+
},
|
|
20
25
|
"./tokens.css": "./dist/tokens.css"
|
|
21
26
|
},
|
|
22
27
|
"files": [
|