@meetreeve/ui 0.2.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 +134 -0
- package/dist/index.d.mts +328 -0
- package/dist/index.d.ts +328 -0
- package/dist/index.js +955 -0
- package/dist/index.mjs +910 -0
- package/dist/tokens/index.d.mts +31 -0
- package/dist/tokens/index.d.ts +31 -0
- package/dist/tokens/index.js +47 -0
- package/dist/tokens/index.mjs +21 -0
- package/dist/tokens.css +25 -0
- package/package.json +75 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode, RefObject } from 'react';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Brand glyph with a favicon-first cascade (ported/generalised from
|
|
7
|
+
* DEV-4164's `context/brand-glyph`).
|
|
8
|
+
*
|
|
9
|
+
* A tiny square favicon reads far better than a wide wordmark, so we try, in
|
|
10
|
+
* order:
|
|
11
|
+
* 1. `brand.favicon` — the site's own square icon,
|
|
12
|
+
* 2. `brand.logo` rendered `object-contain` (no squish),
|
|
13
|
+
* 3. a high-contrast letter monogram in a deterministic brand colour.
|
|
14
|
+
* Each image load error advances to the next candidate.
|
|
15
|
+
*/
|
|
16
|
+
interface Brand {
|
|
17
|
+
/** Display name — drives the wordmark and the monogram fallback. */
|
|
18
|
+
name: string;
|
|
19
|
+
/** Square favicon URL (first cascade step). Use `faviconUrl(domain)` to derive one. */
|
|
20
|
+
favicon?: string | null;
|
|
21
|
+
/** Wider brand logo URL (second cascade step, rendered `object-contain`). */
|
|
22
|
+
logo?: string | null;
|
|
23
|
+
/** Override the deterministic monogram background colour. */
|
|
24
|
+
color?: string | null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Favicon URL for a brand's canonical domain. Returns null unless `domain`
|
|
28
|
+
* actually looks like a hostname, so non-domain values (e.g. a plain company
|
|
29
|
+
* name) skip straight to the logo/monogram steps.
|
|
30
|
+
*/
|
|
31
|
+
declare function faviconUrl(domain: string | null | undefined): string | null;
|
|
32
|
+
interface BrandGlyphProps {
|
|
33
|
+
brand: Brand;
|
|
34
|
+
size?: "sm" | "lg";
|
|
35
|
+
/** Slightly lighter tint behind image sources (e.g. dropdown rows). */
|
|
36
|
+
subtle?: boolean;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
declare function BrandGlyph({ brand, size, subtle, className }: BrandGlyphProps): react.JSX.Element;
|
|
40
|
+
|
|
41
|
+
interface HeaderNavItem {
|
|
42
|
+
label: string;
|
|
43
|
+
href?: string;
|
|
44
|
+
onSelect?: () => void;
|
|
45
|
+
}
|
|
46
|
+
interface ResponsiveHeaderProps {
|
|
47
|
+
/** Brand for the glyph + wordmark lockup. */
|
|
48
|
+
brand: Brand;
|
|
49
|
+
/**
|
|
50
|
+
* The PRIMARY slot. Always rendered inline with a guaranteed min-width — it
|
|
51
|
+
* never collapses to an icon. Give it a full search field.
|
|
52
|
+
*/
|
|
53
|
+
search: ReactNode;
|
|
54
|
+
/** Secondary nav links — inline on desktop, in the menu on mobile. */
|
|
55
|
+
nav?: HeaderNavItem[];
|
|
56
|
+
/** Auth control (avatar / sign-in) — secondary. */
|
|
57
|
+
auth?: ReactNode;
|
|
58
|
+
/** Theme toggle — secondary. */
|
|
59
|
+
themeToggle?: ReactNode;
|
|
60
|
+
/** Accessible label for the mobile menu button. */
|
|
61
|
+
menuLabel?: string;
|
|
62
|
+
className?: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Brand-adaptive header that fixes the "mobile header crushes the search bar"
|
|
66
|
+
* class of bug.
|
|
67
|
+
*
|
|
68
|
+
* - Search is the primary slot: always inline, floored at `MIN_SIZES.searchInput`.
|
|
69
|
+
* - Under width pressure the OTHER controls collapse first — below `md` the
|
|
70
|
+
* brand wordmark drops to glyph-only and the secondary controls fold into a
|
|
71
|
+
* hamburger disclosure. The search is never touched.
|
|
72
|
+
*
|
|
73
|
+
* A `"use client"` leaf: it's interactive (matchMedia + disclosure state).
|
|
74
|
+
*/
|
|
75
|
+
declare function ResponsiveHeader({ brand, search, nav, auth, themeToggle, menuLabel, className, }: ResponsiveHeaderProps): react.JSX.Element;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* SidebarLadder — the per-tenant JSON artifact (DEV-4351) that pre-calculates
|
|
79
|
+
* every viewport-driven collapse step for AppSidebar. Agent-authored at spawn
|
|
80
|
+
* (DEV-4352) or derived deterministically by `deriveFallbackLadder` (see
|
|
81
|
+
* `./fallback`) when no config exists. The client never invents a layout —
|
|
82
|
+
* `useLadderRung` only SELECTS a rung from this data.
|
|
83
|
+
*/
|
|
84
|
+
interface AppRungItem {
|
|
85
|
+
type: "app";
|
|
86
|
+
moduleId: string;
|
|
87
|
+
}
|
|
88
|
+
interface GroupRungItem {
|
|
89
|
+
type: "group";
|
|
90
|
+
/** Stable id for the folded group (also its React key). */
|
|
91
|
+
id: string;
|
|
92
|
+
label: string;
|
|
93
|
+
/** lucide-react named export, e.g. "Layers" — resolved to a component at render time. */
|
|
94
|
+
icon: string;
|
|
95
|
+
/** moduleIds folded into this group. Non-empty (zod-enforced). */
|
|
96
|
+
children: string[];
|
|
97
|
+
}
|
|
98
|
+
type RungItem = AppRungItem | GroupRungItem;
|
|
99
|
+
interface SidebarRung {
|
|
100
|
+
/** Row count for this rung. Always equals `items.length` (zod-enforced) —
|
|
101
|
+
* each item, whether a bare app row or a folded group, is exactly one row. */
|
|
102
|
+
rows: number;
|
|
103
|
+
/** Top-to-bottom render order. */
|
|
104
|
+
items: RungItem[];
|
|
105
|
+
}
|
|
106
|
+
interface SidebarLadder {
|
|
107
|
+
version: number;
|
|
108
|
+
/** Ordered rungs, most-expanded first. Rung 0 is always the fully-expanded
|
|
109
|
+
* layout (no folded groups); `rows` strictly decreases thereafter. */
|
|
110
|
+
rungs: SidebarRung[];
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* SidebarLadder zod schema — enforces every invariant from the DEV-4351 spec:
|
|
114
|
+
* - rung 0 is fully expanded (no folded groups — every item is an app row).
|
|
115
|
+
* - `rows` strictly decreases rung-over-rung.
|
|
116
|
+
* - a rung's declared `rows` matches its actual item count.
|
|
117
|
+
* - group `children` is non-empty (covered at the item level above).
|
|
118
|
+
* - every module appears EXACTLY ONCE per rung (as a row, or inside exactly
|
|
119
|
+
* one group's children — never both, never twice).
|
|
120
|
+
* - the set of modules is identical across every rung — folding can never
|
|
121
|
+
* invent or lose a module.
|
|
122
|
+
*/
|
|
123
|
+
declare const zSidebarLadder: z.ZodObject<{
|
|
124
|
+
version: z.ZodNumber;
|
|
125
|
+
rungs: z.ZodArray<z.ZodObject<{
|
|
126
|
+
rows: z.ZodNumber;
|
|
127
|
+
items: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
128
|
+
type: z.ZodLiteral<"app">;
|
|
129
|
+
moduleId: z.ZodString;
|
|
130
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
131
|
+
type: z.ZodLiteral<"group">;
|
|
132
|
+
id: z.ZodString;
|
|
133
|
+
label: z.ZodString;
|
|
134
|
+
icon: z.ZodString;
|
|
135
|
+
children: z.ZodArray<z.ZodString>;
|
|
136
|
+
}, z.core.$strip>], "type">>;
|
|
137
|
+
}, z.core.$strip>>;
|
|
138
|
+
}, z.core.$strip>;
|
|
139
|
+
type SidebarLadderParsed = z.infer<typeof zSidebarLadder>;
|
|
140
|
+
|
|
141
|
+
interface UseLadderRungOptions {
|
|
142
|
+
/** Approximate rendered height of one nav row, px. */
|
|
143
|
+
rowHeight?: number;
|
|
144
|
+
/** Extra rows of slack required before stepping back UP to a less-folded
|
|
145
|
+
* rung, so a resize sitting right at a boundary doesn't flap. */
|
|
146
|
+
hysteresisRows?: number;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Measures a nav container via ResizeObserver and selects the largest ladder
|
|
150
|
+
* rung (rungs are ordered most-expanded first, `rows` strictly decreasing)
|
|
151
|
+
* whose row requirement fits `rowsAvailable = floor(height / rowHeight)`.
|
|
152
|
+
*
|
|
153
|
+
* SSR-safe: the initial state is always rung 0 (fully expanded) — the
|
|
154
|
+
* measurement only runs client-side inside `useLayoutEffect`, which fires
|
|
155
|
+
* before the browser paints, so there's no visible rung-snap flash on first
|
|
156
|
+
* load (server markup and the first client paint agree).
|
|
157
|
+
*
|
|
158
|
+
* Hysteresis: stepping DOWN to a more-folded rung happens the instant it no
|
|
159
|
+
* longer fits. Stepping back UP to a less-folded rung requires an EXTRA
|
|
160
|
+
* `hysteresisRows` of slack beyond that rung's own row requirement, so a
|
|
161
|
+
* resize hovering right at a boundary doesn't flip back and forth.
|
|
162
|
+
*/
|
|
163
|
+
declare function useLadderRung(ladder: SidebarLadder, containerRef: RefObject<HTMLElement | null>, options?: UseLadderRungOptions): SidebarRung;
|
|
164
|
+
|
|
165
|
+
interface AppSidebarModuleDef {
|
|
166
|
+
moduleId: string;
|
|
167
|
+
label: string;
|
|
168
|
+
/** lucide-react named export, e.g. "Mail". */
|
|
169
|
+
icon: string;
|
|
170
|
+
group: string;
|
|
171
|
+
href: string;
|
|
172
|
+
}
|
|
173
|
+
interface AppSidebarBrand {
|
|
174
|
+
label: string;
|
|
175
|
+
href: string;
|
|
176
|
+
}
|
|
177
|
+
interface AppSidebarTopItem {
|
|
178
|
+
href: string;
|
|
179
|
+
label: string;
|
|
180
|
+
/** lucide-react named export. */
|
|
181
|
+
icon: string;
|
|
182
|
+
}
|
|
183
|
+
interface AppSidebarProps {
|
|
184
|
+
/** Raw, parsed (but not yet zod-validated) sidebar-ladder.json — or
|
|
185
|
+
* `undefined`/`null` when no per-tenant config exists. A ladder that fails
|
|
186
|
+
* validation degrades to the deterministic fallback, same as no config. */
|
|
187
|
+
ladder?: unknown;
|
|
188
|
+
/** The full module catalog — resolves each row's label/icon/href and
|
|
189
|
+
* drives the fallback derivation when no ladder config is supplied. */
|
|
190
|
+
modules: AppSidebarModuleDef[];
|
|
191
|
+
/** Group fold order — the LAST group folds first (fallback derivation +
|
|
192
|
+
* drift reconciliation both use this). */
|
|
193
|
+
groupOrder: string[];
|
|
194
|
+
/** Current pathname — drives the active-route highlight. */
|
|
195
|
+
activePath: string;
|
|
196
|
+
/** Brand block rendered above the ladder. */
|
|
197
|
+
brand?: AppSidebarBrand;
|
|
198
|
+
/** A fixed nav row above the ladder, unaffected by rung folding (e.g. an "Overview" link). */
|
|
199
|
+
topItem?: AppSidebarTopItem;
|
|
200
|
+
/** Accessible label for the mobile hamburger button. */
|
|
201
|
+
menuLabel?: string;
|
|
202
|
+
ladderOptions?: UseLadderRungOptions;
|
|
203
|
+
/** Merged onto the desktop `<aside>` itself — there is no extra wrapping
|
|
204
|
+
* element (see the render note below on why that matters for height). */
|
|
205
|
+
className?: string;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* AppSidebar (DEV-4351) — the viewport-driven auto-collapse ladder sidebar.
|
|
209
|
+
*
|
|
210
|
+
* Desktop: measures its nav container and snaps to the largest ladder rung
|
|
211
|
+
* that fits (see `useLadderRung`); folded groups render as a `Flyout`.
|
|
212
|
+
* Mobile: the ladder is desktop-only — a self-contained hamburger + overlay
|
|
213
|
+
* drawer renders rung 0 (fully expanded), scrollable. No shadcn/Sheet
|
|
214
|
+
* dependency, matching ResponsiveHeader's existing disclosure pattern in
|
|
215
|
+
* this same package.
|
|
216
|
+
*/
|
|
217
|
+
declare function AppSidebar({ ladder, modules, groupOrder, activePath, brand, topItem, menuLabel, ladderOptions, className, }: AppSidebarProps): react.JSX.Element;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Minimal shape AppSidebar's fallback derivation needs from a module registry
|
|
221
|
+
* entry — deliberately decoupled from any one app's full module-def shape
|
|
222
|
+
* (label/icon/description live in the consumer's own registry and are
|
|
223
|
+
* resolved at render time by AppSidebar, not baked into the ladder here).
|
|
224
|
+
*/
|
|
225
|
+
interface FallbackModuleDef {
|
|
226
|
+
moduleId: string;
|
|
227
|
+
group: string;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Deterministic ladder derivation for when `sidebar-ladder.json` is absent or
|
|
231
|
+
* fails zod validation — a bad agent output can never brick a tenant's nav.
|
|
232
|
+
*
|
|
233
|
+
* Rung 0 is the fully-expanded layout (one row per module, registry order
|
|
234
|
+
* preserved). Each subsequent rung folds one more group into a single parent
|
|
235
|
+
* row, walking `groupOrder` BACK TO FRONT — the LAST group folds first (e.g.
|
|
236
|
+
* for Engage/Data/Intelligence/Commerce/Operations, Operations folds first,
|
|
237
|
+
* then Commerce, then Intelligence, then Data, then Engage last).
|
|
238
|
+
*
|
|
239
|
+
* A group with fewer than 2 members is folded into the accumulated set
|
|
240
|
+
* without emitting its own rung — collapsing a 1-member group nets zero row
|
|
241
|
+
* reduction (one app row becomes one group row), which would violate the
|
|
242
|
+
* ladder's strictly-decreasing-rows invariant if emitted as its own step. It
|
|
243
|
+
* still gets folded — just bundled into the rung of whichever fold next
|
|
244
|
+
* produces a real row-count decrease.
|
|
245
|
+
*/
|
|
246
|
+
declare function deriveFallbackLadder(registry: FallbackModuleDef[], groupOrder: string[]): SidebarLadder;
|
|
247
|
+
/**
|
|
248
|
+
* Reconciles a (possibly stale) ladder against the live registry — mirrors
|
|
249
|
+
* `use-sidebar-config.ts`'s `reconcileConfig()` (unknown ids dropped, missing
|
|
250
|
+
* ids appended). Ladder/registry drift can never brick or hide a module:
|
|
251
|
+
* - a moduleId the ladder references but the registry no longer has is
|
|
252
|
+
* dropped from every rung (and from any group's children).
|
|
253
|
+
* - a moduleId the registry has but the ladder doesn't reference anywhere in
|
|
254
|
+
* rung 0 is appended as a new top-level row on EVERY rung.
|
|
255
|
+
*/
|
|
256
|
+
declare function reconcileLadder(ladder: SidebarLadder, registry: FallbackModuleDef[]): SidebarLadder;
|
|
257
|
+
/**
|
|
258
|
+
* The single entry point AppSidebar uses to resolve the ladder it will
|
|
259
|
+
* render: validate the raw (parsed-JSON) ladder if one was supplied,
|
|
260
|
+
* reconcile it against the live registry, and fall back to a fresh
|
|
261
|
+
* deterministic derivation on absence OR validation failure.
|
|
262
|
+
*
|
|
263
|
+
* The reconciled result is re-validated against `zSidebarLadder` before it's
|
|
264
|
+
* returned (deep-review finding, DEV-4351): `reconcileLadder`'s per-rung
|
|
265
|
+
* filtering can — on a specific drift shape (dropping enough of a folded
|
|
266
|
+
* group's members that a rung's row count converges with its neighbor's) —
|
|
267
|
+
* produce a ladder that no longer satisfies the strictly-decreasing-rows
|
|
268
|
+
* invariant, which would silently orphan a rung in `useLadderRung`'s
|
|
269
|
+
* first-fit scan. A bad reconciliation degrades to the same deterministic
|
|
270
|
+
* fallback as a bad or missing config — it never ships invalid output.
|
|
271
|
+
*/
|
|
272
|
+
declare function resolveLadder(raw: unknown, registry: FallbackModuleDef[], groupOrder: string[]): SidebarLadder;
|
|
273
|
+
|
|
274
|
+
interface FlyoutChild {
|
|
275
|
+
moduleId: string;
|
|
276
|
+
label: string;
|
|
277
|
+
href: string;
|
|
278
|
+
active: boolean;
|
|
279
|
+
}
|
|
280
|
+
interface FlyoutProps {
|
|
281
|
+
label: string;
|
|
282
|
+
/** lucide-react named export, e.g. "Layers". */
|
|
283
|
+
icon: string;
|
|
284
|
+
/** True when the active route's module is folded inside this group — the
|
|
285
|
+
* PARENT row shows the active state; the child itself is highlighted via
|
|
286
|
+
* its own `active` flag. */
|
|
287
|
+
active: boolean;
|
|
288
|
+
children: FlyoutChild[];
|
|
289
|
+
className?: string;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* The "offshoot" for a folded sidebar group (DEV-4351): hover opens a
|
|
293
|
+
* horizontal flyout listing the group's children on desktop; click toggles it
|
|
294
|
+
* for keyboard/touch parity. `aria-expanded`, focus moves into the panel on
|
|
295
|
+
* a CLICK/keyboard open (never on a mere hover — CR CLI finding), Esc closes
|
|
296
|
+
* and returns focus to the trigger — via the shared `useDismissableDisclosure`
|
|
297
|
+
* (mirrors ResponsiveHeader's original mobile-disclosure a11y pattern in this
|
|
298
|
+
* same package).
|
|
299
|
+
*
|
|
300
|
+
* Plain links, not `menu`/`menuitem` roles (CR CLI finding): those ARIA roles
|
|
301
|
+
* promise a roving-tabindex, arrow-key/Home/End keyboard contract this
|
|
302
|
+
* disclosure doesn't implement — misusing them would announce a keyboard
|
|
303
|
+
* model to assistive tech that isn't actually there. Native `<a>` Tab
|
|
304
|
+
* navigation is fully accessible on its own without that promise.
|
|
305
|
+
*
|
|
306
|
+
* Hover and click are independent open sources (CR CLI finding): they used
|
|
307
|
+
* to share one boolean, so a mouse user hovering (which opened it) and then
|
|
308
|
+
* clicking the row — the normal way to interact with it — immediately
|
|
309
|
+
* toggled it closed again. `hoverOpen`/`clickOpen` are now tracked
|
|
310
|
+
* separately; the panel shows while EITHER is true, and only a click/
|
|
311
|
+
* keyboard-driven open moves focus into it.
|
|
312
|
+
*
|
|
313
|
+
* The panel is positioned `position: fixed` at the trigger's actual screen
|
|
314
|
+
* coordinates, not CSS-anchored (`absolute; left: 100%`) to its nearest
|
|
315
|
+
* positioned ancestor (outcome-eval finding, live browser check): that
|
|
316
|
+
* ancestor sits inside AppSidebar's `<nav>`, which has `overflow-y: auto` —
|
|
317
|
+
* a CSS quirk auto-promotes the OTHER axis to `overflow-x: auto` too, so an
|
|
318
|
+
* `absolute`-positioned panel extending past the narrow sidebar's edge
|
|
319
|
+
* inflated the nav's own scrollWidth, and the browser auto-scrolled the nav
|
|
320
|
+
* horizontally to reveal it — hiding every OTHER nav item off-screen.
|
|
321
|
+
* `position: fixed` doesn't contribute to any scrolling ancestor's
|
|
322
|
+
* scrollable overflow at all, so it escapes the problem entirely rather
|
|
323
|
+
* than working around it. jsdom never computes real scrollable-overflow
|
|
324
|
+
* geometry, so no unit test caught this — only an actual browser render did.
|
|
325
|
+
*/
|
|
326
|
+
declare function Flyout({ label, icon, active, children, className }: FlyoutProps): react.JSX.Element;
|
|
327
|
+
|
|
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 };
|