@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 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
- "flex shrink-0 items-center justify-center overflow-hidden rounded",
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
- import_image.default,
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
- "flex shrink-0 select-none items-baseline justify-center font-semibold uppercase",
159
- size === "lg" ? "text-[13px]" : "text-[11px]",
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
- wordmarkMaxWidth: 180
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)(import_jsx_runtime2.Fragment, { children: [
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: "hidden truncate text-sm font-semibold text-foreground @min-[768px]:block",
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 gap-2",
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 gap-2", children: brandLockup }),
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) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
803
- "a",
804
- {
805
- ref: i === 0 ? firstItemRef : void 0,
806
- href: child.href,
807
- "aria-current": child.active ? "page" : void 0,
808
- style: TOUCH_TARGET_STYLE,
809
- className: cn(
810
- "flex items-center truncate rounded-md px-3 py-1.5 text-sm transition-colors",
811
- child.active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
812
- ),
813
- children: child.label
814
- },
815
- child.moduleId
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(activePath, href) {
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
- "a",
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: isHrefActive(activePath, def.href),
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: isHrefActive(activePath, href)
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)(Flyout, { label: item.label, icon: item.icon, active, children }, item.id);
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: isHrefActive(activePath, topItem.href),
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
- "flex shrink-0 items-center justify-center overflow-hidden rounded",
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
- Image,
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
- "flex shrink-0 select-none items-baseline justify-center font-semibold uppercase",
111
- size === "lg" ? "text-[13px]" : "text-[11px]",
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
- wordmarkMaxWidth: 180
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(Fragment, { children: [
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: "hidden truncate text-sm font-semibold text-foreground @min-[768px]:block",
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 gap-2",
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 gap-2", children: brandLockup }),
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) => /* @__PURE__ */ jsx3(
755
- "a",
756
- {
757
- ref: i === 0 ? firstItemRef : void 0,
758
- href: child.href,
759
- "aria-current": child.active ? "page" : void 0,
760
- style: TOUCH_TARGET_STYLE,
761
- className: cn(
762
- "flex items-center truncate rounded-md px-3 py-1.5 text-sm transition-colors",
763
- child.active ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground"
764
- ),
765
- children: child.label
766
- },
767
- child.moduleId
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(activePath, href) {
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
- "a",
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: isHrefActive(activePath, def.href),
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: isHrefActive(activePath, href)
925
+ active: isActiveFn(href, activePath)
876
926
  };
877
927
  });
878
928
  const active = children.some((c) => c.active);
879
- return /* @__PURE__ */ jsx4(Flyout, { label: item.label, icon: item.icon, active, children }, item.id);
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: isHrefActive(activePath, topItem.href),
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,
@@ -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: 180;
20
+ readonly wordmarkMaxWidth: 205;
21
21
  };
22
22
  declare const BREAKPOINTS: {
23
23
  /** Tailwind `sm`. */
@@ -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: 180;
20
+ readonly wordmarkMaxWidth: 205;
21
21
  };
22
22
  declare const BREAKPOINTS: {
23
23
  /** Tailwind `sm`. */
@@ -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
- wordmarkMaxWidth: 180
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`. */
@@ -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
- wordmarkMaxWidth: 180
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.5.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": {