@meetreeve/ui 0.5.2 → 0.6.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 +42 -5
- package/dist/index.d.ts +42 -5
- package/dist/index.js +81 -31
- package/dist/index.mjs +79 -31
- package/dist/tokens/index.d.mts +1 -1
- package/dist/tokens/index.d.ts +1 -1
- package/dist/tokens/index.js +3 -1
- package/dist/tokens/index.mjs +3 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -80,8 +80,20 @@ declare function BrandGlyph({ brand, size, subtle, className }: BrandGlyphProps)
|
|
|
80
80
|
/** Rule 2 — mark cap height ÷ wordmark cap height. */
|
|
81
81
|
declare const MARK_CAP_RATIO = 1.08;
|
|
82
82
|
/**
|
|
83
|
-
* Rule
|
|
84
|
-
*
|
|
83
|
+
* Rule 7 (DEV-5340) — family construction weights, pixel-matched to the
|
|
84
|
+
* asset family (reeve-mark.svg, MindFortress "M." v2, reeve-services
|
|
85
|
+
* svg_templates): the mark is Black (900), the wordmark ExtraBold (800).
|
|
86
|
+
* The live-DOM lockup previously ran both at semibold (600) — Matt's
|
|
87
|
+
* measured "too thin" on cadasense. Numeric constants are the spec; the
|
|
88
|
+
* *_CLASS twins are the Tailwind application. The generated-asset pipeline
|
|
89
|
+
* mirrors the numeric values — see DEV-5140. Change them together.
|
|
90
|
+
*/
|
|
91
|
+
declare const MARK_WEIGHT = 900;
|
|
92
|
+
declare const WORDMARK_WEIGHT = 800;
|
|
93
|
+
/**
|
|
94
|
+
* Rule 3, applied: 8px gap ≈ 0.70× the wordmark's ~11.5px cap height (at
|
|
95
|
+
* text-base) — inside the 0.5–0.8 band. Named so nobody "fixes" it
|
|
96
|
+
* per-surface.
|
|
85
97
|
*/
|
|
86
98
|
declare const LOCKUP_GAP = "gap-2";
|
|
87
99
|
/**
|
|
@@ -311,6 +323,22 @@ interface AppSidebarProps {
|
|
|
311
323
|
/** Accessible label for the mobile hamburger button. */
|
|
312
324
|
menuLabel?: string;
|
|
313
325
|
ladderOptions?: UseLadderRungOptions;
|
|
326
|
+
/**
|
|
327
|
+
* Link renderer for every nav row (desktop + mobile drawer NavLink rows,
|
|
328
|
+
* and Flyout's folded-group child rows). Defaults to a bare `"a"` so the
|
|
329
|
+
* package stays framework-agnostic; consumers pass their router's link
|
|
330
|
+
* (e.g. `next/link`) for client-side navigation. Mirrors ResponsiveHeader's
|
|
331
|
+
* `linkComponent` prop (DEV-4429) exactly.
|
|
332
|
+
*/
|
|
333
|
+
linkComponent?: ElementType;
|
|
334
|
+
/**
|
|
335
|
+
* Active-route predicate driving row highlighting + `aria-current`
|
|
336
|
+
* everywhere the package would otherwise use its own naive prefix match
|
|
337
|
+
* (`activePath === href || activePath.startsWith(href + "/")`). Defaults
|
|
338
|
+
* to that same behavior — override to add exclusion semantics (e.g.
|
|
339
|
+
* `/studio` should NOT light up on `/studio/photo`).
|
|
340
|
+
*/
|
|
341
|
+
isActive?: (href: string, activePath: string) => boolean;
|
|
314
342
|
/** Merged onto the desktop `<aside>` itself — there is no extra wrapping
|
|
315
343
|
* element (see the render note below on why that matters for height). */
|
|
316
344
|
className?: string;
|
|
@@ -325,7 +353,7 @@ interface AppSidebarProps {
|
|
|
325
353
|
* dependency, matching ResponsiveHeader's existing disclosure pattern in
|
|
326
354
|
* this same package.
|
|
327
355
|
*/
|
|
328
|
-
declare function AppSidebar({ ladder, modules, groupOrder, activePath, brand, topItem, menuLabel, ladderOptions, className, }: AppSidebarProps): react.JSX.Element;
|
|
356
|
+
declare function AppSidebar({ ladder, modules, groupOrder, activePath, brand, topItem, menuLabel, ladderOptions, linkComponent, isActive, className, }: AppSidebarProps): react.JSX.Element;
|
|
329
357
|
|
|
330
358
|
/**
|
|
331
359
|
* Minimal shape AppSidebar's fallback derivation needs from a module registry
|
|
@@ -397,6 +425,15 @@ interface FlyoutProps {
|
|
|
397
425
|
* its own `active` flag. */
|
|
398
426
|
active: boolean;
|
|
399
427
|
children: FlyoutChild[];
|
|
428
|
+
/**
|
|
429
|
+
* Link renderer for the panel's child rows. Defaults to a bare `"a"` so
|
|
430
|
+
* the package stays framework-agnostic; consumers pass their router's
|
|
431
|
+
* link (e.g. `next/link`) for client-side navigation. Flyout is
|
|
432
|
+
* independently exported (see src/index.ts), so it carries its own copy
|
|
433
|
+
* of this prop rather than requiring AppSidebar as an intermediary —
|
|
434
|
+
* mirrors ResponsiveHeader's `linkComponent` prop (DEV-4429) exactly.
|
|
435
|
+
*/
|
|
436
|
+
linkComponent?: ElementType;
|
|
400
437
|
className?: string;
|
|
401
438
|
}
|
|
402
439
|
/**
|
|
@@ -434,7 +471,7 @@ interface FlyoutProps {
|
|
|
434
471
|
* than working around it. jsdom never computes real scrollable-overflow
|
|
435
472
|
* geometry, so no unit test caught this — only an actual browser render did.
|
|
436
473
|
*/
|
|
437
|
-
declare function Flyout({ label, icon, active, children, className }: FlyoutProps): react.JSX.Element;
|
|
474
|
+
declare function Flyout({ label, icon, active, children, linkComponent, className }: FlyoutProps): react.JSX.Element;
|
|
438
475
|
|
|
439
476
|
/**
|
|
440
477
|
* ContextSwitcher (DEV-4408) — promoted out of reeve-frontend's
|
|
@@ -501,4 +538,4 @@ interface ContextSwitcherProps {
|
|
|
501
538
|
}
|
|
502
539
|
declare function ContextSwitcher({ items, activeRefId, onSelect, footer, placeholder, collapsed, side, align, contentClassName, }: ContextSwitcherProps): react.JSX.Element;
|
|
503
540
|
|
|
504
|
-
export { type AppRungItem, AppSidebar, type AppSidebarBrand, type AppSidebarModuleDef, type AppSidebarProps, type AppSidebarTopItem, type Brand, BrandGlyph, type BrandGlyphProps, type ContextOrgGroup, ContextSwitcher, type ContextSwitcherItem, type ContextSwitcherProps, type FallbackModuleDef, Flyout, type FlyoutChild, type FlyoutProps, GLYPH_CANVAS_FILL, type GroupRungItem, type HeaderNavItem, LOCKUP_GAP, LOCKUP_ROW, MARK_CAP_RATIO, REEVE_ACCENT, ResponsiveHeader, type ResponsiveHeaderProps, type RungItem, type SidebarLadder, type SidebarLadderParsed, type SidebarRung, type UseLadderRungOptions, deriveFallbackLadder, faviconUrl, groupByOrg, reconcileLadder, resolveLadder, useLadderRung, useResponsivePlaceholder, zSidebarLadder };
|
|
541
|
+
export { type AppRungItem, AppSidebar, type AppSidebarBrand, type AppSidebarModuleDef, type AppSidebarProps, type AppSidebarTopItem, type Brand, BrandGlyph, type BrandGlyphProps, type ContextOrgGroup, ContextSwitcher, type ContextSwitcherItem, type ContextSwitcherProps, type FallbackModuleDef, Flyout, type FlyoutChild, type FlyoutProps, GLYPH_CANVAS_FILL, type GroupRungItem, type HeaderNavItem, LOCKUP_GAP, LOCKUP_ROW, MARK_CAP_RATIO, MARK_WEIGHT, REEVE_ACCENT, ResponsiveHeader, type ResponsiveHeaderProps, type RungItem, type SidebarLadder, type SidebarLadderParsed, type SidebarRung, type UseLadderRungOptions, WORDMARK_WEIGHT, deriveFallbackLadder, faviconUrl, groupByOrg, reconcileLadder, resolveLadder, useLadderRung, useResponsivePlaceholder, zSidebarLadder };
|
package/dist/index.d.ts
CHANGED
|
@@ -80,8 +80,20 @@ declare function BrandGlyph({ brand, size, subtle, className }: BrandGlyphProps)
|
|
|
80
80
|
/** Rule 2 — mark cap height ÷ wordmark cap height. */
|
|
81
81
|
declare const MARK_CAP_RATIO = 1.08;
|
|
82
82
|
/**
|
|
83
|
-
* Rule
|
|
84
|
-
*
|
|
83
|
+
* Rule 7 (DEV-5340) — family construction weights, pixel-matched to the
|
|
84
|
+
* asset family (reeve-mark.svg, MindFortress "M." v2, reeve-services
|
|
85
|
+
* svg_templates): the mark is Black (900), the wordmark ExtraBold (800).
|
|
86
|
+
* The live-DOM lockup previously ran both at semibold (600) — Matt's
|
|
87
|
+
* measured "too thin" on cadasense. Numeric constants are the spec; the
|
|
88
|
+
* *_CLASS twins are the Tailwind application. The generated-asset pipeline
|
|
89
|
+
* mirrors the numeric values — see DEV-5140. Change them together.
|
|
90
|
+
*/
|
|
91
|
+
declare const MARK_WEIGHT = 900;
|
|
92
|
+
declare const WORDMARK_WEIGHT = 800;
|
|
93
|
+
/**
|
|
94
|
+
* Rule 3, applied: 8px gap ≈ 0.70× the wordmark's ~11.5px cap height (at
|
|
95
|
+
* text-base) — inside the 0.5–0.8 band. Named so nobody "fixes" it
|
|
96
|
+
* per-surface.
|
|
85
97
|
*/
|
|
86
98
|
declare const LOCKUP_GAP = "gap-2";
|
|
87
99
|
/**
|
|
@@ -311,6 +323,22 @@ interface AppSidebarProps {
|
|
|
311
323
|
/** Accessible label for the mobile hamburger button. */
|
|
312
324
|
menuLabel?: string;
|
|
313
325
|
ladderOptions?: UseLadderRungOptions;
|
|
326
|
+
/**
|
|
327
|
+
* Link renderer for every nav row (desktop + mobile drawer NavLink rows,
|
|
328
|
+
* and Flyout's folded-group child rows). Defaults to a bare `"a"` so the
|
|
329
|
+
* package stays framework-agnostic; consumers pass their router's link
|
|
330
|
+
* (e.g. `next/link`) for client-side navigation. Mirrors ResponsiveHeader's
|
|
331
|
+
* `linkComponent` prop (DEV-4429) exactly.
|
|
332
|
+
*/
|
|
333
|
+
linkComponent?: ElementType;
|
|
334
|
+
/**
|
|
335
|
+
* Active-route predicate driving row highlighting + `aria-current`
|
|
336
|
+
* everywhere the package would otherwise use its own naive prefix match
|
|
337
|
+
* (`activePath === href || activePath.startsWith(href + "/")`). Defaults
|
|
338
|
+
* to that same behavior — override to add exclusion semantics (e.g.
|
|
339
|
+
* `/studio` should NOT light up on `/studio/photo`).
|
|
340
|
+
*/
|
|
341
|
+
isActive?: (href: string, activePath: string) => boolean;
|
|
314
342
|
/** Merged onto the desktop `<aside>` itself — there is no extra wrapping
|
|
315
343
|
* element (see the render note below on why that matters for height). */
|
|
316
344
|
className?: string;
|
|
@@ -325,7 +353,7 @@ interface AppSidebarProps {
|
|
|
325
353
|
* dependency, matching ResponsiveHeader's existing disclosure pattern in
|
|
326
354
|
* this same package.
|
|
327
355
|
*/
|
|
328
|
-
declare function AppSidebar({ ladder, modules, groupOrder, activePath, brand, topItem, menuLabel, ladderOptions, className, }: AppSidebarProps): react.JSX.Element;
|
|
356
|
+
declare function AppSidebar({ ladder, modules, groupOrder, activePath, brand, topItem, menuLabel, ladderOptions, linkComponent, isActive, className, }: AppSidebarProps): react.JSX.Element;
|
|
329
357
|
|
|
330
358
|
/**
|
|
331
359
|
* Minimal shape AppSidebar's fallback derivation needs from a module registry
|
|
@@ -397,6 +425,15 @@ interface FlyoutProps {
|
|
|
397
425
|
* its own `active` flag. */
|
|
398
426
|
active: boolean;
|
|
399
427
|
children: FlyoutChild[];
|
|
428
|
+
/**
|
|
429
|
+
* Link renderer for the panel's child rows. Defaults to a bare `"a"` so
|
|
430
|
+
* the package stays framework-agnostic; consumers pass their router's
|
|
431
|
+
* link (e.g. `next/link`) for client-side navigation. Flyout is
|
|
432
|
+
* independently exported (see src/index.ts), so it carries its own copy
|
|
433
|
+
* of this prop rather than requiring AppSidebar as an intermediary —
|
|
434
|
+
* mirrors ResponsiveHeader's `linkComponent` prop (DEV-4429) exactly.
|
|
435
|
+
*/
|
|
436
|
+
linkComponent?: ElementType;
|
|
400
437
|
className?: string;
|
|
401
438
|
}
|
|
402
439
|
/**
|
|
@@ -434,7 +471,7 @@ interface FlyoutProps {
|
|
|
434
471
|
* than working around it. jsdom never computes real scrollable-overflow
|
|
435
472
|
* geometry, so no unit test caught this — only an actual browser render did.
|
|
436
473
|
*/
|
|
437
|
-
declare function Flyout({ label, icon, active, children, className }: FlyoutProps): react.JSX.Element;
|
|
474
|
+
declare function Flyout({ label, icon, active, children, linkComponent, className }: FlyoutProps): react.JSX.Element;
|
|
438
475
|
|
|
439
476
|
/**
|
|
440
477
|
* ContextSwitcher (DEV-4408) — promoted out of reeve-frontend's
|
|
@@ -501,4 +538,4 @@ interface ContextSwitcherProps {
|
|
|
501
538
|
}
|
|
502
539
|
declare function ContextSwitcher({ items, activeRefId, onSelect, footer, placeholder, collapsed, side, align, contentClassName, }: ContextSwitcherProps): react.JSX.Element;
|
|
503
540
|
|
|
504
|
-
export { type AppRungItem, AppSidebar, type AppSidebarBrand, type AppSidebarModuleDef, type AppSidebarProps, type AppSidebarTopItem, type Brand, BrandGlyph, type BrandGlyphProps, type ContextOrgGroup, ContextSwitcher, type ContextSwitcherItem, type ContextSwitcherProps, type FallbackModuleDef, Flyout, type FlyoutChild, type FlyoutProps, GLYPH_CANVAS_FILL, type GroupRungItem, type HeaderNavItem, LOCKUP_GAP, LOCKUP_ROW, MARK_CAP_RATIO, REEVE_ACCENT, ResponsiveHeader, type ResponsiveHeaderProps, type RungItem, type SidebarLadder, type SidebarLadderParsed, type SidebarRung, type UseLadderRungOptions, deriveFallbackLadder, faviconUrl, groupByOrg, reconcileLadder, resolveLadder, useLadderRung, useResponsivePlaceholder, zSidebarLadder };
|
|
541
|
+
export { type AppRungItem, AppSidebar, type AppSidebarBrand, type AppSidebarModuleDef, type AppSidebarProps, type AppSidebarTopItem, type Brand, BrandGlyph, type BrandGlyphProps, type ContextOrgGroup, ContextSwitcher, type ContextSwitcherItem, type ContextSwitcherProps, type FallbackModuleDef, Flyout, type FlyoutChild, type FlyoutProps, GLYPH_CANVAS_FILL, type GroupRungItem, type HeaderNavItem, LOCKUP_GAP, LOCKUP_ROW, MARK_CAP_RATIO, MARK_WEIGHT, REEVE_ACCENT, ResponsiveHeader, type ResponsiveHeaderProps, type RungItem, type SidebarLadder, type SidebarLadderParsed, type SidebarRung, type UseLadderRungOptions, WORDMARK_WEIGHT, deriveFallbackLadder, faviconUrl, groupByOrg, reconcileLadder, resolveLadder, useLadderRung, useResponsivePlaceholder, zSidebarLadder };
|
package/dist/index.js
CHANGED
|
@@ -39,8 +39,10 @@ __export(src_exports, {
|
|
|
39
39
|
LOCKUP_GAP: () => LOCKUP_GAP,
|
|
40
40
|
LOCKUP_ROW: () => LOCKUP_ROW,
|
|
41
41
|
MARK_CAP_RATIO: () => MARK_CAP_RATIO,
|
|
42
|
+
MARK_WEIGHT: () => MARK_WEIGHT,
|
|
42
43
|
REEVE_ACCENT: () => REEVE_ACCENT,
|
|
43
44
|
ResponsiveHeader: () => ResponsiveHeader,
|
|
45
|
+
WORDMARK_WEIGHT: () => WORDMARK_WEIGHT,
|
|
44
46
|
deriveFallbackLadder: () => deriveFallbackLadder,
|
|
45
47
|
faviconUrl: () => faviconUrl,
|
|
46
48
|
groupByOrg: () => groupByOrg,
|
|
@@ -82,7 +84,12 @@ function firstInitial(name) {
|
|
|
82
84
|
|
|
83
85
|
// src/lib/lockup.ts
|
|
84
86
|
var MARK_CAP_RATIO = 1.08;
|
|
85
|
-
var
|
|
87
|
+
var MARK_WEIGHT = 900;
|
|
88
|
+
var MARK_WEIGHT_CLASS = "font-black";
|
|
89
|
+
var WORDMARK_WEIGHT = 800;
|
|
90
|
+
var WORDMARK_WEIGHT_CLASS = "font-extrabold";
|
|
91
|
+
var WORDMARK_TEXT_LG = "text-base";
|
|
92
|
+
var MONOGRAM_TEXT_LG = "text-[17px]";
|
|
86
93
|
var LOCKUP_GAP = "gap-2";
|
|
87
94
|
var LOCKUP_ROW = "flex items-baseline";
|
|
88
95
|
var GLYPH_CANVAS_FILL = 0.65;
|
|
@@ -170,11 +177,14 @@ function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
|
|
|
170
177
|
"aria-hidden": true,
|
|
171
178
|
"data-testid": "brand-monogram",
|
|
172
179
|
className: cn(
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
|
|
180
|
+
// DEV-5340 rule 7: the mark carries the family weight (Black 900)
|
|
181
|
+
// at every size — weight is family-wide, unlike the size audit.
|
|
182
|
+
"flex shrink-0 select-none items-baseline justify-center uppercase",
|
|
183
|
+
MARK_WEIGHT_CLASS,
|
|
184
|
+
// DEV-5139 rule 2: at "lg" the letter runs 17px = the text-base
|
|
185
|
+
// wordmark (16px) × MARK_CAP_RATIO (1.08) — a lone round glyph at
|
|
176
186
|
// exact parity optically reads small. "sm" has no wordmark partner
|
|
177
|
-
// in its surfaces; unchanged pending a lockup audit there.
|
|
187
|
+
// in its surfaces; SIZE unchanged pending a lockup audit there.
|
|
178
188
|
size === "lg" ? MONOGRAM_TEXT_LG : "text-[11px]",
|
|
179
189
|
box,
|
|
180
190
|
className
|
|
@@ -218,7 +228,9 @@ var MIN_SIZES = {
|
|
|
218
228
|
/** Generic minimum width/height for a secondary control (button, select), px. */
|
|
219
229
|
control: 44,
|
|
220
230
|
/** Max width of the brand wordmark before it truncates, px. */
|
|
221
|
-
|
|
231
|
+
// DEV-5340: scaled with the wordmark's text-sm→text-base bump (180 × 16/14
|
|
232
|
+
// ≈ 206) so the truncation point keeps the same character budget.
|
|
233
|
+
wordmarkMaxWidth: 205
|
|
222
234
|
};
|
|
223
235
|
var BREAKPOINTS = {
|
|
224
236
|
/** Tailwind `sm`. */
|
|
@@ -329,7 +341,13 @@ function ResponsiveHeader({
|
|
|
329
341
|
"span",
|
|
330
342
|
{
|
|
331
343
|
"data-testid": "header-wordmark",
|
|
332
|
-
className:
|
|
344
|
+
className: cn(
|
|
345
|
+
// DEV-5340: family scale + weight — text-base / ExtraBold (800),
|
|
346
|
+
// via the named lockup constants (never per-surface literals).
|
|
347
|
+
"hidden truncate text-foreground @min-[768px]:block",
|
|
348
|
+
WORDMARK_TEXT_LG,
|
|
349
|
+
WORDMARK_WEIGHT_CLASS
|
|
350
|
+
),
|
|
333
351
|
style: { maxWidth: MIN_SIZES.wordmarkMaxWidth },
|
|
334
352
|
children: brand.name
|
|
335
353
|
}
|
|
@@ -734,7 +752,7 @@ function useLadderRung(ladder, containerRef, options = {}) {
|
|
|
734
752
|
var import_react7 = require("react");
|
|
735
753
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
736
754
|
var PANEL_GAP = 4;
|
|
737
|
-
function Flyout({ label, icon, active, children, className }) {
|
|
755
|
+
function Flyout({ label, icon, active, children, linkComponent, className }) {
|
|
738
756
|
const [hoverOpen, setHoverOpen] = (0, import_react7.useState)(false);
|
|
739
757
|
const [clickOpen, setClickOpen] = (0, import_react7.useState)(false);
|
|
740
758
|
const [panelPosition, setPanelPosition] = (0, import_react7.useState)(null);
|
|
@@ -745,6 +763,7 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
745
763
|
const firstItemRef = (0, import_react7.useRef)(null);
|
|
746
764
|
const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
|
|
747
765
|
const Icon = resolveIcon(icon);
|
|
766
|
+
const LinkEl = linkComponent ?? "a";
|
|
748
767
|
const reposition = (0, import_react7.useCallback)(() => {
|
|
749
768
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
750
769
|
if (rect) setPanelPosition({ top: rect.top, left: rect.right + PANEL_GAP });
|
|
@@ -818,20 +837,32 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
818
837
|
"z-50 flex min-w-[12rem] flex-col gap-0.5 rounded-md border border-border bg-background p-2 shadow-lg",
|
|
819
838
|
!prefersReducedMotion && "transition-all duration-150 ease-out"
|
|
820
839
|
),
|
|
821
|
-
children: children.map((child, i) =>
|
|
822
|
-
"
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
840
|
+
children: children.map((child, i) => (
|
|
841
|
+
// `ref` on the first row (DEV-4391): the "focus moves into the
|
|
842
|
+
// panel on click open" behavior above needs a real
|
|
843
|
+
// HTMLAnchorElement here. A custom `linkComponent` must forward
|
|
844
|
+
// its ref to the underlying <a> for that to keep working —
|
|
845
|
+
// true for `next/link` (the intended consumer here) and for any
|
|
846
|
+
// React 19 function component (ref-as-prop, no forwardRef
|
|
847
|
+
// needed). A non-forwarding custom component on React <19 just
|
|
848
|
+
// silently loses the focus-on-open behavior (a console ref
|
|
849
|
+
// warning, not a crash) — same tradeoff any Link-injection
|
|
850
|
+
// pattern accepts; not a regression from today's plain `<a>`.
|
|
851
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
852
|
+
LinkEl,
|
|
853
|
+
{
|
|
854
|
+
ref: i === 0 ? firstItemRef : void 0,
|
|
855
|
+
href: child.href,
|
|
856
|
+
"aria-current": child.active ? "page" : void 0,
|
|
857
|
+
style: TOUCH_TARGET_STYLE,
|
|
858
|
+
className: cn(
|
|
859
|
+
"flex items-center truncate rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
860
|
+
child.active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
|
|
861
|
+
),
|
|
862
|
+
children: child.label
|
|
863
|
+
},
|
|
864
|
+
child.moduleId
|
|
865
|
+
)
|
|
835
866
|
))
|
|
836
867
|
}
|
|
837
868
|
)
|
|
@@ -842,7 +873,7 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
842
873
|
|
|
843
874
|
// src/app-sidebar.tsx
|
|
844
875
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
845
|
-
function isHrefActive(
|
|
876
|
+
function isHrefActive(href, activePath) {
|
|
846
877
|
return activePath === href || activePath.startsWith(`${href}/`);
|
|
847
878
|
}
|
|
848
879
|
function NavLink({
|
|
@@ -850,11 +881,13 @@ function NavLink({
|
|
|
850
881
|
active,
|
|
851
882
|
icon,
|
|
852
883
|
label,
|
|
853
|
-
testId
|
|
884
|
+
testId,
|
|
885
|
+
linkComponent
|
|
854
886
|
}) {
|
|
855
887
|
const Icon = resolveIcon(icon);
|
|
888
|
+
const LinkEl = linkComponent ?? "a";
|
|
856
889
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
857
|
-
|
|
890
|
+
LinkEl,
|
|
858
891
|
{
|
|
859
892
|
href,
|
|
860
893
|
"data-testid": testId,
|
|
@@ -886,8 +919,11 @@ function AppSidebar({
|
|
|
886
919
|
topItem,
|
|
887
920
|
menuLabel = "Menu",
|
|
888
921
|
ladderOptions,
|
|
922
|
+
linkComponent,
|
|
923
|
+
isActive,
|
|
889
924
|
className
|
|
890
925
|
}) {
|
|
926
|
+
const isActiveFn = isActive ?? isHrefActive;
|
|
891
927
|
const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINTS.md}px)`);
|
|
892
928
|
const [mobileOpen, setMobileOpen] = (0, import_react8.useState)(false);
|
|
893
929
|
const containerRef = (0, import_react8.useRef)(null);
|
|
@@ -923,10 +959,11 @@ function AppSidebar({
|
|
|
923
959
|
NavLink,
|
|
924
960
|
{
|
|
925
961
|
href: def.href,
|
|
926
|
-
active:
|
|
962
|
+
active: isActiveFn(def.href, activePath),
|
|
927
963
|
icon: def.icon,
|
|
928
964
|
label: def.label,
|
|
929
|
-
testId: `${testIdPrefix}-${item.moduleId}
|
|
965
|
+
testId: `${testIdPrefix}-${item.moduleId}`,
|
|
966
|
+
linkComponent
|
|
930
967
|
},
|
|
931
968
|
item.moduleId
|
|
932
969
|
);
|
|
@@ -939,11 +976,21 @@ function AppSidebar({
|
|
|
939
976
|
moduleId,
|
|
940
977
|
label: def?.label ?? moduleId,
|
|
941
978
|
href,
|
|
942
|
-
active:
|
|
979
|
+
active: isActiveFn(href, activePath)
|
|
943
980
|
};
|
|
944
981
|
});
|
|
945
982
|
const active = children.some((c) => c.active);
|
|
946
|
-
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
983
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
984
|
+
Flyout,
|
|
985
|
+
{
|
|
986
|
+
label: item.label,
|
|
987
|
+
icon: item.icon,
|
|
988
|
+
active,
|
|
989
|
+
children,
|
|
990
|
+
linkComponent
|
|
991
|
+
},
|
|
992
|
+
item.id
|
|
993
|
+
);
|
|
947
994
|
}
|
|
948
995
|
const brandBlock = brand && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
949
996
|
"a",
|
|
@@ -963,10 +1010,11 @@ function AppSidebar({
|
|
|
963
1010
|
NavLink,
|
|
964
1011
|
{
|
|
965
1012
|
href: topItem.href,
|
|
966
|
-
active:
|
|
1013
|
+
active: isActiveFn(topItem.href, activePath),
|
|
967
1014
|
icon: topItem.icon,
|
|
968
1015
|
label: topItem.label,
|
|
969
|
-
testId
|
|
1016
|
+
testId,
|
|
1017
|
+
linkComponent
|
|
970
1018
|
}
|
|
971
1019
|
) });
|
|
972
1020
|
}
|
|
@@ -1235,8 +1283,10 @@ function ContextSwitcher({
|
|
|
1235
1283
|
LOCKUP_GAP,
|
|
1236
1284
|
LOCKUP_ROW,
|
|
1237
1285
|
MARK_CAP_RATIO,
|
|
1286
|
+
MARK_WEIGHT,
|
|
1238
1287
|
REEVE_ACCENT,
|
|
1239
1288
|
ResponsiveHeader,
|
|
1289
|
+
WORDMARK_WEIGHT,
|
|
1240
1290
|
deriveFallbackLadder,
|
|
1241
1291
|
faviconUrl,
|
|
1242
1292
|
groupByOrg,
|
package/dist/index.mjs
CHANGED
|
@@ -30,7 +30,12 @@ function firstInitial(name) {
|
|
|
30
30
|
|
|
31
31
|
// src/lib/lockup.ts
|
|
32
32
|
var MARK_CAP_RATIO = 1.08;
|
|
33
|
-
var
|
|
33
|
+
var MARK_WEIGHT = 900;
|
|
34
|
+
var MARK_WEIGHT_CLASS = "font-black";
|
|
35
|
+
var WORDMARK_WEIGHT = 800;
|
|
36
|
+
var WORDMARK_WEIGHT_CLASS = "font-extrabold";
|
|
37
|
+
var WORDMARK_TEXT_LG = "text-base";
|
|
38
|
+
var MONOGRAM_TEXT_LG = "text-[17px]";
|
|
34
39
|
var LOCKUP_GAP = "gap-2";
|
|
35
40
|
var LOCKUP_ROW = "flex items-baseline";
|
|
36
41
|
var GLYPH_CANVAS_FILL = 0.65;
|
|
@@ -118,11 +123,14 @@ function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
|
|
|
118
123
|
"aria-hidden": true,
|
|
119
124
|
"data-testid": "brand-monogram",
|
|
120
125
|
className: cn(
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
|
|
126
|
+
// DEV-5340 rule 7: the mark carries the family weight (Black 900)
|
|
127
|
+
// at every size — weight is family-wide, unlike the size audit.
|
|
128
|
+
"flex shrink-0 select-none items-baseline justify-center uppercase",
|
|
129
|
+
MARK_WEIGHT_CLASS,
|
|
130
|
+
// DEV-5139 rule 2: at "lg" the letter runs 17px = the text-base
|
|
131
|
+
// wordmark (16px) × MARK_CAP_RATIO (1.08) — a lone round glyph at
|
|
124
132
|
// exact parity optically reads small. "sm" has no wordmark partner
|
|
125
|
-
// in its surfaces; unchanged pending a lockup audit there.
|
|
133
|
+
// in its surfaces; SIZE unchanged pending a lockup audit there.
|
|
126
134
|
size === "lg" ? MONOGRAM_TEXT_LG : "text-[11px]",
|
|
127
135
|
box,
|
|
128
136
|
className
|
|
@@ -166,7 +174,9 @@ var MIN_SIZES = {
|
|
|
166
174
|
/** Generic minimum width/height for a secondary control (button, select), px. */
|
|
167
175
|
control: 44,
|
|
168
176
|
/** Max width of the brand wordmark before it truncates, px. */
|
|
169
|
-
|
|
177
|
+
// DEV-5340: scaled with the wordmark's text-sm→text-base bump (180 × 16/14
|
|
178
|
+
// ≈ 206) so the truncation point keeps the same character budget.
|
|
179
|
+
wordmarkMaxWidth: 205
|
|
170
180
|
};
|
|
171
181
|
var BREAKPOINTS = {
|
|
172
182
|
/** Tailwind `sm`. */
|
|
@@ -277,7 +287,13 @@ function ResponsiveHeader({
|
|
|
277
287
|
"span",
|
|
278
288
|
{
|
|
279
289
|
"data-testid": "header-wordmark",
|
|
280
|
-
className:
|
|
290
|
+
className: cn(
|
|
291
|
+
// DEV-5340: family scale + weight — text-base / ExtraBold (800),
|
|
292
|
+
// via the named lockup constants (never per-surface literals).
|
|
293
|
+
"hidden truncate text-foreground @min-[768px]:block",
|
|
294
|
+
WORDMARK_TEXT_LG,
|
|
295
|
+
WORDMARK_WEIGHT_CLASS
|
|
296
|
+
),
|
|
281
297
|
style: { maxWidth: MIN_SIZES.wordmarkMaxWidth },
|
|
282
298
|
children: brand.name
|
|
283
299
|
}
|
|
@@ -682,7 +698,7 @@ function useLadderRung(ladder, containerRef, options = {}) {
|
|
|
682
698
|
import { useCallback as useCallback2, useEffect as useEffect4, useId as useId2, useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState5 } from "react";
|
|
683
699
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
684
700
|
var PANEL_GAP = 4;
|
|
685
|
-
function Flyout({ label, icon, active, children, className }) {
|
|
701
|
+
function Flyout({ label, icon, active, children, linkComponent, className }) {
|
|
686
702
|
const [hoverOpen, setHoverOpen] = useState5(false);
|
|
687
703
|
const [clickOpen, setClickOpen] = useState5(false);
|
|
688
704
|
const [panelPosition, setPanelPosition] = useState5(null);
|
|
@@ -693,6 +709,7 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
693
709
|
const firstItemRef = useRef3(null);
|
|
694
710
|
const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
|
|
695
711
|
const Icon = resolveIcon(icon);
|
|
712
|
+
const LinkEl = linkComponent ?? "a";
|
|
696
713
|
const reposition = useCallback2(() => {
|
|
697
714
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
698
715
|
if (rect) setPanelPosition({ top: rect.top, left: rect.right + PANEL_GAP });
|
|
@@ -766,20 +783,32 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
766
783
|
"z-50 flex min-w-[12rem] flex-col gap-0.5 rounded-md border border-border bg-background p-2 shadow-lg",
|
|
767
784
|
!prefersReducedMotion && "transition-all duration-150 ease-out"
|
|
768
785
|
),
|
|
769
|
-
children: children.map((child, i) =>
|
|
770
|
-
"
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
786
|
+
children: children.map((child, i) => (
|
|
787
|
+
// `ref` on the first row (DEV-4391): the "focus moves into the
|
|
788
|
+
// panel on click open" behavior above needs a real
|
|
789
|
+
// HTMLAnchorElement here. A custom `linkComponent` must forward
|
|
790
|
+
// its ref to the underlying <a> for that to keep working —
|
|
791
|
+
// true for `next/link` (the intended consumer here) and for any
|
|
792
|
+
// React 19 function component (ref-as-prop, no forwardRef
|
|
793
|
+
// needed). A non-forwarding custom component on React <19 just
|
|
794
|
+
// silently loses the focus-on-open behavior (a console ref
|
|
795
|
+
// warning, not a crash) — same tradeoff any Link-injection
|
|
796
|
+
// pattern accepts; not a regression from today's plain `<a>`.
|
|
797
|
+
/* @__PURE__ */ jsx3(
|
|
798
|
+
LinkEl,
|
|
799
|
+
{
|
|
800
|
+
ref: i === 0 ? firstItemRef : void 0,
|
|
801
|
+
href: child.href,
|
|
802
|
+
"aria-current": child.active ? "page" : void 0,
|
|
803
|
+
style: TOUCH_TARGET_STYLE,
|
|
804
|
+
className: cn(
|
|
805
|
+
"flex items-center truncate rounded-md px-3 py-1.5 text-sm transition-colors",
|
|
806
|
+
child.active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
|
|
807
|
+
),
|
|
808
|
+
children: child.label
|
|
809
|
+
},
|
|
810
|
+
child.moduleId
|
|
811
|
+
)
|
|
783
812
|
))
|
|
784
813
|
}
|
|
785
814
|
)
|
|
@@ -790,7 +819,7 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
790
819
|
|
|
791
820
|
// src/app-sidebar.tsx
|
|
792
821
|
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
793
|
-
function isHrefActive(
|
|
822
|
+
function isHrefActive(href, activePath) {
|
|
794
823
|
return activePath === href || activePath.startsWith(`${href}/`);
|
|
795
824
|
}
|
|
796
825
|
function NavLink({
|
|
@@ -798,11 +827,13 @@ function NavLink({
|
|
|
798
827
|
active,
|
|
799
828
|
icon,
|
|
800
829
|
label,
|
|
801
|
-
testId
|
|
830
|
+
testId,
|
|
831
|
+
linkComponent
|
|
802
832
|
}) {
|
|
803
833
|
const Icon = resolveIcon(icon);
|
|
834
|
+
const LinkEl = linkComponent ?? "a";
|
|
804
835
|
return /* @__PURE__ */ jsxs4(
|
|
805
|
-
|
|
836
|
+
LinkEl,
|
|
806
837
|
{
|
|
807
838
|
href,
|
|
808
839
|
"data-testid": testId,
|
|
@@ -834,8 +865,11 @@ function AppSidebar({
|
|
|
834
865
|
topItem,
|
|
835
866
|
menuLabel = "Menu",
|
|
836
867
|
ladderOptions,
|
|
868
|
+
linkComponent,
|
|
869
|
+
isActive,
|
|
837
870
|
className
|
|
838
871
|
}) {
|
|
872
|
+
const isActiveFn = isActive ?? isHrefActive;
|
|
839
873
|
const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINTS.md}px)`);
|
|
840
874
|
const [mobileOpen, setMobileOpen] = useState6(false);
|
|
841
875
|
const containerRef = useRef4(null);
|
|
@@ -871,10 +905,11 @@ function AppSidebar({
|
|
|
871
905
|
NavLink,
|
|
872
906
|
{
|
|
873
907
|
href: def.href,
|
|
874
|
-
active:
|
|
908
|
+
active: isActiveFn(def.href, activePath),
|
|
875
909
|
icon: def.icon,
|
|
876
910
|
label: def.label,
|
|
877
|
-
testId: `${testIdPrefix}-${item.moduleId}
|
|
911
|
+
testId: `${testIdPrefix}-${item.moduleId}`,
|
|
912
|
+
linkComponent
|
|
878
913
|
},
|
|
879
914
|
item.moduleId
|
|
880
915
|
);
|
|
@@ -887,11 +922,21 @@ function AppSidebar({
|
|
|
887
922
|
moduleId,
|
|
888
923
|
label: def?.label ?? moduleId,
|
|
889
924
|
href,
|
|
890
|
-
active:
|
|
925
|
+
active: isActiveFn(href, activePath)
|
|
891
926
|
};
|
|
892
927
|
});
|
|
893
928
|
const active = children.some((c) => c.active);
|
|
894
|
-
return /* @__PURE__ */ jsx4(
|
|
929
|
+
return /* @__PURE__ */ jsx4(
|
|
930
|
+
Flyout,
|
|
931
|
+
{
|
|
932
|
+
label: item.label,
|
|
933
|
+
icon: item.icon,
|
|
934
|
+
active,
|
|
935
|
+
children,
|
|
936
|
+
linkComponent
|
|
937
|
+
},
|
|
938
|
+
item.id
|
|
939
|
+
);
|
|
895
940
|
}
|
|
896
941
|
const brandBlock = brand && /* @__PURE__ */ jsxs4(
|
|
897
942
|
"a",
|
|
@@ -911,10 +956,11 @@ function AppSidebar({
|
|
|
911
956
|
NavLink,
|
|
912
957
|
{
|
|
913
958
|
href: topItem.href,
|
|
914
|
-
active:
|
|
959
|
+
active: isActiveFn(topItem.href, activePath),
|
|
915
960
|
icon: topItem.icon,
|
|
916
961
|
label: topItem.label,
|
|
917
|
-
testId
|
|
962
|
+
testId,
|
|
963
|
+
linkComponent
|
|
918
964
|
}
|
|
919
965
|
) });
|
|
920
966
|
}
|
|
@@ -1182,8 +1228,10 @@ export {
|
|
|
1182
1228
|
LOCKUP_GAP,
|
|
1183
1229
|
LOCKUP_ROW,
|
|
1184
1230
|
MARK_CAP_RATIO,
|
|
1231
|
+
MARK_WEIGHT,
|
|
1185
1232
|
REEVE_ACCENT,
|
|
1186
1233
|
ResponsiveHeader,
|
|
1234
|
+
WORDMARK_WEIGHT,
|
|
1187
1235
|
deriveFallbackLadder,
|
|
1188
1236
|
faviconUrl,
|
|
1189
1237
|
groupByOrg,
|
package/dist/tokens/index.d.mts
CHANGED
|
@@ -17,7 +17,7 @@ declare const MIN_SIZES: {
|
|
|
17
17
|
/** Generic minimum width/height for a secondary control (button, select), px. */
|
|
18
18
|
readonly control: 44;
|
|
19
19
|
/** Max width of the brand wordmark before it truncates, px. */
|
|
20
|
-
readonly wordmarkMaxWidth:
|
|
20
|
+
readonly wordmarkMaxWidth: 205;
|
|
21
21
|
};
|
|
22
22
|
declare const BREAKPOINTS: {
|
|
23
23
|
/** Tailwind `sm`. */
|
package/dist/tokens/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ declare const MIN_SIZES: {
|
|
|
17
17
|
/** Generic minimum width/height for a secondary control (button, select), px. */
|
|
18
18
|
readonly control: 44;
|
|
19
19
|
/** Max width of the brand wordmark before it truncates, px. */
|
|
20
|
-
readonly wordmarkMaxWidth:
|
|
20
|
+
readonly wordmarkMaxWidth: 205;
|
|
21
21
|
};
|
|
22
22
|
declare const BREAKPOINTS: {
|
|
23
23
|
/** Tailwind `sm`. */
|
package/dist/tokens/index.js
CHANGED
|
@@ -33,7 +33,9 @@ var MIN_SIZES = {
|
|
|
33
33
|
/** Generic minimum width/height for a secondary control (button, select), px. */
|
|
34
34
|
control: 44,
|
|
35
35
|
/** Max width of the brand wordmark before it truncates, px. */
|
|
36
|
-
|
|
36
|
+
// DEV-5340: scaled with the wordmark's text-sm→text-base bump (180 × 16/14
|
|
37
|
+
// ≈ 206) so the truncation point keeps the same character budget.
|
|
38
|
+
wordmarkMaxWidth: 205
|
|
37
39
|
};
|
|
38
40
|
var BREAKPOINTS = {
|
|
39
41
|
/** Tailwind `sm`. */
|
package/dist/tokens/index.mjs
CHANGED
|
@@ -7,7 +7,9 @@ var MIN_SIZES = {
|
|
|
7
7
|
/** Generic minimum width/height for a secondary control (button, select), px. */
|
|
8
8
|
control: 44,
|
|
9
9
|
/** Max width of the brand wordmark before it truncates, px. */
|
|
10
|
-
|
|
10
|
+
// DEV-5340: scaled with the wordmark's text-sm→text-base bump (180 × 16/14
|
|
11
|
+
// ≈ 206) so the truncation point keeps the same character budget.
|
|
12
|
+
wordmarkMaxWidth: 205
|
|
11
13
|
};
|
|
12
14
|
var BREAKPOINTS = {
|
|
13
15
|
/** Tailwind `sm`. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meetreeve/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|