@meetreeve/ui 0.5.0 → 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 +83 -3
- package/dist/index.d.ts +83 -3
- package/dist/index.js +108 -35
- package/dist/index.mjs +102 -35
- 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 +2 -1
package/dist/index.d.mts
CHANGED
|
@@ -49,6 +49,61 @@ interface BrandGlyphProps {
|
|
|
49
49
|
}
|
|
50
50
|
declare function BrandGlyph({ brand, size, subtle, className }: BrandGlyphProps): react.JSX.Element;
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Brand lockup metric rules (DEV-5139) — the single source of truth for how a
|
|
54
|
+
* lettermark relates to its wordmark. Grounded in standard identity-system
|
|
55
|
+
* practice: the measurement unit for a lockup is the WORDMARK'S CAP HEIGHT,
|
|
56
|
+
* proportions are locked as ratios, and the pieces share one baseline.
|
|
57
|
+
*
|
|
58
|
+
* Rules v1:
|
|
59
|
+
* 1. Baseline lock — mark, accent dot, and wordmark sit on ONE shared
|
|
60
|
+
* baseline. Flex `items-baseline` in the lockup; never `items-center`
|
|
61
|
+
* for text marks (center-aligning boxes ≠ aligning baselines — measured
|
|
62
|
+
* 3px baseline float on cadasense, DEV-5139).
|
|
63
|
+
* 2. Cap-height parity + optical overshoot — the mark's cap height is
|
|
64
|
+
* MARK_CAP_RATIO × the wordmark's cap height. A lone round glyph
|
|
65
|
+
* (C/G/O/Q/S) optically undershoots flat caps, so exact parity reads
|
|
66
|
+
* small; 1.08 restores perceived equality (measured 0.93 before).
|
|
67
|
+
* 3. Lockup gap — mark→wordmark gap is 0.5–0.8× the wordmark cap height.
|
|
68
|
+
* 4. Clearspace — ≥1 cap height of empty space around the whole lockup
|
|
69
|
+
* (the header's own padding satisfies this; consumers embedding the
|
|
70
|
+
* lockup elsewhere own the rule).
|
|
71
|
+
* 5. Standalone glyph fill — on a square canvas (favicon, avatar), the
|
|
72
|
+
* letter's cap height is ~60–70% of the canvas edge; ghost-tile below
|
|
73
|
+
* 20px (DEV-4539).
|
|
74
|
+
* 6. Ratios are locked — scale the lockup uniformly; never resize one
|
|
75
|
+
* element of it. These constants are that lock.
|
|
76
|
+
*
|
|
77
|
+
* The generated-asset pipeline (reeve-services brand_identity
|
|
78
|
+
* svg_templates) mirrors these values — see DEV-5140. Change them together.
|
|
79
|
+
*/
|
|
80
|
+
/** Rule 2 — mark cap height ÷ wordmark cap height. */
|
|
81
|
+
declare const MARK_CAP_RATIO = 1.08;
|
|
82
|
+
/**
|
|
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.
|
|
97
|
+
*/
|
|
98
|
+
declare const LOCKUP_GAP = "gap-2";
|
|
99
|
+
/**
|
|
100
|
+
* Rule 1, applied: the lockup is one locked unit — baseline-aligned inside,
|
|
101
|
+
* centered as a whole by whatever wraps it (e.g. a touch target).
|
|
102
|
+
*/
|
|
103
|
+
declare const LOCKUP_ROW = "flex items-baseline";
|
|
104
|
+
/** Rule 5 — standalone glyph cap height as a fraction of the canvas edge. */
|
|
105
|
+
declare const GLYPH_CANVAS_FILL = 0.65;
|
|
106
|
+
|
|
52
107
|
interface HeaderNavItem {
|
|
53
108
|
label: string;
|
|
54
109
|
href?: string;
|
|
@@ -268,6 +323,22 @@ interface AppSidebarProps {
|
|
|
268
323
|
/** Accessible label for the mobile hamburger button. */
|
|
269
324
|
menuLabel?: string;
|
|
270
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;
|
|
271
342
|
/** Merged onto the desktop `<aside>` itself — there is no extra wrapping
|
|
272
343
|
* element (see the render note below on why that matters for height). */
|
|
273
344
|
className?: string;
|
|
@@ -282,7 +353,7 @@ interface AppSidebarProps {
|
|
|
282
353
|
* dependency, matching ResponsiveHeader's existing disclosure pattern in
|
|
283
354
|
* this same package.
|
|
284
355
|
*/
|
|
285
|
-
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;
|
|
286
357
|
|
|
287
358
|
/**
|
|
288
359
|
* Minimal shape AppSidebar's fallback derivation needs from a module registry
|
|
@@ -354,6 +425,15 @@ interface FlyoutProps {
|
|
|
354
425
|
* its own `active` flag. */
|
|
355
426
|
active: boolean;
|
|
356
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;
|
|
357
437
|
className?: string;
|
|
358
438
|
}
|
|
359
439
|
/**
|
|
@@ -391,7 +471,7 @@ interface FlyoutProps {
|
|
|
391
471
|
* than working around it. jsdom never computes real scrollable-overflow
|
|
392
472
|
* geometry, so no unit test caught this — only an actual browser render did.
|
|
393
473
|
*/
|
|
394
|
-
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;
|
|
395
475
|
|
|
396
476
|
/**
|
|
397
477
|
* ContextSwitcher (DEV-4408) — promoted out of reeve-frontend's
|
|
@@ -458,4 +538,4 @@ interface ContextSwitcherProps {
|
|
|
458
538
|
}
|
|
459
539
|
declare function ContextSwitcher({ items, activeRefId, onSelect, footer, placeholder, collapsed, side, align, contentClassName, }: ContextSwitcherProps): react.JSX.Element;
|
|
460
540
|
|
|
461
|
-
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, type GroupRungItem, type HeaderNavItem, 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
|
@@ -49,6 +49,61 @@ interface BrandGlyphProps {
|
|
|
49
49
|
}
|
|
50
50
|
declare function BrandGlyph({ brand, size, subtle, className }: BrandGlyphProps): react.JSX.Element;
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Brand lockup metric rules (DEV-5139) — the single source of truth for how a
|
|
54
|
+
* lettermark relates to its wordmark. Grounded in standard identity-system
|
|
55
|
+
* practice: the measurement unit for a lockup is the WORDMARK'S CAP HEIGHT,
|
|
56
|
+
* proportions are locked as ratios, and the pieces share one baseline.
|
|
57
|
+
*
|
|
58
|
+
* Rules v1:
|
|
59
|
+
* 1. Baseline lock — mark, accent dot, and wordmark sit on ONE shared
|
|
60
|
+
* baseline. Flex `items-baseline` in the lockup; never `items-center`
|
|
61
|
+
* for text marks (center-aligning boxes ≠ aligning baselines — measured
|
|
62
|
+
* 3px baseline float on cadasense, DEV-5139).
|
|
63
|
+
* 2. Cap-height parity + optical overshoot — the mark's cap height is
|
|
64
|
+
* MARK_CAP_RATIO × the wordmark's cap height. A lone round glyph
|
|
65
|
+
* (C/G/O/Q/S) optically undershoots flat caps, so exact parity reads
|
|
66
|
+
* small; 1.08 restores perceived equality (measured 0.93 before).
|
|
67
|
+
* 3. Lockup gap — mark→wordmark gap is 0.5–0.8× the wordmark cap height.
|
|
68
|
+
* 4. Clearspace — ≥1 cap height of empty space around the whole lockup
|
|
69
|
+
* (the header's own padding satisfies this; consumers embedding the
|
|
70
|
+
* lockup elsewhere own the rule).
|
|
71
|
+
* 5. Standalone glyph fill — on a square canvas (favicon, avatar), the
|
|
72
|
+
* letter's cap height is ~60–70% of the canvas edge; ghost-tile below
|
|
73
|
+
* 20px (DEV-4539).
|
|
74
|
+
* 6. Ratios are locked — scale the lockup uniformly; never resize one
|
|
75
|
+
* element of it. These constants are that lock.
|
|
76
|
+
*
|
|
77
|
+
* The generated-asset pipeline (reeve-services brand_identity
|
|
78
|
+
* svg_templates) mirrors these values — see DEV-5140. Change them together.
|
|
79
|
+
*/
|
|
80
|
+
/** Rule 2 — mark cap height ÷ wordmark cap height. */
|
|
81
|
+
declare const MARK_CAP_RATIO = 1.08;
|
|
82
|
+
/**
|
|
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.
|
|
97
|
+
*/
|
|
98
|
+
declare const LOCKUP_GAP = "gap-2";
|
|
99
|
+
/**
|
|
100
|
+
* Rule 1, applied: the lockup is one locked unit — baseline-aligned inside,
|
|
101
|
+
* centered as a whole by whatever wraps it (e.g. a touch target).
|
|
102
|
+
*/
|
|
103
|
+
declare const LOCKUP_ROW = "flex items-baseline";
|
|
104
|
+
/** Rule 5 — standalone glyph cap height as a fraction of the canvas edge. */
|
|
105
|
+
declare const GLYPH_CANVAS_FILL = 0.65;
|
|
106
|
+
|
|
52
107
|
interface HeaderNavItem {
|
|
53
108
|
label: string;
|
|
54
109
|
href?: string;
|
|
@@ -268,6 +323,22 @@ interface AppSidebarProps {
|
|
|
268
323
|
/** Accessible label for the mobile hamburger button. */
|
|
269
324
|
menuLabel?: string;
|
|
270
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;
|
|
271
342
|
/** Merged onto the desktop `<aside>` itself — there is no extra wrapping
|
|
272
343
|
* element (see the render note below on why that matters for height). */
|
|
273
344
|
className?: string;
|
|
@@ -282,7 +353,7 @@ interface AppSidebarProps {
|
|
|
282
353
|
* dependency, matching ResponsiveHeader's existing disclosure pattern in
|
|
283
354
|
* this same package.
|
|
284
355
|
*/
|
|
285
|
-
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;
|
|
286
357
|
|
|
287
358
|
/**
|
|
288
359
|
* Minimal shape AppSidebar's fallback derivation needs from a module registry
|
|
@@ -354,6 +425,15 @@ interface FlyoutProps {
|
|
|
354
425
|
* its own `active` flag. */
|
|
355
426
|
active: boolean;
|
|
356
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;
|
|
357
437
|
className?: string;
|
|
358
438
|
}
|
|
359
439
|
/**
|
|
@@ -391,7 +471,7 @@ interface FlyoutProps {
|
|
|
391
471
|
* than working around it. jsdom never computes real scrollable-overflow
|
|
392
472
|
* geometry, so no unit test caught this — only an actual browser render did.
|
|
393
473
|
*/
|
|
394
|
-
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;
|
|
395
475
|
|
|
396
476
|
/**
|
|
397
477
|
* ContextSwitcher (DEV-4408) — promoted out of reeve-frontend's
|
|
@@ -458,4 +538,4 @@ interface ContextSwitcherProps {
|
|
|
458
538
|
}
|
|
459
539
|
declare function ContextSwitcher({ items, activeRefId, onSelect, footer, placeholder, collapsed, side, align, contentClassName, }: ContextSwitcherProps): react.JSX.Element;
|
|
460
540
|
|
|
461
|
-
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, type GroupRungItem, type HeaderNavItem, 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
|
@@ -35,8 +35,14 @@ __export(src_exports, {
|
|
|
35
35
|
BrandGlyph: () => BrandGlyph,
|
|
36
36
|
ContextSwitcher: () => ContextSwitcher,
|
|
37
37
|
Flyout: () => Flyout,
|
|
38
|
+
GLYPH_CANVAS_FILL: () => GLYPH_CANVAS_FILL,
|
|
39
|
+
LOCKUP_GAP: () => LOCKUP_GAP,
|
|
40
|
+
LOCKUP_ROW: () => LOCKUP_ROW,
|
|
41
|
+
MARK_CAP_RATIO: () => MARK_CAP_RATIO,
|
|
42
|
+
MARK_WEIGHT: () => MARK_WEIGHT,
|
|
38
43
|
REEVE_ACCENT: () => REEVE_ACCENT,
|
|
39
44
|
ResponsiveHeader: () => ResponsiveHeader,
|
|
45
|
+
WORDMARK_WEIGHT: () => WORDMARK_WEIGHT,
|
|
40
46
|
deriveFallbackLadder: () => deriveFallbackLadder,
|
|
41
47
|
faviconUrl: () => faviconUrl,
|
|
42
48
|
groupByOrg: () => groupByOrg,
|
|
@@ -49,7 +55,6 @@ __export(src_exports, {
|
|
|
49
55
|
module.exports = __toCommonJS(src_exports);
|
|
50
56
|
|
|
51
57
|
// src/brand-glyph.tsx
|
|
52
|
-
var import_image = __toESM(require("next/image"));
|
|
53
58
|
var import_react = require("react");
|
|
54
59
|
|
|
55
60
|
// src/lib/cn.ts
|
|
@@ -77,6 +82,18 @@ function firstInitial(name) {
|
|
|
77
82
|
return trimmed ? trimmed[0].toUpperCase() : "?";
|
|
78
83
|
}
|
|
79
84
|
|
|
85
|
+
// src/lib/lockup.ts
|
|
86
|
+
var MARK_CAP_RATIO = 1.08;
|
|
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]";
|
|
93
|
+
var LOCKUP_GAP = "gap-2";
|
|
94
|
+
var LOCKUP_ROW = "flex items-baseline";
|
|
95
|
+
var GLYPH_CANVAS_FILL = 0.65;
|
|
96
|
+
|
|
80
97
|
// src/lib/monogram.ts
|
|
81
98
|
var MONOGRAM_COLORS = [
|
|
82
99
|
"#7c3aed",
|
|
@@ -125,21 +142,26 @@ function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
|
|
|
125
142
|
"span",
|
|
126
143
|
{
|
|
127
144
|
className: cn(
|
|
128
|
-
|
|
145
|
+
// `self-center` opts image glyphs out of the lockup's baseline
|
|
146
|
+
// alignment (DEV-5139): an image box has no text baseline, so
|
|
147
|
+
// flexbox would seat its BOTTOM edge on the wordmark baseline and
|
|
148
|
+
// shove the icon upward. Centering preserves the pre-DEV-5139 look.
|
|
149
|
+
"flex shrink-0 items-center justify-center self-center overflow-hidden rounded",
|
|
129
150
|
// Tint needs a dark-mode counterpart or it vanishes on dark backgrounds.
|
|
130
151
|
subtle ? "bg-black/5 dark:bg-white/10" : "bg-black/10 dark:bg-white/15",
|
|
131
152
|
box,
|
|
132
153
|
className
|
|
133
154
|
),
|
|
134
155
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
135
|
-
|
|
156
|
+
"img",
|
|
136
157
|
{
|
|
137
158
|
src,
|
|
138
159
|
alt: "",
|
|
139
160
|
width: dim,
|
|
140
161
|
height: dim,
|
|
162
|
+
loading: "lazy",
|
|
163
|
+
decoding: "async",
|
|
141
164
|
"data-testid": "brand-image",
|
|
142
|
-
unoptimized: true,
|
|
143
165
|
onError: () => setFailed((prev) => new Set(prev).add(src)),
|
|
144
166
|
className: cn(img, "rounded-sm object-contain")
|
|
145
167
|
}
|
|
@@ -155,8 +177,15 @@ function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
|
|
|
155
177
|
"aria-hidden": true,
|
|
156
178
|
"data-testid": "brand-monogram",
|
|
157
179
|
className: cn(
|
|
158
|
-
|
|
159
|
-
size
|
|
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
|
|
186
|
+
// exact parity optically reads small. "sm" has no wordmark partner
|
|
187
|
+
// in its surfaces; SIZE unchanged pending a lockup audit there.
|
|
188
|
+
size === "lg" ? MONOGRAM_TEXT_LG : "text-[11px]",
|
|
160
189
|
box,
|
|
161
190
|
className
|
|
162
191
|
),
|
|
@@ -199,7 +228,9 @@ var MIN_SIZES = {
|
|
|
199
228
|
/** Generic minimum width/height for a secondary control (button, select), px. */
|
|
200
229
|
control: 44,
|
|
201
230
|
/** Max width of the brand wordmark before it truncates, px. */
|
|
202
|
-
|
|
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
|
|
203
234
|
};
|
|
204
235
|
var BREAKPOINTS = {
|
|
205
236
|
/** Tailwind `sm`. */
|
|
@@ -304,13 +335,19 @@ function ResponsiveHeader({
|
|
|
304
335
|
};
|
|
305
336
|
}, [open]);
|
|
306
337
|
const hasSecondary = Boolean(nav && nav.length > 0 || auth || themeToggle);
|
|
307
|
-
const brandLockup = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
338
|
+
const brandLockup = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { "data-testid": "header-lockup", className: cn(LOCKUP_ROW, LOCKUP_GAP), children: [
|
|
308
339
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BrandGlyph, { brand, size: "lg" }),
|
|
309
340
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
310
341
|
"span",
|
|
311
342
|
{
|
|
312
343
|
"data-testid": "header-wordmark",
|
|
313
|
-
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
|
+
),
|
|
314
351
|
style: { maxWidth: MIN_SIZES.wordmarkMaxWidth },
|
|
315
352
|
children: brand.name
|
|
316
353
|
}
|
|
@@ -335,10 +372,10 @@ function ResponsiveHeader({
|
|
|
335
372
|
"aria-label": brand.name || "Home",
|
|
336
373
|
"data-testid": "header-brand",
|
|
337
374
|
style: TOUCH_TARGET_STYLE,
|
|
338
|
-
className: "flex shrink-0 items-center
|
|
375
|
+
className: "flex shrink-0 items-center",
|
|
339
376
|
children: brandLockup
|
|
340
377
|
}
|
|
341
|
-
) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-testid": "header-brand", className: "flex shrink-0 items-center
|
|
378
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-testid": "header-brand", className: "flex shrink-0 items-center", children: brandLockup }),
|
|
342
379
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
343
380
|
"div",
|
|
344
381
|
{
|
|
@@ -715,7 +752,7 @@ function useLadderRung(ladder, containerRef, options = {}) {
|
|
|
715
752
|
var import_react7 = require("react");
|
|
716
753
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
717
754
|
var PANEL_GAP = 4;
|
|
718
|
-
function Flyout({ label, icon, active, children, className }) {
|
|
755
|
+
function Flyout({ label, icon, active, children, linkComponent, className }) {
|
|
719
756
|
const [hoverOpen, setHoverOpen] = (0, import_react7.useState)(false);
|
|
720
757
|
const [clickOpen, setClickOpen] = (0, import_react7.useState)(false);
|
|
721
758
|
const [panelPosition, setPanelPosition] = (0, import_react7.useState)(null);
|
|
@@ -726,6 +763,7 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
726
763
|
const firstItemRef = (0, import_react7.useRef)(null);
|
|
727
764
|
const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
|
|
728
765
|
const Icon = resolveIcon(icon);
|
|
766
|
+
const LinkEl = linkComponent ?? "a";
|
|
729
767
|
const reposition = (0, import_react7.useCallback)(() => {
|
|
730
768
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
731
769
|
if (rect) setPanelPosition({ top: rect.top, left: rect.right + PANEL_GAP });
|
|
@@ -799,20 +837,32 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
799
837
|
"z-50 flex min-w-[12rem] flex-col gap-0.5 rounded-md border border-border bg-background p-2 shadow-lg",
|
|
800
838
|
!prefersReducedMotion && "transition-all duration-150 ease-out"
|
|
801
839
|
),
|
|
802
|
-
children: children.map((child, i) =>
|
|
803
|
-
"
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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
|
+
)
|
|
816
866
|
))
|
|
817
867
|
}
|
|
818
868
|
)
|
|
@@ -823,7 +873,7 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
823
873
|
|
|
824
874
|
// src/app-sidebar.tsx
|
|
825
875
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
826
|
-
function isHrefActive(
|
|
876
|
+
function isHrefActive(href, activePath) {
|
|
827
877
|
return activePath === href || activePath.startsWith(`${href}/`);
|
|
828
878
|
}
|
|
829
879
|
function NavLink({
|
|
@@ -831,11 +881,13 @@ function NavLink({
|
|
|
831
881
|
active,
|
|
832
882
|
icon,
|
|
833
883
|
label,
|
|
834
|
-
testId
|
|
884
|
+
testId,
|
|
885
|
+
linkComponent
|
|
835
886
|
}) {
|
|
836
887
|
const Icon = resolveIcon(icon);
|
|
888
|
+
const LinkEl = linkComponent ?? "a";
|
|
837
889
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
838
|
-
|
|
890
|
+
LinkEl,
|
|
839
891
|
{
|
|
840
892
|
href,
|
|
841
893
|
"data-testid": testId,
|
|
@@ -867,8 +919,11 @@ function AppSidebar({
|
|
|
867
919
|
topItem,
|
|
868
920
|
menuLabel = "Menu",
|
|
869
921
|
ladderOptions,
|
|
922
|
+
linkComponent,
|
|
923
|
+
isActive,
|
|
870
924
|
className
|
|
871
925
|
}) {
|
|
926
|
+
const isActiveFn = isActive ?? isHrefActive;
|
|
872
927
|
const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINTS.md}px)`);
|
|
873
928
|
const [mobileOpen, setMobileOpen] = (0, import_react8.useState)(false);
|
|
874
929
|
const containerRef = (0, import_react8.useRef)(null);
|
|
@@ -904,10 +959,11 @@ function AppSidebar({
|
|
|
904
959
|
NavLink,
|
|
905
960
|
{
|
|
906
961
|
href: def.href,
|
|
907
|
-
active:
|
|
962
|
+
active: isActiveFn(def.href, activePath),
|
|
908
963
|
icon: def.icon,
|
|
909
964
|
label: def.label,
|
|
910
|
-
testId: `${testIdPrefix}-${item.moduleId}
|
|
965
|
+
testId: `${testIdPrefix}-${item.moduleId}`,
|
|
966
|
+
linkComponent
|
|
911
967
|
},
|
|
912
968
|
item.moduleId
|
|
913
969
|
);
|
|
@@ -920,11 +976,21 @@ function AppSidebar({
|
|
|
920
976
|
moduleId,
|
|
921
977
|
label: def?.label ?? moduleId,
|
|
922
978
|
href,
|
|
923
|
-
active:
|
|
979
|
+
active: isActiveFn(href, activePath)
|
|
924
980
|
};
|
|
925
981
|
});
|
|
926
982
|
const active = children.some((c) => c.active);
|
|
927
|
-
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
|
+
);
|
|
928
994
|
}
|
|
929
995
|
const brandBlock = brand && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
930
996
|
"a",
|
|
@@ -944,10 +1010,11 @@ function AppSidebar({
|
|
|
944
1010
|
NavLink,
|
|
945
1011
|
{
|
|
946
1012
|
href: topItem.href,
|
|
947
|
-
active:
|
|
1013
|
+
active: isActiveFn(topItem.href, activePath),
|
|
948
1014
|
icon: topItem.icon,
|
|
949
1015
|
label: topItem.label,
|
|
950
|
-
testId
|
|
1016
|
+
testId,
|
|
1017
|
+
linkComponent
|
|
951
1018
|
}
|
|
952
1019
|
) });
|
|
953
1020
|
}
|
|
@@ -1212,8 +1279,14 @@ function ContextSwitcher({
|
|
|
1212
1279
|
BrandGlyph,
|
|
1213
1280
|
ContextSwitcher,
|
|
1214
1281
|
Flyout,
|
|
1282
|
+
GLYPH_CANVAS_FILL,
|
|
1283
|
+
LOCKUP_GAP,
|
|
1284
|
+
LOCKUP_ROW,
|
|
1285
|
+
MARK_CAP_RATIO,
|
|
1286
|
+
MARK_WEIGHT,
|
|
1215
1287
|
REEVE_ACCENT,
|
|
1216
1288
|
ResponsiveHeader,
|
|
1289
|
+
WORDMARK_WEIGHT,
|
|
1217
1290
|
deriveFallbackLadder,
|
|
1218
1291
|
faviconUrl,
|
|
1219
1292
|
groupByOrg,
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/brand-glyph.tsx
|
|
4
|
-
import Image from "next/image";
|
|
5
4
|
import { useState } from "react";
|
|
6
5
|
|
|
7
6
|
// src/lib/cn.ts
|
|
@@ -29,6 +28,18 @@ function firstInitial(name) {
|
|
|
29
28
|
return trimmed ? trimmed[0].toUpperCase() : "?";
|
|
30
29
|
}
|
|
31
30
|
|
|
31
|
+
// src/lib/lockup.ts
|
|
32
|
+
var MARK_CAP_RATIO = 1.08;
|
|
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]";
|
|
39
|
+
var LOCKUP_GAP = "gap-2";
|
|
40
|
+
var LOCKUP_ROW = "flex items-baseline";
|
|
41
|
+
var GLYPH_CANVAS_FILL = 0.65;
|
|
42
|
+
|
|
32
43
|
// src/lib/monogram.ts
|
|
33
44
|
var MONOGRAM_COLORS = [
|
|
34
45
|
"#7c3aed",
|
|
@@ -77,21 +88,26 @@ function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
|
|
|
77
88
|
"span",
|
|
78
89
|
{
|
|
79
90
|
className: cn(
|
|
80
|
-
|
|
91
|
+
// `self-center` opts image glyphs out of the lockup's baseline
|
|
92
|
+
// alignment (DEV-5139): an image box has no text baseline, so
|
|
93
|
+
// flexbox would seat its BOTTOM edge on the wordmark baseline and
|
|
94
|
+
// shove the icon upward. Centering preserves the pre-DEV-5139 look.
|
|
95
|
+
"flex shrink-0 items-center justify-center self-center overflow-hidden rounded",
|
|
81
96
|
// Tint needs a dark-mode counterpart or it vanishes on dark backgrounds.
|
|
82
97
|
subtle ? "bg-black/5 dark:bg-white/10" : "bg-black/10 dark:bg-white/15",
|
|
83
98
|
box,
|
|
84
99
|
className
|
|
85
100
|
),
|
|
86
101
|
children: /* @__PURE__ */ jsx(
|
|
87
|
-
|
|
102
|
+
"img",
|
|
88
103
|
{
|
|
89
104
|
src,
|
|
90
105
|
alt: "",
|
|
91
106
|
width: dim,
|
|
92
107
|
height: dim,
|
|
108
|
+
loading: "lazy",
|
|
109
|
+
decoding: "async",
|
|
93
110
|
"data-testid": "brand-image",
|
|
94
|
-
unoptimized: true,
|
|
95
111
|
onError: () => setFailed((prev) => new Set(prev).add(src)),
|
|
96
112
|
className: cn(img, "rounded-sm object-contain")
|
|
97
113
|
}
|
|
@@ -107,8 +123,15 @@ function BrandGlyph({ brand, size = "sm", subtle = false, className }) {
|
|
|
107
123
|
"aria-hidden": true,
|
|
108
124
|
"data-testid": "brand-monogram",
|
|
109
125
|
className: cn(
|
|
110
|
-
|
|
111
|
-
size
|
|
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
|
|
132
|
+
// exact parity optically reads small. "sm" has no wordmark partner
|
|
133
|
+
// in its surfaces; SIZE unchanged pending a lockup audit there.
|
|
134
|
+
size === "lg" ? MONOGRAM_TEXT_LG : "text-[11px]",
|
|
112
135
|
box,
|
|
113
136
|
className
|
|
114
137
|
),
|
|
@@ -151,7 +174,9 @@ var MIN_SIZES = {
|
|
|
151
174
|
/** Generic minimum width/height for a secondary control (button, select), px. */
|
|
152
175
|
control: 44,
|
|
153
176
|
/** Max width of the brand wordmark before it truncates, px. */
|
|
154
|
-
|
|
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
|
|
155
180
|
};
|
|
156
181
|
var BREAKPOINTS = {
|
|
157
182
|
/** Tailwind `sm`. */
|
|
@@ -256,13 +281,19 @@ function ResponsiveHeader({
|
|
|
256
281
|
};
|
|
257
282
|
}, [open]);
|
|
258
283
|
const hasSecondary = Boolean(nav && nav.length > 0 || auth || themeToggle);
|
|
259
|
-
const brandLockup = /* @__PURE__ */ jsxs2(
|
|
284
|
+
const brandLockup = /* @__PURE__ */ jsxs2("span", { "data-testid": "header-lockup", className: cn(LOCKUP_ROW, LOCKUP_GAP), children: [
|
|
260
285
|
/* @__PURE__ */ jsx2(BrandGlyph, { brand, size: "lg" }),
|
|
261
286
|
/* @__PURE__ */ jsx2(
|
|
262
287
|
"span",
|
|
263
288
|
{
|
|
264
289
|
"data-testid": "header-wordmark",
|
|
265
|
-
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
|
+
),
|
|
266
297
|
style: { maxWidth: MIN_SIZES.wordmarkMaxWidth },
|
|
267
298
|
children: brand.name
|
|
268
299
|
}
|
|
@@ -287,10 +318,10 @@ function ResponsiveHeader({
|
|
|
287
318
|
"aria-label": brand.name || "Home",
|
|
288
319
|
"data-testid": "header-brand",
|
|
289
320
|
style: TOUCH_TARGET_STYLE,
|
|
290
|
-
className: "flex shrink-0 items-center
|
|
321
|
+
className: "flex shrink-0 items-center",
|
|
291
322
|
children: brandLockup
|
|
292
323
|
}
|
|
293
|
-
) : /* @__PURE__ */ jsx2("div", { "data-testid": "header-brand", className: "flex shrink-0 items-center
|
|
324
|
+
) : /* @__PURE__ */ jsx2("div", { "data-testid": "header-brand", className: "flex shrink-0 items-center", children: brandLockup }),
|
|
294
325
|
/* @__PURE__ */ jsx2(
|
|
295
326
|
"div",
|
|
296
327
|
{
|
|
@@ -667,7 +698,7 @@ function useLadderRung(ladder, containerRef, options = {}) {
|
|
|
667
698
|
import { useCallback as useCallback2, useEffect as useEffect4, useId as useId2, useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState5 } from "react";
|
|
668
699
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
669
700
|
var PANEL_GAP = 4;
|
|
670
|
-
function Flyout({ label, icon, active, children, className }) {
|
|
701
|
+
function Flyout({ label, icon, active, children, linkComponent, className }) {
|
|
671
702
|
const [hoverOpen, setHoverOpen] = useState5(false);
|
|
672
703
|
const [clickOpen, setClickOpen] = useState5(false);
|
|
673
704
|
const [panelPosition, setPanelPosition] = useState5(null);
|
|
@@ -678,6 +709,7 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
678
709
|
const firstItemRef = useRef3(null);
|
|
679
710
|
const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
|
|
680
711
|
const Icon = resolveIcon(icon);
|
|
712
|
+
const LinkEl = linkComponent ?? "a";
|
|
681
713
|
const reposition = useCallback2(() => {
|
|
682
714
|
const rect = triggerRef.current?.getBoundingClientRect();
|
|
683
715
|
if (rect) setPanelPosition({ top: rect.top, left: rect.right + PANEL_GAP });
|
|
@@ -751,20 +783,32 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
751
783
|
"z-50 flex min-w-[12rem] flex-col gap-0.5 rounded-md border border-border bg-background p-2 shadow-lg",
|
|
752
784
|
!prefersReducedMotion && "transition-all duration-150 ease-out"
|
|
753
785
|
),
|
|
754
|
-
children: children.map((child, i) =>
|
|
755
|
-
"
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
+
)
|
|
768
812
|
))
|
|
769
813
|
}
|
|
770
814
|
)
|
|
@@ -775,7 +819,7 @@ function Flyout({ label, icon, active, children, className }) {
|
|
|
775
819
|
|
|
776
820
|
// src/app-sidebar.tsx
|
|
777
821
|
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
778
|
-
function isHrefActive(
|
|
822
|
+
function isHrefActive(href, activePath) {
|
|
779
823
|
return activePath === href || activePath.startsWith(`${href}/`);
|
|
780
824
|
}
|
|
781
825
|
function NavLink({
|
|
@@ -783,11 +827,13 @@ function NavLink({
|
|
|
783
827
|
active,
|
|
784
828
|
icon,
|
|
785
829
|
label,
|
|
786
|
-
testId
|
|
830
|
+
testId,
|
|
831
|
+
linkComponent
|
|
787
832
|
}) {
|
|
788
833
|
const Icon = resolveIcon(icon);
|
|
834
|
+
const LinkEl = linkComponent ?? "a";
|
|
789
835
|
return /* @__PURE__ */ jsxs4(
|
|
790
|
-
|
|
836
|
+
LinkEl,
|
|
791
837
|
{
|
|
792
838
|
href,
|
|
793
839
|
"data-testid": testId,
|
|
@@ -819,8 +865,11 @@ function AppSidebar({
|
|
|
819
865
|
topItem,
|
|
820
866
|
menuLabel = "Menu",
|
|
821
867
|
ladderOptions,
|
|
868
|
+
linkComponent,
|
|
869
|
+
isActive,
|
|
822
870
|
className
|
|
823
871
|
}) {
|
|
872
|
+
const isActiveFn = isActive ?? isHrefActive;
|
|
824
873
|
const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINTS.md}px)`);
|
|
825
874
|
const [mobileOpen, setMobileOpen] = useState6(false);
|
|
826
875
|
const containerRef = useRef4(null);
|
|
@@ -856,10 +905,11 @@ function AppSidebar({
|
|
|
856
905
|
NavLink,
|
|
857
906
|
{
|
|
858
907
|
href: def.href,
|
|
859
|
-
active:
|
|
908
|
+
active: isActiveFn(def.href, activePath),
|
|
860
909
|
icon: def.icon,
|
|
861
910
|
label: def.label,
|
|
862
|
-
testId: `${testIdPrefix}-${item.moduleId}
|
|
911
|
+
testId: `${testIdPrefix}-${item.moduleId}`,
|
|
912
|
+
linkComponent
|
|
863
913
|
},
|
|
864
914
|
item.moduleId
|
|
865
915
|
);
|
|
@@ -872,11 +922,21 @@ function AppSidebar({
|
|
|
872
922
|
moduleId,
|
|
873
923
|
label: def?.label ?? moduleId,
|
|
874
924
|
href,
|
|
875
|
-
active:
|
|
925
|
+
active: isActiveFn(href, activePath)
|
|
876
926
|
};
|
|
877
927
|
});
|
|
878
928
|
const active = children.some((c) => c.active);
|
|
879
|
-
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
|
+
);
|
|
880
940
|
}
|
|
881
941
|
const brandBlock = brand && /* @__PURE__ */ jsxs4(
|
|
882
942
|
"a",
|
|
@@ -896,10 +956,11 @@ function AppSidebar({
|
|
|
896
956
|
NavLink,
|
|
897
957
|
{
|
|
898
958
|
href: topItem.href,
|
|
899
|
-
active:
|
|
959
|
+
active: isActiveFn(topItem.href, activePath),
|
|
900
960
|
icon: topItem.icon,
|
|
901
961
|
label: topItem.label,
|
|
902
|
-
testId
|
|
962
|
+
testId,
|
|
963
|
+
linkComponent
|
|
903
964
|
}
|
|
904
965
|
) });
|
|
905
966
|
}
|
|
@@ -1163,8 +1224,14 @@ export {
|
|
|
1163
1224
|
BrandGlyph,
|
|
1164
1225
|
ContextSwitcher,
|
|
1165
1226
|
Flyout,
|
|
1227
|
+
GLYPH_CANVAS_FILL,
|
|
1228
|
+
LOCKUP_GAP,
|
|
1229
|
+
LOCKUP_ROW,
|
|
1230
|
+
MARK_CAP_RATIO,
|
|
1231
|
+
MARK_WEIGHT,
|
|
1166
1232
|
REEVE_ACCENT,
|
|
1167
1233
|
ResponsiveHeader,
|
|
1234
|
+
WORDMARK_WEIGHT,
|
|
1168
1235
|
deriveFallbackLadder,
|
|
1169
1236
|
faviconUrl,
|
|
1170
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",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"dev": "tsup --watch",
|
|
31
31
|
"test": "vitest run",
|
|
32
32
|
"typecheck": "tsc --noEmit",
|
|
33
|
+
"check:esm": "node scripts/check-esm-import.mjs",
|
|
33
34
|
"prepublishOnly": "npm run build"
|
|
34
35
|
},
|
|
35
36
|
"publishConfig": {
|