@meetreeve/ui 0.2.0 → 0.5.2
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/README.md +46 -0
- package/dist/index.d.mts +182 -6
- package/dist/index.d.ts +182 -6
- package/dist/index.js +403 -110
- package/dist/index.mjs +398 -113
- package/dist/tokens/index.d.mts +15 -1
- package/dist/tokens/index.d.ts +15 -1
- package/dist/tokens/index.js +8 -2
- package/dist/tokens/index.mjs +6 -1
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -102,6 +102,18 @@ import { ResponsiveHeader } from "@meetreeve/ui";
|
|
|
102
102
|
|
|
103
103
|
Search is the primary slot — always inline, floored at a minimum width from `@meetreeve/ui/tokens`. Under width pressure everything ELSE collapses first: the wordmark drops to glyph-only, secondary controls fold into a hamburger disclosure below `md`.
|
|
104
104
|
|
|
105
|
+
### CSS container-query collapse (DEV-4260)
|
|
106
|
+
|
|
107
|
+
The wordmark↔glyph and inline-actions↔hamburger split is pure CSS — a Tailwind v4 `@container` on the header plus container-query variants (`@min-[768px]:...`) — not JS/`matchMedia`. Both the desktop and mobile structures are always in the DOM; CSS alone decides which is visible at the header's *current container width* (not the viewport — the header can be narrower than the viewport, e.g. inside a sidebar). This means: no desktop first-paint FOUC, and a no-JS viewer gets the correct layout for their actual width instead of always falling back to the mobile/hamburger markup. JS is used only for the hamburger's open/close state and one `ResizeObserver` that closes an open mobile menu when the header's own container regrows past the split.
|
|
108
|
+
|
|
109
|
+
**Your Tailwind build must scan this package's dist for these classes to work** — same requirement as `AppSidebar`'s `md:flex` (DEV-4351). Tailwind v4 ignores `.gitignore`'d paths (`node_modules`) by default, so add:
|
|
110
|
+
|
|
111
|
+
```css
|
|
112
|
+
@source "../../node_modules/@meetreeve/ui/dist";
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
to your `globals.css` (adjust the relative path to your app). See `reeve-tenant-frontend`'s `src/app/globals.css` for the existing precedent. Without this, `ResponsiveHeader`'s container-variant classes (and `AppSidebar`'s) never make it into your compiled CSS, and the header renders as if `display: none` applied everywhere.
|
|
116
|
+
|
|
105
117
|
## BrandGlyph
|
|
106
118
|
|
|
107
119
|
```tsx
|
|
@@ -112,6 +124,40 @@ import { BrandGlyph } from "@meetreeve/ui";
|
|
|
112
124
|
|
|
113
125
|
Resolution order: favicon → logo → initials monogram.
|
|
114
126
|
|
|
127
|
+
## ContextSwitcher (DEV-4408)
|
|
128
|
+
|
|
129
|
+
One reusable, data-driven, org-grouped dropdown for switching the active
|
|
130
|
+
"context" — a brand, a tenant, or whatever `refId`-addressable entity a host
|
|
131
|
+
app maps onto `ContextSwitcherItem[]`. Fully headless: no data fetching, no
|
|
132
|
+
app-specific types. Host apps write a thin wrapper that pulls their own data
|
|
133
|
+
source (e.g. `useBrand()`) and maps it to items — see reeve-frontend's
|
|
134
|
+
`BrandContextSwitcher` for the pattern.
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import { ContextSwitcher, type ContextSwitcherItem } from "@meetreeve/ui";
|
|
138
|
+
|
|
139
|
+
const items: ContextSwitcherItem[] = brands.map((b) => ({
|
|
140
|
+
refId: b.id,
|
|
141
|
+
name: b.name,
|
|
142
|
+
favicon: faviconUrl(b.slug), // pre-resolve any host-specific favicon rules yourself
|
|
143
|
+
logo: b.logo_url,
|
|
144
|
+
orgId: b.org_id,
|
|
145
|
+
orgName: b.org_name,
|
|
146
|
+
}));
|
|
147
|
+
|
|
148
|
+
<ContextSwitcher
|
|
149
|
+
items={items}
|
|
150
|
+
activeRefId={activeBrand?.id ?? null}
|
|
151
|
+
onSelect={(item) => switchBrand(item.refId)}
|
|
152
|
+
placeholder="Select brand"
|
|
153
|
+
/>;
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Org headers render only when items span more than one org — the common
|
|
157
|
+
single-org case stays a flat list. `collapsed` swaps the trigger for an
|
|
158
|
+
icon-only button (56px nav rail); `footer` adds a slot below the list (e.g.
|
|
159
|
+
an "Add brand" CTA).
|
|
160
|
+
|
|
115
161
|
## Tokens
|
|
116
162
|
|
|
117
163
|
```ts
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode, RefObject } from 'react';
|
|
2
|
+
import { ReactNode, ElementType, RefObject } from 'react';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -10,9 +10,11 @@ import { z } from 'zod';
|
|
|
10
10
|
* order:
|
|
11
11
|
* 1. `brand.favicon` — the site's own square icon,
|
|
12
12
|
* 2. `brand.logo` rendered `object-contain` (no squish),
|
|
13
|
-
* 3. a
|
|
13
|
+
* 3. a single-letter monogram in the brand colour + an accent dot
|
|
14
|
+
* (DEV-4539 — modeled on Reeve's own "R." canon; no boxed container).
|
|
14
15
|
* Each image load error advances to the next candidate.
|
|
15
16
|
*/
|
|
17
|
+
declare const REEVE_ACCENT = "#FF6B2B";
|
|
16
18
|
interface Brand {
|
|
17
19
|
/** Display name — drives the wordmark and the monogram fallback. */
|
|
18
20
|
name: string;
|
|
@@ -20,8 +22,17 @@ interface Brand {
|
|
|
20
22
|
favicon?: string | null;
|
|
21
23
|
/** Wider brand logo URL (second cascade step, rendered `object-contain`). */
|
|
22
24
|
logo?: string | null;
|
|
23
|
-
/** Override the deterministic monogram
|
|
25
|
+
/** Override the deterministic monogram letter colour. */
|
|
24
26
|
color?: string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Accent-dot colour for the single-letter monogram fallback (DEV-4539) —
|
|
29
|
+
* the "family mark". Pass the brand DNA's own strong accent colour when
|
|
30
|
+
* one is known; falls back to Reeve's own accent dot (REEVE_ACCENT)
|
|
31
|
+
* otherwise, the same "part of the Reeve ecosystem" signal the backend
|
|
32
|
+
* logo generator resolves. `null`/omitted both mean "use the fallback" —
|
|
33
|
+
* this field becomes a brand-manifest-owned value once C2 lands.
|
|
34
|
+
*/
|
|
35
|
+
accentColor?: string | null;
|
|
25
36
|
}
|
|
26
37
|
/**
|
|
27
38
|
* Favicon URL for a brand's canonical domain. Returns null unless `domain`
|
|
@@ -38,6 +49,49 @@ interface BrandGlyphProps {
|
|
|
38
49
|
}
|
|
39
50
|
declare function BrandGlyph({ brand, size, subtle, className }: BrandGlyphProps): react.JSX.Element;
|
|
40
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 3, applied: 8px gap ≈ 0.79× the wordmark's ~10.1px cap height —
|
|
84
|
+
* inside the 0.5–0.8 band. Named so nobody "fixes" it per-surface.
|
|
85
|
+
*/
|
|
86
|
+
declare const LOCKUP_GAP = "gap-2";
|
|
87
|
+
/**
|
|
88
|
+
* Rule 1, applied: the lockup is one locked unit — baseline-aligned inside,
|
|
89
|
+
* centered as a whole by whatever wraps it (e.g. a touch target).
|
|
90
|
+
*/
|
|
91
|
+
declare const LOCKUP_ROW = "flex items-baseline";
|
|
92
|
+
/** Rule 5 — standalone glyph cap height as a fraction of the canvas edge. */
|
|
93
|
+
declare const GLYPH_CANVAS_FILL = 0.65;
|
|
94
|
+
|
|
41
95
|
interface HeaderNavItem {
|
|
42
96
|
label: string;
|
|
43
97
|
href?: string;
|
|
@@ -53,6 +107,19 @@ interface ResponsiveHeaderProps {
|
|
|
53
107
|
search: ReactNode;
|
|
54
108
|
/** Secondary nav links — inline on desktop, in the menu on mobile. */
|
|
55
109
|
nav?: HeaderNavItem[];
|
|
110
|
+
/**
|
|
111
|
+
* Optional destination for the brand lockup. When set, the glyph + wordmark
|
|
112
|
+
* become a single clickable link to this href (restores the "click the logo
|
|
113
|
+
* to go home" affordance). When omitted, the lockup is a plain, non-link div.
|
|
114
|
+
*/
|
|
115
|
+
brandHref?: string;
|
|
116
|
+
/**
|
|
117
|
+
* Link renderer for the brand lockup and `nav` href items. Defaults to a bare
|
|
118
|
+
* `"a"` so the package stays framework-agnostic; consumers pass their router's
|
|
119
|
+
* link (e.g. `next/link`) for client-side navigation. Action-only nav items
|
|
120
|
+
* (no href) stay `<button>` regardless.
|
|
121
|
+
*/
|
|
122
|
+
linkComponent?: ElementType;
|
|
56
123
|
/** Auth control (avatar / sign-in) — secondary. */
|
|
57
124
|
auth?: ReactNode;
|
|
58
125
|
/** Theme toggle — secondary. */
|
|
@@ -70,9 +137,53 @@ interface ResponsiveHeaderProps {
|
|
|
70
137
|
* brand wordmark drops to glyph-only and the secondary controls fold into a
|
|
71
138
|
* hamburger disclosure. The search is never touched.
|
|
72
139
|
*
|
|
73
|
-
*
|
|
140
|
+
* CSS container-query collapse (DEV-4260): the wordmark<->glyph and
|
|
141
|
+
* inline-actions<->hamburger split is pure CSS (`@container` on the header +
|
|
142
|
+
* Tailwind v4 container-query variants), NOT JS/matchMedia. Both the desktop
|
|
143
|
+
* and mobile structures render in EVERY environment — SSR, first paint, and
|
|
144
|
+
* with JS fully disabled — so there's no desktop first-paint FOUC and no
|
|
145
|
+
* "no-JS users get hamburger-only" fallback (DEV-4248 follow-up).
|
|
146
|
+
*
|
|
147
|
+
* The split is pinned to `BREAKPOINTS.md` (768px) via an ARBITRARY container
|
|
148
|
+
* variant (`@min-[768px]:...`) rather than Tailwind's *named* `@md` container
|
|
149
|
+
* variant: Tailwind v4's default container scale (`@md` = 448px) is a
|
|
150
|
+
* different scale than the viewport breakpoint scale and does NOT match
|
|
151
|
+
* `BREAKPOINTS.md`. Because Tailwind's build-time scanner needs a literal
|
|
152
|
+
* class string (it greps compiled output, it doesn't evaluate JS), "768" is a
|
|
153
|
+
* hardcoded literal in the classNames below, not interpolated from
|
|
154
|
+
* `BREAKPOINTS.md` — `responsive-header.test.tsx`'s className-contract tests
|
|
155
|
+
* assert against `BREAKPOINTS.md` directly so drift between the token and the
|
|
156
|
+
* literal fails loudly.
|
|
157
|
+
*
|
|
158
|
+
* Consumers must add `@meetreeve/ui/dist` to their Tailwind `@source` scan
|
|
159
|
+
* (see this package's README) or these container-variant classes never make
|
|
160
|
+
* it into the consumer's compiled CSS — same requirement as AppSidebar's
|
|
161
|
+
* `md:flex` (DEV-4351).
|
|
162
|
+
*
|
|
163
|
+
* JS is used ONLY for two things: the hamburger disclosure's open/close
|
|
164
|
+
* state, and ONE ResizeObserver on the header itself (deliberately NOT a
|
|
165
|
+
* viewport matchMedia — the point of DEV-4260/DEV-4248 is that the collapse
|
|
166
|
+
* reacts to the header's own *container* width, e.g. a narrow sidebar on a
|
|
167
|
+
* wide viewport) that tears down an open mobile menu when the header's
|
|
168
|
+
* container regrows past the CSS split.
|
|
74
169
|
*/
|
|
75
|
-
declare function ResponsiveHeader({ brand, search, nav, auth, themeToggle, menuLabel, className, }: ResponsiveHeaderProps): react.JSX.Element;
|
|
170
|
+
declare function ResponsiveHeader({ brand, search, nav, brandHref, linkComponent, auth, themeToggle, menuLabel, className, }: ResponsiveHeaderProps): react.JSX.Element;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Pick a viewport-appropriate placeholder string: the `desktop` copy at/above
|
|
174
|
+
* `md`, the shorter `mobile` copy below it (falling back to `desktop` when no
|
|
175
|
+
* mobile string is supplied). Fixes the "long search prompt clips in the
|
|
176
|
+
* 200px-floored mobile slot" class of bug without the consumer wiring up its
|
|
177
|
+
* own media query.
|
|
178
|
+
*
|
|
179
|
+
* Built on the package's guarded `useMediaQuery`, which returns `false` when
|
|
180
|
+
* `matchMedia` is unavailable — so SSR and the first client paint yield the
|
|
181
|
+
* MOBILE string. That mobile-first default is intentional: the short string
|
|
182
|
+
* never clips, and the value reconciles up to the desktop string on mount once
|
|
183
|
+
* the viewport is measured `>= md` (consistent with the DEV-4260 FOUC note; no
|
|
184
|
+
* hydration mismatch, since server and first client render both read `false`).
|
|
185
|
+
*/
|
|
186
|
+
declare function useResponsivePlaceholder(desktop: string, mobile?: string): string;
|
|
76
187
|
|
|
77
188
|
/**
|
|
78
189
|
* SidebarLadder — the per-tenant JSON artifact (DEV-4351) that pre-calculates
|
|
@@ -325,4 +436,69 @@ interface FlyoutProps {
|
|
|
325
436
|
*/
|
|
326
437
|
declare function Flyout({ label, icon, active, children, className }: FlyoutProps): react.JSX.Element;
|
|
327
438
|
|
|
328
|
-
|
|
439
|
+
/**
|
|
440
|
+
* ContextSwitcher (DEV-4408) — promoted out of reeve-frontend's
|
|
441
|
+
* src/components/context/context-switcher.tsx (DEV-2513), which was already
|
|
442
|
+
* fully headless (props in, no app-specific data fetching). One reusable,
|
|
443
|
+
* data-driven, org-grouped dropdown for switching the active "context" —
|
|
444
|
+
* whatever refId-addressable entity a host app's own thin wrapper maps onto
|
|
445
|
+
* `ContextSwitcherItem[]` (reeve-frontend's brand, reeve-tenant-frontend's
|
|
446
|
+
* tenant, or — once the spawned-company substrate lands — a company
|
|
447
|
+
* cockpit). Orgs are pure visual groupers — headers render ONLY for
|
|
448
|
+
* multi-org accounts; the common single-org case stays a flat list.
|
|
449
|
+
*
|
|
450
|
+
* The menu renders through Radix DropdownMenu so it portals out of a
|
|
451
|
+
* narrow/collapsible rail and stays on-screen — Radix handles collision
|
|
452
|
+
* flip/shift. `collapsed` swaps the trigger for an icon-only button that
|
|
453
|
+
* fits a 56px collapsed rail.
|
|
454
|
+
*/
|
|
455
|
+
interface ContextSwitcherItem {
|
|
456
|
+
/** Stable identity for this context — a brand id, tenant id, etc. */
|
|
457
|
+
refId: string;
|
|
458
|
+
name: string;
|
|
459
|
+
/**
|
|
460
|
+
* Pre-resolved favicon URL (BrandGlyph's favicon-first cascade). Any
|
|
461
|
+
* host-specific favicon rules (e.g. reeve-frontend's synthetic-domain
|
|
462
|
+
* guard around `faviconUrl`) must be applied by the host BEFORE mapping
|
|
463
|
+
* into this item — this package stays agnostic of any one host's domain
|
|
464
|
+
* conventions.
|
|
465
|
+
*/
|
|
466
|
+
favicon?: string | null;
|
|
467
|
+
/** Wider brand/tenant logo URL (BrandGlyph's second cascade step). */
|
|
468
|
+
logo?: string | null;
|
|
469
|
+
/** Override BrandGlyph's deterministic monogram letter colour. */
|
|
470
|
+
color?: string | null;
|
|
471
|
+
/** Override BrandGlyph's monogram accent-dot colour (DEV-4539). */
|
|
472
|
+
accentColor?: string | null;
|
|
473
|
+
orgId?: string | null;
|
|
474
|
+
orgName?: string | null;
|
|
475
|
+
}
|
|
476
|
+
interface ContextOrgGroup {
|
|
477
|
+
orgId: string | null;
|
|
478
|
+
orgName: string | null;
|
|
479
|
+
items: ContextSwitcherItem[];
|
|
480
|
+
}
|
|
481
|
+
/** Group contexts by owning org. Items sort by name; groups by org name. */
|
|
482
|
+
declare function groupByOrg(items: ContextSwitcherItem[]): ContextOrgGroup[];
|
|
483
|
+
interface ContextSwitcherProps {
|
|
484
|
+
items: ContextSwitcherItem[];
|
|
485
|
+
/** refId of the active context. */
|
|
486
|
+
activeRefId: string | null;
|
|
487
|
+
onSelect: (item: ContextSwitcherItem) => void;
|
|
488
|
+
/** Optional dropdown footer slot, e.g. an "Add brand" CTA. */
|
|
489
|
+
footer?: React.ReactNode;
|
|
490
|
+
/** Trigger label when nothing is active. */
|
|
491
|
+
placeholder?: string;
|
|
492
|
+
/** Icon-only trigger — for a collapsed (56px) nav rail. */
|
|
493
|
+
collapsed?: boolean;
|
|
494
|
+
/** Dropdown placement (Radix). Defaults to bottom/start; Radix auto-flips. */
|
|
495
|
+
side?: "top" | "right" | "bottom" | "left";
|
|
496
|
+
align?: "start" | "center" | "end";
|
|
497
|
+
/** Extra classes merged onto the portaled DropdownMenuContent — e.g. a
|
|
498
|
+
* higher z-index so the dropdown clears a high-z overlay (a mobile
|
|
499
|
+
* drawer above the default z-50 content). */
|
|
500
|
+
contentClassName?: string;
|
|
501
|
+
}
|
|
502
|
+
declare function ContextSwitcher({ items, activeRefId, onSelect, footer, placeholder, collapsed, side, align, contentClassName, }: ContextSwitcherProps): react.JSX.Element;
|
|
503
|
+
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode, RefObject } from 'react';
|
|
2
|
+
import { ReactNode, ElementType, RefObject } from 'react';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -10,9 +10,11 @@ import { z } from 'zod';
|
|
|
10
10
|
* order:
|
|
11
11
|
* 1. `brand.favicon` — the site's own square icon,
|
|
12
12
|
* 2. `brand.logo` rendered `object-contain` (no squish),
|
|
13
|
-
* 3. a
|
|
13
|
+
* 3. a single-letter monogram in the brand colour + an accent dot
|
|
14
|
+
* (DEV-4539 — modeled on Reeve's own "R." canon; no boxed container).
|
|
14
15
|
* Each image load error advances to the next candidate.
|
|
15
16
|
*/
|
|
17
|
+
declare const REEVE_ACCENT = "#FF6B2B";
|
|
16
18
|
interface Brand {
|
|
17
19
|
/** Display name — drives the wordmark and the monogram fallback. */
|
|
18
20
|
name: string;
|
|
@@ -20,8 +22,17 @@ interface Brand {
|
|
|
20
22
|
favicon?: string | null;
|
|
21
23
|
/** Wider brand logo URL (second cascade step, rendered `object-contain`). */
|
|
22
24
|
logo?: string | null;
|
|
23
|
-
/** Override the deterministic monogram
|
|
25
|
+
/** Override the deterministic monogram letter colour. */
|
|
24
26
|
color?: string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Accent-dot colour for the single-letter monogram fallback (DEV-4539) —
|
|
29
|
+
* the "family mark". Pass the brand DNA's own strong accent colour when
|
|
30
|
+
* one is known; falls back to Reeve's own accent dot (REEVE_ACCENT)
|
|
31
|
+
* otherwise, the same "part of the Reeve ecosystem" signal the backend
|
|
32
|
+
* logo generator resolves. `null`/omitted both mean "use the fallback" —
|
|
33
|
+
* this field becomes a brand-manifest-owned value once C2 lands.
|
|
34
|
+
*/
|
|
35
|
+
accentColor?: string | null;
|
|
25
36
|
}
|
|
26
37
|
/**
|
|
27
38
|
* Favicon URL for a brand's canonical domain. Returns null unless `domain`
|
|
@@ -38,6 +49,49 @@ interface BrandGlyphProps {
|
|
|
38
49
|
}
|
|
39
50
|
declare function BrandGlyph({ brand, size, subtle, className }: BrandGlyphProps): react.JSX.Element;
|
|
40
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 3, applied: 8px gap ≈ 0.79× the wordmark's ~10.1px cap height —
|
|
84
|
+
* inside the 0.5–0.8 band. Named so nobody "fixes" it per-surface.
|
|
85
|
+
*/
|
|
86
|
+
declare const LOCKUP_GAP = "gap-2";
|
|
87
|
+
/**
|
|
88
|
+
* Rule 1, applied: the lockup is one locked unit — baseline-aligned inside,
|
|
89
|
+
* centered as a whole by whatever wraps it (e.g. a touch target).
|
|
90
|
+
*/
|
|
91
|
+
declare const LOCKUP_ROW = "flex items-baseline";
|
|
92
|
+
/** Rule 5 — standalone glyph cap height as a fraction of the canvas edge. */
|
|
93
|
+
declare const GLYPH_CANVAS_FILL = 0.65;
|
|
94
|
+
|
|
41
95
|
interface HeaderNavItem {
|
|
42
96
|
label: string;
|
|
43
97
|
href?: string;
|
|
@@ -53,6 +107,19 @@ interface ResponsiveHeaderProps {
|
|
|
53
107
|
search: ReactNode;
|
|
54
108
|
/** Secondary nav links — inline on desktop, in the menu on mobile. */
|
|
55
109
|
nav?: HeaderNavItem[];
|
|
110
|
+
/**
|
|
111
|
+
* Optional destination for the brand lockup. When set, the glyph + wordmark
|
|
112
|
+
* become a single clickable link to this href (restores the "click the logo
|
|
113
|
+
* to go home" affordance). When omitted, the lockup is a plain, non-link div.
|
|
114
|
+
*/
|
|
115
|
+
brandHref?: string;
|
|
116
|
+
/**
|
|
117
|
+
* Link renderer for the brand lockup and `nav` href items. Defaults to a bare
|
|
118
|
+
* `"a"` so the package stays framework-agnostic; consumers pass their router's
|
|
119
|
+
* link (e.g. `next/link`) for client-side navigation. Action-only nav items
|
|
120
|
+
* (no href) stay `<button>` regardless.
|
|
121
|
+
*/
|
|
122
|
+
linkComponent?: ElementType;
|
|
56
123
|
/** Auth control (avatar / sign-in) — secondary. */
|
|
57
124
|
auth?: ReactNode;
|
|
58
125
|
/** Theme toggle — secondary. */
|
|
@@ -70,9 +137,53 @@ interface ResponsiveHeaderProps {
|
|
|
70
137
|
* brand wordmark drops to glyph-only and the secondary controls fold into a
|
|
71
138
|
* hamburger disclosure. The search is never touched.
|
|
72
139
|
*
|
|
73
|
-
*
|
|
140
|
+
* CSS container-query collapse (DEV-4260): the wordmark<->glyph and
|
|
141
|
+
* inline-actions<->hamburger split is pure CSS (`@container` on the header +
|
|
142
|
+
* Tailwind v4 container-query variants), NOT JS/matchMedia. Both the desktop
|
|
143
|
+
* and mobile structures render in EVERY environment — SSR, first paint, and
|
|
144
|
+
* with JS fully disabled — so there's no desktop first-paint FOUC and no
|
|
145
|
+
* "no-JS users get hamburger-only" fallback (DEV-4248 follow-up).
|
|
146
|
+
*
|
|
147
|
+
* The split is pinned to `BREAKPOINTS.md` (768px) via an ARBITRARY container
|
|
148
|
+
* variant (`@min-[768px]:...`) rather than Tailwind's *named* `@md` container
|
|
149
|
+
* variant: Tailwind v4's default container scale (`@md` = 448px) is a
|
|
150
|
+
* different scale than the viewport breakpoint scale and does NOT match
|
|
151
|
+
* `BREAKPOINTS.md`. Because Tailwind's build-time scanner needs a literal
|
|
152
|
+
* class string (it greps compiled output, it doesn't evaluate JS), "768" is a
|
|
153
|
+
* hardcoded literal in the classNames below, not interpolated from
|
|
154
|
+
* `BREAKPOINTS.md` — `responsive-header.test.tsx`'s className-contract tests
|
|
155
|
+
* assert against `BREAKPOINTS.md` directly so drift between the token and the
|
|
156
|
+
* literal fails loudly.
|
|
157
|
+
*
|
|
158
|
+
* Consumers must add `@meetreeve/ui/dist` to their Tailwind `@source` scan
|
|
159
|
+
* (see this package's README) or these container-variant classes never make
|
|
160
|
+
* it into the consumer's compiled CSS — same requirement as AppSidebar's
|
|
161
|
+
* `md:flex` (DEV-4351).
|
|
162
|
+
*
|
|
163
|
+
* JS is used ONLY for two things: the hamburger disclosure's open/close
|
|
164
|
+
* state, and ONE ResizeObserver on the header itself (deliberately NOT a
|
|
165
|
+
* viewport matchMedia — the point of DEV-4260/DEV-4248 is that the collapse
|
|
166
|
+
* reacts to the header's own *container* width, e.g. a narrow sidebar on a
|
|
167
|
+
* wide viewport) that tears down an open mobile menu when the header's
|
|
168
|
+
* container regrows past the CSS split.
|
|
74
169
|
*/
|
|
75
|
-
declare function ResponsiveHeader({ brand, search, nav, auth, themeToggle, menuLabel, className, }: ResponsiveHeaderProps): react.JSX.Element;
|
|
170
|
+
declare function ResponsiveHeader({ brand, search, nav, brandHref, linkComponent, auth, themeToggle, menuLabel, className, }: ResponsiveHeaderProps): react.JSX.Element;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Pick a viewport-appropriate placeholder string: the `desktop` copy at/above
|
|
174
|
+
* `md`, the shorter `mobile` copy below it (falling back to `desktop` when no
|
|
175
|
+
* mobile string is supplied). Fixes the "long search prompt clips in the
|
|
176
|
+
* 200px-floored mobile slot" class of bug without the consumer wiring up its
|
|
177
|
+
* own media query.
|
|
178
|
+
*
|
|
179
|
+
* Built on the package's guarded `useMediaQuery`, which returns `false` when
|
|
180
|
+
* `matchMedia` is unavailable — so SSR and the first client paint yield the
|
|
181
|
+
* MOBILE string. That mobile-first default is intentional: the short string
|
|
182
|
+
* never clips, and the value reconciles up to the desktop string on mount once
|
|
183
|
+
* the viewport is measured `>= md` (consistent with the DEV-4260 FOUC note; no
|
|
184
|
+
* hydration mismatch, since server and first client render both read `false`).
|
|
185
|
+
*/
|
|
186
|
+
declare function useResponsivePlaceholder(desktop: string, mobile?: string): string;
|
|
76
187
|
|
|
77
188
|
/**
|
|
78
189
|
* SidebarLadder — the per-tenant JSON artifact (DEV-4351) that pre-calculates
|
|
@@ -325,4 +436,69 @@ interface FlyoutProps {
|
|
|
325
436
|
*/
|
|
326
437
|
declare function Flyout({ label, icon, active, children, className }: FlyoutProps): react.JSX.Element;
|
|
327
438
|
|
|
328
|
-
|
|
439
|
+
/**
|
|
440
|
+
* ContextSwitcher (DEV-4408) — promoted out of reeve-frontend's
|
|
441
|
+
* src/components/context/context-switcher.tsx (DEV-2513), which was already
|
|
442
|
+
* fully headless (props in, no app-specific data fetching). One reusable,
|
|
443
|
+
* data-driven, org-grouped dropdown for switching the active "context" —
|
|
444
|
+
* whatever refId-addressable entity a host app's own thin wrapper maps onto
|
|
445
|
+
* `ContextSwitcherItem[]` (reeve-frontend's brand, reeve-tenant-frontend's
|
|
446
|
+
* tenant, or — once the spawned-company substrate lands — a company
|
|
447
|
+
* cockpit). Orgs are pure visual groupers — headers render ONLY for
|
|
448
|
+
* multi-org accounts; the common single-org case stays a flat list.
|
|
449
|
+
*
|
|
450
|
+
* The menu renders through Radix DropdownMenu so it portals out of a
|
|
451
|
+
* narrow/collapsible rail and stays on-screen — Radix handles collision
|
|
452
|
+
* flip/shift. `collapsed` swaps the trigger for an icon-only button that
|
|
453
|
+
* fits a 56px collapsed rail.
|
|
454
|
+
*/
|
|
455
|
+
interface ContextSwitcherItem {
|
|
456
|
+
/** Stable identity for this context — a brand id, tenant id, etc. */
|
|
457
|
+
refId: string;
|
|
458
|
+
name: string;
|
|
459
|
+
/**
|
|
460
|
+
* Pre-resolved favicon URL (BrandGlyph's favicon-first cascade). Any
|
|
461
|
+
* host-specific favicon rules (e.g. reeve-frontend's synthetic-domain
|
|
462
|
+
* guard around `faviconUrl`) must be applied by the host BEFORE mapping
|
|
463
|
+
* into this item — this package stays agnostic of any one host's domain
|
|
464
|
+
* conventions.
|
|
465
|
+
*/
|
|
466
|
+
favicon?: string | null;
|
|
467
|
+
/** Wider brand/tenant logo URL (BrandGlyph's second cascade step). */
|
|
468
|
+
logo?: string | null;
|
|
469
|
+
/** Override BrandGlyph's deterministic monogram letter colour. */
|
|
470
|
+
color?: string | null;
|
|
471
|
+
/** Override BrandGlyph's monogram accent-dot colour (DEV-4539). */
|
|
472
|
+
accentColor?: string | null;
|
|
473
|
+
orgId?: string | null;
|
|
474
|
+
orgName?: string | null;
|
|
475
|
+
}
|
|
476
|
+
interface ContextOrgGroup {
|
|
477
|
+
orgId: string | null;
|
|
478
|
+
orgName: string | null;
|
|
479
|
+
items: ContextSwitcherItem[];
|
|
480
|
+
}
|
|
481
|
+
/** Group contexts by owning org. Items sort by name; groups by org name. */
|
|
482
|
+
declare function groupByOrg(items: ContextSwitcherItem[]): ContextOrgGroup[];
|
|
483
|
+
interface ContextSwitcherProps {
|
|
484
|
+
items: ContextSwitcherItem[];
|
|
485
|
+
/** refId of the active context. */
|
|
486
|
+
activeRefId: string | null;
|
|
487
|
+
onSelect: (item: ContextSwitcherItem) => void;
|
|
488
|
+
/** Optional dropdown footer slot, e.g. an "Add brand" CTA. */
|
|
489
|
+
footer?: React.ReactNode;
|
|
490
|
+
/** Trigger label when nothing is active. */
|
|
491
|
+
placeholder?: string;
|
|
492
|
+
/** Icon-only trigger — for a collapsed (56px) nav rail. */
|
|
493
|
+
collapsed?: boolean;
|
|
494
|
+
/** Dropdown placement (Radix). Defaults to bottom/start; Radix auto-flips. */
|
|
495
|
+
side?: "top" | "right" | "bottom" | "left";
|
|
496
|
+
align?: "start" | "center" | "end";
|
|
497
|
+
/** Extra classes merged onto the portaled DropdownMenuContent — e.g. a
|
|
498
|
+
* higher z-index so the dropdown clears a high-z overlay (a mobile
|
|
499
|
+
* drawer above the default z-50 content). */
|
|
500
|
+
contentClassName?: string;
|
|
501
|
+
}
|
|
502
|
+
declare function ContextSwitcher({ items, activeRefId, onSelect, footer, placeholder, collapsed, side, align, contentClassName, }: ContextSwitcherProps): react.JSX.Element;
|
|
503
|
+
|
|
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 };
|