@meetreeve/ui 0.2.0 → 0.5.0

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 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 high-contrast letter monogram in a deterministic brand colour.
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 background colour. */
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`
@@ -53,6 +64,19 @@ interface ResponsiveHeaderProps {
53
64
  search: ReactNode;
54
65
  /** Secondary nav links — inline on desktop, in the menu on mobile. */
55
66
  nav?: HeaderNavItem[];
67
+ /**
68
+ * Optional destination for the brand lockup. When set, the glyph + wordmark
69
+ * become a single clickable link to this href (restores the "click the logo
70
+ * to go home" affordance). When omitted, the lockup is a plain, non-link div.
71
+ */
72
+ brandHref?: string;
73
+ /**
74
+ * Link renderer for the brand lockup and `nav` href items. Defaults to a bare
75
+ * `"a"` so the package stays framework-agnostic; consumers pass their router's
76
+ * link (e.g. `next/link`) for client-side navigation. Action-only nav items
77
+ * (no href) stay `<button>` regardless.
78
+ */
79
+ linkComponent?: ElementType;
56
80
  /** Auth control (avatar / sign-in) — secondary. */
57
81
  auth?: ReactNode;
58
82
  /** Theme toggle — secondary. */
@@ -70,9 +94,53 @@ interface ResponsiveHeaderProps {
70
94
  * brand wordmark drops to glyph-only and the secondary controls fold into a
71
95
  * hamburger disclosure. The search is never touched.
72
96
  *
73
- * A `"use client"` leaf: it's interactive (matchMedia + disclosure state).
97
+ * CSS container-query collapse (DEV-4260): the wordmark<->glyph and
98
+ * inline-actions<->hamburger split is pure CSS (`@container` on the header +
99
+ * Tailwind v4 container-query variants), NOT JS/matchMedia. Both the desktop
100
+ * and mobile structures render in EVERY environment — SSR, first paint, and
101
+ * with JS fully disabled — so there's no desktop first-paint FOUC and no
102
+ * "no-JS users get hamburger-only" fallback (DEV-4248 follow-up).
103
+ *
104
+ * The split is pinned to `BREAKPOINTS.md` (768px) via an ARBITRARY container
105
+ * variant (`@min-[768px]:...`) rather than Tailwind's *named* `@md` container
106
+ * variant: Tailwind v4's default container scale (`@md` = 448px) is a
107
+ * different scale than the viewport breakpoint scale and does NOT match
108
+ * `BREAKPOINTS.md`. Because Tailwind's build-time scanner needs a literal
109
+ * class string (it greps compiled output, it doesn't evaluate JS), "768" is a
110
+ * hardcoded literal in the classNames below, not interpolated from
111
+ * `BREAKPOINTS.md` — `responsive-header.test.tsx`'s className-contract tests
112
+ * assert against `BREAKPOINTS.md` directly so drift between the token and the
113
+ * literal fails loudly.
114
+ *
115
+ * Consumers must add `@meetreeve/ui/dist` to their Tailwind `@source` scan
116
+ * (see this package's README) or these container-variant classes never make
117
+ * it into the consumer's compiled CSS — same requirement as AppSidebar's
118
+ * `md:flex` (DEV-4351).
119
+ *
120
+ * JS is used ONLY for two things: the hamburger disclosure's open/close
121
+ * state, and ONE ResizeObserver on the header itself (deliberately NOT a
122
+ * viewport matchMedia — the point of DEV-4260/DEV-4248 is that the collapse
123
+ * reacts to the header's own *container* width, e.g. a narrow sidebar on a
124
+ * wide viewport) that tears down an open mobile menu when the header's
125
+ * container regrows past the CSS split.
126
+ */
127
+ declare function ResponsiveHeader({ brand, search, nav, brandHref, linkComponent, auth, themeToggle, menuLabel, className, }: ResponsiveHeaderProps): react.JSX.Element;
128
+
129
+ /**
130
+ * Pick a viewport-appropriate placeholder string: the `desktop` copy at/above
131
+ * `md`, the shorter `mobile` copy below it (falling back to `desktop` when no
132
+ * mobile string is supplied). Fixes the "long search prompt clips in the
133
+ * 200px-floored mobile slot" class of bug without the consumer wiring up its
134
+ * own media query.
135
+ *
136
+ * Built on the package's guarded `useMediaQuery`, which returns `false` when
137
+ * `matchMedia` is unavailable — so SSR and the first client paint yield the
138
+ * MOBILE string. That mobile-first default is intentional: the short string
139
+ * never clips, and the value reconciles up to the desktop string on mount once
140
+ * the viewport is measured `>= md` (consistent with the DEV-4260 FOUC note; no
141
+ * hydration mismatch, since server and first client render both read `false`).
74
142
  */
75
- declare function ResponsiveHeader({ brand, search, nav, auth, themeToggle, menuLabel, className, }: ResponsiveHeaderProps): react.JSX.Element;
143
+ declare function useResponsivePlaceholder(desktop: string, mobile?: string): string;
76
144
 
77
145
  /**
78
146
  * SidebarLadder — the per-tenant JSON artifact (DEV-4351) that pre-calculates
@@ -325,4 +393,69 @@ interface FlyoutProps {
325
393
  */
326
394
  declare function Flyout({ label, icon, active, children, className }: FlyoutProps): react.JSX.Element;
327
395
 
328
- export { type AppRungItem, AppSidebar, type AppSidebarBrand, type AppSidebarModuleDef, type AppSidebarProps, type AppSidebarTopItem, type Brand, BrandGlyph, type BrandGlyphProps, type FallbackModuleDef, Flyout, type FlyoutChild, type FlyoutProps, type GroupRungItem, type HeaderNavItem, ResponsiveHeader, type ResponsiveHeaderProps, type RungItem, type SidebarLadder, type SidebarLadderParsed, type SidebarRung, type UseLadderRungOptions, deriveFallbackLadder, faviconUrl, reconcileLadder, resolveLadder, useLadderRung, zSidebarLadder };
396
+ /**
397
+ * ContextSwitcher (DEV-4408) — promoted out of reeve-frontend's
398
+ * src/components/context/context-switcher.tsx (DEV-2513), which was already
399
+ * fully headless (props in, no app-specific data fetching). One reusable,
400
+ * data-driven, org-grouped dropdown for switching the active "context" —
401
+ * whatever refId-addressable entity a host app's own thin wrapper maps onto
402
+ * `ContextSwitcherItem[]` (reeve-frontend's brand, reeve-tenant-frontend's
403
+ * tenant, or — once the spawned-company substrate lands — a company
404
+ * cockpit). Orgs are pure visual groupers — headers render ONLY for
405
+ * multi-org accounts; the common single-org case stays a flat list.
406
+ *
407
+ * The menu renders through Radix DropdownMenu so it portals out of a
408
+ * narrow/collapsible rail and stays on-screen — Radix handles collision
409
+ * flip/shift. `collapsed` swaps the trigger for an icon-only button that
410
+ * fits a 56px collapsed rail.
411
+ */
412
+ interface ContextSwitcherItem {
413
+ /** Stable identity for this context — a brand id, tenant id, etc. */
414
+ refId: string;
415
+ name: string;
416
+ /**
417
+ * Pre-resolved favicon URL (BrandGlyph's favicon-first cascade). Any
418
+ * host-specific favicon rules (e.g. reeve-frontend's synthetic-domain
419
+ * guard around `faviconUrl`) must be applied by the host BEFORE mapping
420
+ * into this item — this package stays agnostic of any one host's domain
421
+ * conventions.
422
+ */
423
+ favicon?: string | null;
424
+ /** Wider brand/tenant logo URL (BrandGlyph's second cascade step). */
425
+ logo?: string | null;
426
+ /** Override BrandGlyph's deterministic monogram letter colour. */
427
+ color?: string | null;
428
+ /** Override BrandGlyph's monogram accent-dot colour (DEV-4539). */
429
+ accentColor?: string | null;
430
+ orgId?: string | null;
431
+ orgName?: string | null;
432
+ }
433
+ interface ContextOrgGroup {
434
+ orgId: string | null;
435
+ orgName: string | null;
436
+ items: ContextSwitcherItem[];
437
+ }
438
+ /** Group contexts by owning org. Items sort by name; groups by org name. */
439
+ declare function groupByOrg(items: ContextSwitcherItem[]): ContextOrgGroup[];
440
+ interface ContextSwitcherProps {
441
+ items: ContextSwitcherItem[];
442
+ /** refId of the active context. */
443
+ activeRefId: string | null;
444
+ onSelect: (item: ContextSwitcherItem) => void;
445
+ /** Optional dropdown footer slot, e.g. an "Add brand" CTA. */
446
+ footer?: React.ReactNode;
447
+ /** Trigger label when nothing is active. */
448
+ placeholder?: string;
449
+ /** Icon-only trigger — for a collapsed (56px) nav rail. */
450
+ collapsed?: boolean;
451
+ /** Dropdown placement (Radix). Defaults to bottom/start; Radix auto-flips. */
452
+ side?: "top" | "right" | "bottom" | "left";
453
+ align?: "start" | "center" | "end";
454
+ /** Extra classes merged onto the portaled DropdownMenuContent — e.g. a
455
+ * higher z-index so the dropdown clears a high-z overlay (a mobile
456
+ * drawer above the default z-50 content). */
457
+ contentClassName?: string;
458
+ }
459
+ declare function ContextSwitcher({ items, activeRefId, onSelect, footer, placeholder, collapsed, side, align, contentClassName, }: ContextSwitcherProps): react.JSX.Element;
460
+
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 };
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 high-contrast letter monogram in a deterministic brand colour.
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 background colour. */
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`
@@ -53,6 +64,19 @@ interface ResponsiveHeaderProps {
53
64
  search: ReactNode;
54
65
  /** Secondary nav links — inline on desktop, in the menu on mobile. */
55
66
  nav?: HeaderNavItem[];
67
+ /**
68
+ * Optional destination for the brand lockup. When set, the glyph + wordmark
69
+ * become a single clickable link to this href (restores the "click the logo
70
+ * to go home" affordance). When omitted, the lockup is a plain, non-link div.
71
+ */
72
+ brandHref?: string;
73
+ /**
74
+ * Link renderer for the brand lockup and `nav` href items. Defaults to a bare
75
+ * `"a"` so the package stays framework-agnostic; consumers pass their router's
76
+ * link (e.g. `next/link`) for client-side navigation. Action-only nav items
77
+ * (no href) stay `<button>` regardless.
78
+ */
79
+ linkComponent?: ElementType;
56
80
  /** Auth control (avatar / sign-in) — secondary. */
57
81
  auth?: ReactNode;
58
82
  /** Theme toggle — secondary. */
@@ -70,9 +94,53 @@ interface ResponsiveHeaderProps {
70
94
  * brand wordmark drops to glyph-only and the secondary controls fold into a
71
95
  * hamburger disclosure. The search is never touched.
72
96
  *
73
- * A `"use client"` leaf: it's interactive (matchMedia + disclosure state).
97
+ * CSS container-query collapse (DEV-4260): the wordmark<->glyph and
98
+ * inline-actions<->hamburger split is pure CSS (`@container` on the header +
99
+ * Tailwind v4 container-query variants), NOT JS/matchMedia. Both the desktop
100
+ * and mobile structures render in EVERY environment — SSR, first paint, and
101
+ * with JS fully disabled — so there's no desktop first-paint FOUC and no
102
+ * "no-JS users get hamburger-only" fallback (DEV-4248 follow-up).
103
+ *
104
+ * The split is pinned to `BREAKPOINTS.md` (768px) via an ARBITRARY container
105
+ * variant (`@min-[768px]:...`) rather than Tailwind's *named* `@md` container
106
+ * variant: Tailwind v4's default container scale (`@md` = 448px) is a
107
+ * different scale than the viewport breakpoint scale and does NOT match
108
+ * `BREAKPOINTS.md`. Because Tailwind's build-time scanner needs a literal
109
+ * class string (it greps compiled output, it doesn't evaluate JS), "768" is a
110
+ * hardcoded literal in the classNames below, not interpolated from
111
+ * `BREAKPOINTS.md` — `responsive-header.test.tsx`'s className-contract tests
112
+ * assert against `BREAKPOINTS.md` directly so drift between the token and the
113
+ * literal fails loudly.
114
+ *
115
+ * Consumers must add `@meetreeve/ui/dist` to their Tailwind `@source` scan
116
+ * (see this package's README) or these container-variant classes never make
117
+ * it into the consumer's compiled CSS — same requirement as AppSidebar's
118
+ * `md:flex` (DEV-4351).
119
+ *
120
+ * JS is used ONLY for two things: the hamburger disclosure's open/close
121
+ * state, and ONE ResizeObserver on the header itself (deliberately NOT a
122
+ * viewport matchMedia — the point of DEV-4260/DEV-4248 is that the collapse
123
+ * reacts to the header's own *container* width, e.g. a narrow sidebar on a
124
+ * wide viewport) that tears down an open mobile menu when the header's
125
+ * container regrows past the CSS split.
126
+ */
127
+ declare function ResponsiveHeader({ brand, search, nav, brandHref, linkComponent, auth, themeToggle, menuLabel, className, }: ResponsiveHeaderProps): react.JSX.Element;
128
+
129
+ /**
130
+ * Pick a viewport-appropriate placeholder string: the `desktop` copy at/above
131
+ * `md`, the shorter `mobile` copy below it (falling back to `desktop` when no
132
+ * mobile string is supplied). Fixes the "long search prompt clips in the
133
+ * 200px-floored mobile slot" class of bug without the consumer wiring up its
134
+ * own media query.
135
+ *
136
+ * Built on the package's guarded `useMediaQuery`, which returns `false` when
137
+ * `matchMedia` is unavailable — so SSR and the first client paint yield the
138
+ * MOBILE string. That mobile-first default is intentional: the short string
139
+ * never clips, and the value reconciles up to the desktop string on mount once
140
+ * the viewport is measured `>= md` (consistent with the DEV-4260 FOUC note; no
141
+ * hydration mismatch, since server and first client render both read `false`).
74
142
  */
75
- declare function ResponsiveHeader({ brand, search, nav, auth, themeToggle, menuLabel, className, }: ResponsiveHeaderProps): react.JSX.Element;
143
+ declare function useResponsivePlaceholder(desktop: string, mobile?: string): string;
76
144
 
77
145
  /**
78
146
  * SidebarLadder — the per-tenant JSON artifact (DEV-4351) that pre-calculates
@@ -325,4 +393,69 @@ interface FlyoutProps {
325
393
  */
326
394
  declare function Flyout({ label, icon, active, children, className }: FlyoutProps): react.JSX.Element;
327
395
 
328
- export { type AppRungItem, AppSidebar, type AppSidebarBrand, type AppSidebarModuleDef, type AppSidebarProps, type AppSidebarTopItem, type Brand, BrandGlyph, type BrandGlyphProps, type FallbackModuleDef, Flyout, type FlyoutChild, type FlyoutProps, type GroupRungItem, type HeaderNavItem, ResponsiveHeader, type ResponsiveHeaderProps, type RungItem, type SidebarLadder, type SidebarLadderParsed, type SidebarRung, type UseLadderRungOptions, deriveFallbackLadder, faviconUrl, reconcileLadder, resolveLadder, useLadderRung, zSidebarLadder };
396
+ /**
397
+ * ContextSwitcher (DEV-4408) — promoted out of reeve-frontend's
398
+ * src/components/context/context-switcher.tsx (DEV-2513), which was already
399
+ * fully headless (props in, no app-specific data fetching). One reusable,
400
+ * data-driven, org-grouped dropdown for switching the active "context" —
401
+ * whatever refId-addressable entity a host app's own thin wrapper maps onto
402
+ * `ContextSwitcherItem[]` (reeve-frontend's brand, reeve-tenant-frontend's
403
+ * tenant, or — once the spawned-company substrate lands — a company
404
+ * cockpit). Orgs are pure visual groupers — headers render ONLY for
405
+ * multi-org accounts; the common single-org case stays a flat list.
406
+ *
407
+ * The menu renders through Radix DropdownMenu so it portals out of a
408
+ * narrow/collapsible rail and stays on-screen — Radix handles collision
409
+ * flip/shift. `collapsed` swaps the trigger for an icon-only button that
410
+ * fits a 56px collapsed rail.
411
+ */
412
+ interface ContextSwitcherItem {
413
+ /** Stable identity for this context — a brand id, tenant id, etc. */
414
+ refId: string;
415
+ name: string;
416
+ /**
417
+ * Pre-resolved favicon URL (BrandGlyph's favicon-first cascade). Any
418
+ * host-specific favicon rules (e.g. reeve-frontend's synthetic-domain
419
+ * guard around `faviconUrl`) must be applied by the host BEFORE mapping
420
+ * into this item — this package stays agnostic of any one host's domain
421
+ * conventions.
422
+ */
423
+ favicon?: string | null;
424
+ /** Wider brand/tenant logo URL (BrandGlyph's second cascade step). */
425
+ logo?: string | null;
426
+ /** Override BrandGlyph's deterministic monogram letter colour. */
427
+ color?: string | null;
428
+ /** Override BrandGlyph's monogram accent-dot colour (DEV-4539). */
429
+ accentColor?: string | null;
430
+ orgId?: string | null;
431
+ orgName?: string | null;
432
+ }
433
+ interface ContextOrgGroup {
434
+ orgId: string | null;
435
+ orgName: string | null;
436
+ items: ContextSwitcherItem[];
437
+ }
438
+ /** Group contexts by owning org. Items sort by name; groups by org name. */
439
+ declare function groupByOrg(items: ContextSwitcherItem[]): ContextOrgGroup[];
440
+ interface ContextSwitcherProps {
441
+ items: ContextSwitcherItem[];
442
+ /** refId of the active context. */
443
+ activeRefId: string | null;
444
+ onSelect: (item: ContextSwitcherItem) => void;
445
+ /** Optional dropdown footer slot, e.g. an "Add brand" CTA. */
446
+ footer?: React.ReactNode;
447
+ /** Trigger label when nothing is active. */
448
+ placeholder?: string;
449
+ /** Icon-only trigger — for a collapsed (56px) nav rail. */
450
+ collapsed?: boolean;
451
+ /** Dropdown placement (Radix). Defaults to bottom/start; Radix auto-flips. */
452
+ side?: "top" | "right" | "bottom" | "left";
453
+ align?: "start" | "center" | "end";
454
+ /** Extra classes merged onto the portaled DropdownMenuContent — e.g. a
455
+ * higher z-index so the dropdown clears a high-z overlay (a mobile
456
+ * drawer above the default z-50 content). */
457
+ contentClassName?: string;
458
+ }
459
+ declare function ContextSwitcher({ items, activeRefId, onSelect, footer, placeholder, collapsed, side, align, contentClassName, }: ContextSwitcherProps): react.JSX.Element;
460
+
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 };