@boost33/ui 0.1.0 → 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.
@@ -0,0 +1,69 @@
1
+ import { KeyboardEvent } from 'react';
2
+ /** What `aria-haspopup` announces. Per consumer: a product list is not a dialog. */
3
+ export type AnchoredOverlayPopup = 'menu' | 'dialog' | 'listbox' | 'tree' | 'grid';
4
+ export interface UseAnchoredOverlayOptions {
5
+ /**
6
+ * The value for the trigger's `aria-haspopup`. Per consumer, not fixed:
7
+ * B33-493 records `dialog` for the account menu, and the switcher's product
8
+ * list is a `menu`. One hardcoded value would misannounce the other.
9
+ *
10
+ * Optional because a ⌘K-opened overlay (B33-495) may have no trigger element
11
+ * at all. Omit it and `triggerProps` is not produced -- there is nothing to
12
+ * spread it onto, and `aria-expanded`/`aria-controls` would announce a
13
+ * trigger that does not exist. Focus still restores, to the captured opener.
14
+ */
15
+ haspopup?: AnchoredOverlayPopup;
16
+ /**
17
+ * Drives the overlay from outside. Supplying it makes the overlay
18
+ * controlled: it opens and closes only when this changes, and every attempt
19
+ * to move it arrives at `onOpenChange` instead. A specimen route passes it
20
+ * to draw the open panel with no interaction, which is Stage A.
21
+ */
22
+ open?: boolean;
23
+ /** Starting state when uncontrolled. Ignored once `open` is supplied. */
24
+ defaultOpen?: boolean;
25
+ /**
26
+ * Every open and close, including the ones this hook decides on its own. A
27
+ * consumer whose ancestor clips overflow needs this to un-clip while the
28
+ * panel overhangs -- the sidebar does exactly that in boostledger.
29
+ */
30
+ onOpenChange?: (open: boolean) => void;
31
+ /**
32
+ * The modal variant, for the command palette (B33-495) alone. It additionally
33
+ * traps focus inside the panel, marks the rest of the document `inert`, and
34
+ * skips the focusout dismissal listener -- under a trap focus never leaves the
35
+ * panel, so that listener could never fire and is not attached. Everything
36
+ * else in the contract is unchanged: Escape restores to the opener and an
37
+ * outside (backdrop) pointer-down closes.
38
+ *
39
+ * A per-consumer flag, and only the palette sets it. AppSwitcher, AccountMenu
40
+ * and NotificationCenter never become modal -- B33-479 carries that as a hard
41
+ * acceptance criterion. See docs/adr/0009.
42
+ */
43
+ modal?: boolean;
44
+ }
45
+ export interface AnchoredOverlay {
46
+ open: boolean;
47
+ /** Move the overlay. On a controlled overlay this only reports. */
48
+ setOpen: (next: boolean) => void;
49
+ toggle: () => void;
50
+ /** Close and return focus, for a row that acts and dismisses. */
51
+ close: () => void;
52
+ /**
53
+ * Undefined when no `haspopup` was supplied: a trigger-less overlay has
54
+ * nothing to carry these attributes, so the hook produces none.
55
+ */
56
+ triggerProps?: {
57
+ ref: (node: HTMLElement | null) => void;
58
+ 'aria-haspopup': AnchoredOverlayPopup;
59
+ 'aria-expanded': boolean;
60
+ 'aria-controls': string | undefined;
61
+ onKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
62
+ };
63
+ panelProps: {
64
+ ref: (node: HTMLElement | null) => void;
65
+ id: string;
66
+ onKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
67
+ };
68
+ }
69
+ export declare function useAnchoredOverlay(options: UseAnchoredOverlayOptions): AnchoredOverlay;
@@ -0,0 +1,65 @@
1
+ import { ReactNode } from 'react';
2
+ import { TopBarProps } from '../TopBar/TopBar';
3
+ import { PageHeaderProps } from '../PageHeader/PageHeader';
4
+ /** One nav row, described as data. The consumer never hands the shell markup. */
5
+ export interface AppShellNavItem {
6
+ /** The row's name. Truncates rather than wrapping; on the rail it collapses to the accessibility tree. */
7
+ label: string;
8
+ /** The leading icon. Decorative -- the label carries the name. On the rail it is all that shows. */
9
+ icon?: ReactNode;
10
+ /** Marks this row as the current page. Sets `aria-current="page"`. */
11
+ active?: boolean;
12
+ /** Trailing-slot count. Omit to leave the slot empty; it still reserves its width so counts line up. */
13
+ count?: number;
14
+ /** Renders `count` in the primary color instead of muted. */
15
+ countUrgent?: boolean;
16
+ /** Where the row goes. A nav row's destination is data; the shell renders it as a link. */
17
+ href?: string;
18
+ }
19
+ /** One nav section: an optional header over a run of rows. */
20
+ export interface AppShellNavSection {
21
+ /** The section header. Omit for a headerless section (the nav's first section has none). On the rail every header drops. */
22
+ label?: string;
23
+ /** The section's rows, as data. */
24
+ items: AppShellNavItem[];
25
+ }
26
+ export interface AppShellProps {
27
+ /**
28
+ * The product this shell renders for. The consumer names it once, here, and
29
+ * the shell carries it down -- it labels the sidebar and its navigation, and
30
+ * nothing above the nav (the Boost33 wordmark and brand) changes between
31
+ * products. This one prop is the whole of the shell's product-awareness.
32
+ */
33
+ product: string;
34
+ /** The navigation, as data. No product hardcodes rows; the shell composes them. */
35
+ nav: AppShellNavSection[];
36
+ /** The top bar's content -- the breadcrumb and the shell actions on its right. */
37
+ topBar: TopBarProps;
38
+ /** The page header's content -- the page title, its optional subtitle and trailing controls. */
39
+ pageHeader: PageHeaderProps;
40
+ /** The Boost33 mark for the wordmark lockup. Passed in, the way Wordmark takes it; on the rail it is all that shows. */
41
+ mark?: ReactNode;
42
+ /**
43
+ * The product switcher, sitting under the wordmark. A slot: the registry it
44
+ * draws is served by Identity (ADR-0003), not owned here. Render its own
45
+ * collapsed form for the rail.
46
+ */
47
+ switcher?: ReactNode;
48
+ /**
49
+ * The wallet chip, pinned above the footer. A slot: the balance comes from
50
+ * `@boost33/ledger-client`, never read here. Render its own collapsed form
51
+ * for the rail.
52
+ */
53
+ wallet?: ReactNode;
54
+ /** The sidebar footer, below the wallet chip. A slot -- the product's own. */
55
+ footer?: ReactNode;
56
+ /**
57
+ * The sidebar collapsed to the 56px rail. A state of the shell: the sidebar
58
+ * narrows and every piece it owns collapses off this one flag. The slots
59
+ * (switcher, wallet, footer) render their own collapsed form.
60
+ */
61
+ collapsed?: boolean;
62
+ /** The page body. Sits on white, below the header. */
63
+ children: ReactNode;
64
+ }
65
+ export declare function AppShell({ product, nav, topBar, pageHeader, mark, switcher, wallet, footer, collapsed, children, }: AppShellProps): import("react").JSX.Element;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The smallest size the artwork survives, in px. Recorded, not enforced — the
3
+ * charte's brand guide warns the monogram stops working below about 20px: the
4
+ * bowls of the B and the hole in the o close up and the whole thing reads as a
5
+ * dark blob. Anything smaller needs a simplified variant from the parent, not
6
+ * this scaled down. Nothing in this package ships below it.
7
+ *
8
+ * This floor is the **monogram's alone**. The product marks have the opposite
9
+ * requirement — they are drawn on a grid chosen so one path renders correctly
10
+ * from 96px down to 20px — so do not generalise this number into a shared rule
11
+ * when the second mark lands.
12
+ */
13
+ export declare const BOOST33_MARK_MIN_LEGIBLE_PX = 20;
14
+ export interface Boost33MarkProps {
15
+ /**
16
+ * Rendered width and height in px. Read BOOST33_MARK_MIN_LEGIBLE_PX before
17
+ * going small.
18
+ */
19
+ size?: number;
20
+ /**
21
+ * An accessible name. Omit it — the default — and the mark is decorative:
22
+ * `aria-hidden`, contributing nothing to the accessibility tree. That is the
23
+ * right answer in the lockup, where the word "Boost33" sits beside it and a
24
+ * name here would only make a screen reader say it twice. Pass one only where
25
+ * the mark stands alone as the sole carrier of meaning.
26
+ */
27
+ title?: string;
28
+ /** Escape hatch for a consumer that needs to position the mark. */
29
+ className?: string;
30
+ }
31
+ /**
32
+ * The Boost33 mark — the "Bo" squircle monogram.
33
+ *
34
+ * **This artwork is not ours.** The monogram and the wordmark are set by the
35
+ * Boost33 charte graphique and a product cannot change them; the parent's brand
36
+ * documentation lists the monogram as "black tile, orange letterform" under what
37
+ * it owns. The paths below are lifted verbatim out of the official lockup at
38
+ * `docs/brand/logo-of-noir-orange.svg` in the Wallet repo (`boostledger`), by way
39
+ * of its `src/ui/primitives/icons.tsx` — reproduced, not redrawn. If the mark
40
+ * ever changes, it changes in that SVG first and this is re-extracted from it.
41
+ * Nobody here gets to nudge a curve.
42
+ *
43
+ * What stood in for it in this package until B33-556 was a Stage A placeholder in
44
+ * the specimen: an orange tile with a `<text>B</text>` typed on top. The Wallet
45
+ * carried the same mistake — tile and letterform the wrong way round — until
46
+ * 9 Sep 2026, which is why the original fidelity baselines showed it too. It had
47
+ * been wrong against the charte the whole time, not just against Paper.
48
+ *
49
+ * How it is built, since the fills are not obvious from the path list: the first
50
+ * path is the black squircle with the B, the o and the underscore knocked out of
51
+ * it as counter-wound subpaths, so those three show through as white. The orange
52
+ * ring is then drawn into the o, and the last three paths fill the B's two bowls
53
+ * and the o's centre back to black. **Order matters between the ring and the
54
+ * centre** — swap them and the o fills solid orange. Nothing else overlaps.
55
+ *
56
+ * The two `fill: none` paths in the source SVG are omitted. They are export
57
+ * artefacts that render nothing, and dropping them does not affect the winding of
58
+ * the path above them.
59
+ *
60
+ * The viewBox is the monogram's own bounding box **inside the lockup**, which is
61
+ * why it is an offset rectangle rather than starting at the origin. Do not "tidy"
62
+ * it to `0 0 …` without re-deriving the paths; they carry absolute coordinates
63
+ * and translating one without the other silently crops the mark.
64
+ */
65
+ export declare function Boost33Mark({ size, title, className }: Boost33MarkProps): import("react").JSX.Element;
@@ -2,6 +2,12 @@ import { ReactNode } from 'react';
2
2
  export interface NavItemProps {
3
3
  /** The nav label. Truncates with an ellipsis rather than wrapping. */
4
4
  children: ReactNode;
5
+ /**
6
+ * The leading icon. Decorative -- the label carries the name, so pass an
7
+ * `aria-hidden` glyph. On the rail it is the only thing that shows, the
8
+ * label having collapsed to the accessibility tree.
9
+ */
10
+ icon?: ReactNode;
5
11
  /** Marks this row as the current page. Sets `aria-current="page"`. */
6
12
  active?: boolean;
7
13
  /** Sidebar-on-the-rail state. Hides the label and trailing slot visually; the label stays in the accessibility tree. */
@@ -13,4 +19,4 @@ export interface NavItemProps {
13
19
  href?: string;
14
20
  onClick?: () => void;
15
21
  }
16
- export declare function NavItem({ children, active, collapsed, count, countUrgent, href, onClick }: NavItemProps): import("react").JSX.Element;
22
+ export declare function NavItem({ children, icon, active, collapsed, count, countUrgent, href, onClick }: NavItemProps): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { ReactNode } from 'react';
2
+ export interface NavSectionProps {
3
+ /**
4
+ * The section header -- the small uppercase label above the rows. Omit for a
5
+ * section with no header (the sidebar's first section has none). Passed in
6
+ * its natural case; the uppercasing is presentational.
7
+ */
8
+ label?: string;
9
+ /** The section's rows -- NavItems, passed in as data, never hardcoded here. */
10
+ children: ReactNode;
11
+ }
12
+ export declare function NavSection({ label, children }: NavSectionProps): import("react").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { ReactNode } from 'react';
2
+ export interface PageHeaderProps {
3
+ /** The page's name. Renders as the page heading. */
4
+ title: ReactNode;
5
+ /** A line under the title. Omit for a title-only header. */
6
+ subtitle?: ReactNode;
7
+ /** Trailing controls, aligned to the right edge. Omit to leave the row empty. */
8
+ actions?: ReactNode;
9
+ }
10
+ export declare function PageHeader({ title, subtitle, actions }: PageHeaderProps): import("react").JSX.Element;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The four products Identity serves, as one list. Everything else keys off it:
3
+ * ProductSlug is derived from it, and GLYPHS is typed against it so adding a
4
+ * slug without artwork is a compile error rather than a mark that silently
5
+ * draws nothing. These are Identity's *slugs*, not display names -- the Wallet
6
+ * keys its own record on `'Boost Score' | ... | 'Wallet'`, which a rename
7
+ * breaks, and that is the exact trap this ordering avoids.
8
+ */
9
+ export declare const PRODUCT_SLUGS: readonly ["wallet", "score", "voice", "outbound"];
10
+ /** One of Identity's four product slugs. */
11
+ export type ProductSlug = (typeof PRODUCT_SLUGS)[number];
12
+ export interface ProductMarkProps {
13
+ /**
14
+ * Identity's product slug. An unknown one -- a product the served registry
15
+ * carries but this package has no artwork for yet -- draws the bare tile
16
+ * rather than throwing (ADR-0003's safe degrade), so the registry can run
17
+ * ahead of a mark release.
18
+ */
19
+ slug: string;
20
+ /** Rendered width and height in px. Defaults to 20, the Wallet's own default. */
21
+ size?: number;
22
+ /**
23
+ * Dims the tile for a product the account is not entitled to -- the switcher's
24
+ * unowned rows need it. It swaps the **ground only**, to --color-neutral-300;
25
+ * the glyph stays white. Fading the glyph too would cost the shape the one
26
+ * thing telling the four marks apart, and it is a prop rather than a wrapper
27
+ * `opacity-40 grayscale` filter because that lands on a mid-grey visibly
28
+ * darker than the grey Paper draws.
29
+ */
30
+ muted?: boolean;
31
+ /**
32
+ * An accessible name. Omit it -- the default -- and the mark is decorative:
33
+ * `aria-hidden`, contributing nothing to the accessibility tree. That is the
34
+ * right answer in the switcher, where the product's name sits beside it and a
35
+ * name here would only make a screen reader say it twice. Pass one only where
36
+ * the mark stands alone as the sole carrier of meaning.
37
+ */
38
+ title?: string;
39
+ /** Escape hatch for a consumer that needs to position the mark. */
40
+ className?: string;
41
+ }
42
+ /**
43
+ * The tile-and-glyph mark for one Boost33 product -- a rounded --color-neutral-950
44
+ * square with the product's glyph in --color-primary-foreground. The four marks
45
+ * share one palette and are told apart by shape alone; there is zero brand
46
+ * orange in any of them.
47
+ */
48
+ export declare function ProductMark({ slug, size, muted, title, className }: ProductMarkProps): import("react").JSX.Element;
@@ -0,0 +1,19 @@
1
+ import { ReactNode } from 'react';
2
+ export interface SidebarProps {
3
+ /** Names the sidebar landmark, so it stays distinct from other regions on the page. */
4
+ label: string;
5
+ /** Sidebar-on-the-rail state. Narrows to the 56px rail; the pieces inside collapse themselves. */
6
+ collapsed?: boolean;
7
+ /**
8
+ * What the sidebar holds -- wordmark, switcher, nav sections, wallet chip, footer. Passed in, never hardcoded.
9
+ *
10
+ * **Put the nav in a `<nav>` element that is a direct child of the sidebar.**
11
+ * That element is the sidebar's scroll region: a nav longer than the column
12
+ * scrolls inside itself, and everything above and below it -- the wordmark,
13
+ * the switcher, the wallet chip, the footer -- stays pinned. A nav nested
14
+ * inside another element does not get that, and says so in development. See
15
+ * ADR-0005.
16
+ */
17
+ children: ReactNode;
18
+ }
19
+ export declare function Sidebar({ label, collapsed, children }: SidebarProps): import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { ReactNode } from 'react';
2
+ export interface TopBarProps {
3
+ /** The breadcrumb naming the current location, on the left of the bar. */
4
+ breadcrumb: ReactNode;
5
+ /** Shell actions on the right -- search, notifications, the account. Omit to leave the right empty. */
6
+ actions?: ReactNode;
7
+ }
8
+ export declare function TopBar({ breadcrumb, actions }: TopBarProps): import("react").JSX.Element;
@@ -0,0 +1,27 @@
1
+ export interface WalletChipProps {
2
+ /** Remaining granted tokens. Granted burns first and expires. */
3
+ granted: number;
4
+ /** Remaining purchased tokens. Purchased never expires. */
5
+ purchased: number;
6
+ /**
7
+ * The grant issued for this cycle. The denominator for "how much has burned",
8
+ * and so for the threshold; drawn as the bar's full width alongside purchased
9
+ * so the filled portion shortens as the month runs.
10
+ */
11
+ grantCycle: number;
12
+ /** When the current grant expires, preformatted for display, e.g. "30 Sep". */
13
+ grantExpires?: string;
14
+ /**
15
+ * The threshold state's runway line, preformatted, e.g. "About four days left
16
+ * at your current burn." Passed in rather than derived: the burn estimate is
17
+ * the product's to make, not the chip's -- the chip renders what it is given.
18
+ */
19
+ burnNote?: string;
20
+ /** The balance could not be confirmed. Renders as unknown, never as zero. */
21
+ unknown?: boolean;
22
+ /** The 56px rail. Abbreviates the balance and turns the split on its side. */
23
+ collapsed?: boolean;
24
+ /** Top up. Wiring for the one button the sidebar is allowed to have. */
25
+ onTopUp?: () => void;
26
+ }
27
+ export declare function WalletChip({ granted, purchased, grantCycle, grantExpires, burnNote, unknown, collapsed, onTopUp }: WalletChipProps): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ /** The full balance, thousands grouped: 4600 -> "4,600". */
2
+ export declare function formatTokens(value: number): string;
3
+ /**
4
+ * The rail's short form: one decimal thousand, always. At 56px there is no
5
+ * room for four digits and a separator, and "4.6k" still answers "am I fine?".
6
+ */
7
+ export declare function abbreviateThousands(value: number): string;
@@ -0,0 +1,8 @@
1
+ /** The card mark shown on the rail, where there is no room for the word "Wallet". */
2
+ export declare function WalletMark({ className }: {
3
+ className?: string;
4
+ }): import("react").JSX.Element;
5
+ /** The retry affordance shown when the balance could not be confirmed. */
6
+ export declare function RetryMark({ className }: {
7
+ className?: string;
8
+ }): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { ReactNode } from 'react';
2
+ export interface WordmarkProps {
3
+ /**
4
+ * The Boost33 mark -- the logo artwork, owned by its own ticket. Passed in
5
+ * rather than hardcoded here so the wordmark does not carry a second copy of
6
+ * the mark. On the rail it is all that shows.
7
+ */
8
+ mark?: ReactNode;
9
+ /** Sidebar-on-the-rail state. Hides the "Boost33" name; the mark stays. */
10
+ collapsed?: boolean;
11
+ }
12
+ export declare function Wordmark({ mark, collapsed }: WordmarkProps): import("react").JSX.Element;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,22 @@
1
+ export { Sidebar } from './components/Sidebar/Sidebar';
2
+ export type { SidebarProps } from './components/Sidebar/Sidebar';
3
+ export { NavSection } from './components/NavSection/NavSection';
4
+ export type { NavSectionProps } from './components/NavSection/NavSection';
5
+ export { Wordmark } from './components/Wordmark/Wordmark';
6
+ export type { WordmarkProps } from './components/Wordmark/Wordmark';
7
+ export { Boost33Mark, BOOST33_MARK_MIN_LEGIBLE_PX } from './components/Boost33Mark/Boost33Mark';
8
+ export type { Boost33MarkProps } from './components/Boost33Mark/Boost33Mark';
9
+ export { ProductMark, PRODUCT_SLUGS } from './components/ProductMark/ProductMark';
10
+ export type { ProductMarkProps, ProductSlug } from './components/ProductMark/ProductMark';
1
11
  export { NavItem } from './components/NavItem/NavItem';
2
12
  export type { NavItemProps } from './components/NavItem/NavItem';
3
- export { NAV_HEIGHT_BUDGET_PX, checkNavHeightBudget, measureNavHeight } from './nav-height-budget';
4
- export type { NavHeightBudget } from './nav-height-budget';
13
+ export { WalletChip } from './components/WalletChip/WalletChip';
14
+ export type { WalletChipProps } from './components/WalletChip/WalletChip';
15
+ export { TopBar } from './components/TopBar/TopBar';
16
+ export type { TopBarProps } from './components/TopBar/TopBar';
17
+ export { PageHeader } from './components/PageHeader/PageHeader';
18
+ export type { PageHeaderProps } from './components/PageHeader/PageHeader';
19
+ export { AppShell } from './components/AppShell/AppShell';
20
+ export type { AppShellProps, AppShellNavSection, AppShellNavItem } from './components/AppShell/AppShell';
21
+ export { NAV_HEIGHT_BUDGET_PX, NAV_SECTION_HEADER_PX, NAV_ITEM_PX, NAV_SHELL_FIXED_PX, checkNavHeightBudget, measureNavHeight, navContentHeight, navBudgetForViewport, } from './nav-height-budget';
22
+ export type { NavHeightBudget, NavSectionShape } from './nav-height-budget';
package/dist/index.js CHANGED
@@ -1,24 +1,16 @@
1
- import { jsx as o, jsxs as _, Fragment as f } from "react/jsx-runtime";
2
- const g = "_row_5f12t_4", m = "_label_5f12t_36", d = "_trailingSlot_5f12t_62", h = "_count_5f12t_76", p = "_countUrgent_5f12t_80", a = {
3
- row: g,
4
- label: m,
5
- trailingSlot: d,
6
- count: h,
7
- countUrgent: p
8
- };
9
- function w({ children: t, active: n = !1, collapsed: i = !1, count: e, countUrgent: u = !1, href: r, onClick: l }) {
10
- const c = {
11
- "data-active": n ? "true" : void 0,
12
- "data-collapsed": i ? "true" : void 0,
13
- "aria-current": n ? "page" : void 0
14
- }, s = /* @__PURE__ */ _(f, { children: [
15
- /* @__PURE__ */ o("span", { className: a.label, children: t }),
16
- /* @__PURE__ */ o("span", { className: a.trailingSlot, children: e != null && /* @__PURE__ */ o("span", { className: u ? a.countUrgent : a.count, children: e }) })
17
- ] });
18
- return r ? /* @__PURE__ */ o("a", { className: a.row, href: r, onClick: l, ...c, children: s }) : /* @__PURE__ */ o("button", { type: "button", className: a.row, onClick: l, ...c, children: s });
1
+ import { jsx as e, jsxs as a, Fragment as k } from "react/jsx-runtime";
2
+ import { useRef as B, useEffect as I, useId as L } from "react";
3
+ const H = 700, C = 40, E = 32, P = 198;
4
+ function lt(t) {
5
+ return t.reduce(
6
+ (n, l) => n + (l.hasHeader ? C : 0) + l.itemCount * E,
7
+ 0
8
+ );
9
+ }
10
+ function st(t) {
11
+ return t - P;
19
12
  }
20
- const N = 700;
21
- function H(t, n = N) {
13
+ function U(t, n = H) {
22
14
  return {
23
15
  usedPx: t,
24
16
  budgetPx: n,
@@ -26,13 +18,406 @@ function H(t, n = N) {
26
18
  overBy: Math.max(0, t - n)
27
19
  };
28
20
  }
29
- function U(t) {
21
+ function T(t) {
30
22
  return t.scrollHeight;
31
23
  }
24
+ const G = "_sidebar_17kel_7", W = {
25
+ sidebar: G
26
+ };
27
+ function j({ label: t, collapsed: n = !1, children: l }) {
28
+ const i = B(null);
29
+ return R(i, t, l), /* @__PURE__ */ e("aside", { ref: i, className: W.sidebar, "aria-label": t, "data-collapsed": n ? "true" : void 0, children: l });
30
+ }
31
+ function R(t, n, l) {
32
+ I(() => {
33
+ if (process.env.NODE_ENV === "production") return;
34
+ const i = t.current;
35
+ if (!i) return;
36
+ const o = i.querySelector(":scope > nav");
37
+ if (!(o instanceof HTMLElement)) {
38
+ i.querySelector("nav") && console.warn(
39
+ `@boost33/ui Sidebar ("${n}"): the nav is nested inside another element, so it is not the sidebar's scroll region. Make the <nav> a direct child of the Sidebar, or a nav longer than the column will paint past its bottom edge instead of scrolling. See ADR-0005.`
40
+ );
41
+ return;
42
+ }
43
+ const s = U(T(o), o.clientHeight);
44
+ s.fits || console.warn(
45
+ `@boost33/ui Sidebar ("${n}"): the nav needs ${s.usedPx}px and the column has ${s.budgetPx}px for it, so it is ${s.overBy}px over and scrolling. Nothing is lost -- the wallet chip and footer stay pinned -- but the shell's nav budget is about ${H}px at a 900px viewport, and a nav past it is a nav worth cutting. navContentHeight() and checkNavHeightBudget(), exported from this package, check a nav's shape before it renders.`
46
+ );
47
+ }, [t, n, l]);
48
+ }
49
+ const V = "_section_3sxsg_5", O = "_header_3sxsg_13", y = {
50
+ section: V,
51
+ header: O
52
+ };
53
+ function F({ label: t, children: n }) {
54
+ const l = L();
55
+ return t == null ? /* @__PURE__ */ e("div", { className: y.section, children: n }) : /* @__PURE__ */ a("div", { className: y.section, role: "group", "aria-labelledby": l, children: [
56
+ /* @__PURE__ */ e("p", { id: l, className: y.header, children: t }),
57
+ n
58
+ ] });
59
+ }
60
+ const Z = "_wordmark_1k88r_4", D = "_mark_1k88r_17", X = "_name_1k88r_35", w = {
61
+ wordmark: Z,
62
+ mark: D,
63
+ name: X
64
+ };
65
+ function q({ mark: t, collapsed: n = !1 }) {
66
+ return /* @__PURE__ */ a("div", { className: w.wordmark, "data-collapsed": n ? "true" : void 0, children: [
67
+ t != null && /* @__PURE__ */ e("span", { className: w.mark, "aria-hidden": "true", children: t }),
68
+ /* @__PURE__ */ e("span", { className: w.name, children: "Boost33" })
69
+ ] });
70
+ }
71
+ const K = "_tile_5vhrh_12", Y = "_ring_5vhrh_16", J = "_mark_5vhrh_24", m = {
72
+ tile: K,
73
+ ring: Y,
74
+ mark: J
75
+ }, Q = 24, ot = 20;
76
+ function it({ size: t = Q, title: n, className: l }) {
77
+ const i = L(), o = n == null;
78
+ return /* @__PURE__ */ a(
79
+ "svg",
80
+ {
81
+ className: l != null ? `${m.mark} ${l}` : m.mark,
82
+ width: t,
83
+ height: t,
84
+ viewBox: "402.17 34.21 234.9 235",
85
+ role: o ? void 0 : "img",
86
+ "aria-hidden": o ? "true" : void 0,
87
+ "aria-labelledby": o ? void 0 : i,
88
+ children: [
89
+ !o && /* @__PURE__ */ e("title", { id: i, children: n }),
90
+ /* @__PURE__ */ e(
91
+ "path",
92
+ {
93
+ className: m.tile,
94
+ d: "M404.19,97c3.79-36.53,38.18-62.45,74.88-62.79q40-.37,80,0c43.66.4,77.69,34.58,78,78.28q.27,38.63,0,77.27c-.35,45.47-34.21,79.19-79.71,79.45q-38.62.23-77.24,0c-43.46-.34-77.66-34.79-77.95-78.34-.09-13,0-26.07,0-39.1m42.93-56V207c23.4,0,46.31.89,69.1-.33,14.41-.77,25.11-13.75,26.36-28.3,1.37-16-6.92-26.34-20.61-33.54,1.15-2.88,2.34-5.37,3.16-8C529,118.35,517,97.73,497.72,96,480.55,94.49,463.15,95.7,445.11,95.7Zm127.65,63.44a31.89,31.89,0,1,0-31.21-32.47A31.87,31.87,0,0,0,572.76,159.14ZM597.12,183V168.11H550.05V183Z"
95
+ }
96
+ ),
97
+ /* @__PURE__ */ e(
98
+ "path",
99
+ {
100
+ className: m.ring,
101
+ d: "M572.76,159.14a31.89,31.89,0,1,1,32.47-31.31A31.62,31.62,0,0,1,572.76,159.14Zm.56-47.42a15.53,15.53,0,1,0,15.61,16A15.44,15.44,0,0,0,573.32,111.72Z"
102
+ }
103
+ ),
104
+ /* @__PURE__ */ e(
105
+ "path",
106
+ {
107
+ className: m.tile,
108
+ d: "M460.7,190.94V160.11c17.05,0,33.68-.53,50.25.23,8.11.37,13.22,7.7,13,15.5s-5.75,14.69-13.73,14.94C493.9,191.3,477.58,190.94,460.7,190.94Z"
109
+ }
110
+ ),
111
+ /* @__PURE__ */ e(
112
+ "path",
113
+ {
114
+ className: m.tile,
115
+ d: "M460.65,142.62V112.36c12.1,0,24.4-.89,36.5.33,7.46.76,11.4,7.3,11.05,15-.37,8.1-5,14-12.95,14.64C484,143.3,472.52,142.62,460.65,142.62Z"
116
+ }
117
+ ),
118
+ /* @__PURE__ */ e(
119
+ "path",
120
+ {
121
+ className: m.tile,
122
+ d: "M573.32,111.72a15.53,15.53,0,1,1,0,31.06,15.53,15.53,0,1,1,0-31.06Z"
123
+ }
124
+ )
125
+ ]
126
+ }
127
+ );
128
+ }
129
+ const z = "_mark_1jt5g_6", ee = "_ground_1jt5g_17", x = {
130
+ mark: z,
131
+ ground: ee
132
+ }, te = 20, ne = ["wallet", "score", "voice", "outbound"], re = {
133
+ // A credit card with its magnetic stripe: an outline card (fill none, so the
134
+ // tile shows through) plus a filled stripe across it.
135
+ wallet: /* @__PURE__ */ a(k, { children: [
136
+ /* @__PURE__ */ e("rect", { x: "5", y: "10", width: "30", height: "20", rx: "4", fill: "none", stroke: "currentColor", strokeWidth: "3" }),
137
+ /* @__PURE__ */ e("rect", { x: "5", y: "16", width: "30", height: "5", fill: "currentColor" })
138
+ ] }),
139
+ // A gauge with the needle swung high: an arc, the needle, and the hub it
140
+ // pivots on. Arc and needle stroke at width 4 with round caps.
141
+ score: /* @__PURE__ */ a(k, { children: [
142
+ /* @__PURE__ */ e("path", { d: "M6 26 A14 14 0 0 1 34 26", fill: "none", stroke: "currentColor", strokeWidth: "4", strokeLinecap: "round" }),
143
+ /* @__PURE__ */ e("line", { x1: "20", y1: "26", x2: "28", y2: "14", stroke: "currentColor", strokeWidth: "4", strokeLinecap: "round" }),
144
+ /* @__PURE__ */ e("circle", { cx: "20", cy: "26", r: "3", fill: "currentColor" })
145
+ ] }),
146
+ // A soundwave, tall in the middle: three pill bars, all centred on y=20.
147
+ voice: /* @__PURE__ */ a(k, { children: [
148
+ /* @__PURE__ */ e("rect", { x: "6", y: "14", width: "7", height: "12", rx: "3.5", fill: "currentColor" }),
149
+ /* @__PURE__ */ e("rect", { x: "16.5", y: "4", width: "7", height: "32", rx: "3.5", fill: "currentColor" }),
150
+ /* @__PURE__ */ e("rect", { x: "27", y: "14", width: "7", height: "12", rx: "3.5", fill: "currentColor" })
151
+ ] }),
152
+ // An arrow leaving through the top-right: the shaft and the elbow of its head,
153
+ // both stroke width 4 with round caps and joins.
154
+ outbound: /* @__PURE__ */ a(k, { children: [
155
+ /* @__PURE__ */ e("line", { x1: "8", y1: "32", x2: "32", y2: "8", stroke: "currentColor", strokeWidth: "4", strokeLinecap: "round", strokeLinejoin: "round" }),
156
+ /* @__PURE__ */ e("path", { d: "M16 8 H32 V24", fill: "none", stroke: "currentColor", strokeWidth: "4", strokeLinecap: "round", strokeLinejoin: "round" })
157
+ ] })
158
+ };
159
+ function ae(t) {
160
+ return ne.includes(t);
161
+ }
162
+ function ct({ slug: t, size: n = te, muted: l, title: i, className: o }) {
163
+ const s = L(), c = i == null, d = ae(t) ? re[t] : null;
164
+ return /* @__PURE__ */ a(
165
+ "svg",
166
+ {
167
+ className: o != null ? `${x.mark} ${o}` : x.mark,
168
+ width: n,
169
+ height: n,
170
+ viewBox: "0 0 40 40",
171
+ role: c ? void 0 : "img",
172
+ "aria-hidden": c ? "true" : void 0,
173
+ "aria-labelledby": c ? void 0 : s,
174
+ children: [
175
+ !c && /* @__PURE__ */ e("title", { id: s, children: i }),
176
+ /* @__PURE__ */ e(
177
+ "rect",
178
+ {
179
+ className: x.ground,
180
+ width: "40",
181
+ height: "40",
182
+ style: { fill: l ? "var(--color-neutral-300)" : "var(--color-neutral-950)" }
183
+ }
184
+ ),
185
+ d && /* @__PURE__ */ e(
186
+ "g",
187
+ {
188
+ transform: "translate(10 10) scale(0.5)",
189
+ style: { color: "var(--color-primary-foreground)" },
190
+ children: d
191
+ }
192
+ )
193
+ ]
194
+ }
195
+ );
196
+ }
197
+ const le = "_row_16hr7_4", se = "_icon_16hr7_41", oe = "_label_16hr7_66", ie = "_trailingSlot_16hr7_92", ce = "_count_16hr7_106", de = "_countUrgent_16hr7_110", p = {
198
+ row: le,
199
+ icon: se,
200
+ label: oe,
201
+ trailingSlot: ie,
202
+ count: ce,
203
+ countUrgent: de
204
+ };
205
+ function he({ children: t, icon: n, active: l = !1, collapsed: i = !1, count: o, countUrgent: s = !1, href: c, onClick: d }) {
206
+ const h = {
207
+ "data-active": l ? "true" : void 0,
208
+ "data-collapsed": i ? "true" : void 0,
209
+ "aria-current": l ? "page" : void 0
210
+ }, _ = /* @__PURE__ */ a(k, { children: [
211
+ n != null && /* @__PURE__ */ e("span", { className: p.icon, "aria-hidden": "true", children: n }),
212
+ /* @__PURE__ */ e("span", { className: p.label, children: t }),
213
+ /* @__PURE__ */ e("span", { className: p.trailingSlot, children: o != null && /* @__PURE__ */ e("span", { className: s ? p.countUrgent : p.count, children: o }) })
214
+ ] });
215
+ return c ? /* @__PURE__ */ e("a", { className: p.row, href: c, onClick: d, ...h, children: _ }) : /* @__PURE__ */ e("button", { type: "button", className: p.row, onClick: d, ...h, children: _ });
216
+ }
217
+ const ue = "_chip_676ll_37", _e = "_header_676ll_74", me = "_name_676ll_83", pe = "_pill_676ll_94", ve = "_figure_676ll_103", ge = "_amount_676ll_118", Ne = "_unit_676ll_127", be = "_retry_676ll_132", fe = "_bar_676ll_141", ke = "_segmentGranted_676ll_162", ye = "_segmentPurchased_676ll_163", we = "_details_676ll_183", xe = "_split_676ll_192", $e = "_splitRow_676ll_199", Le = "_splitLabel_676ll_205", Se = "_splitAmount_676ll_213", Me = "_dotGranted_676ll_221", He = "_dotPurchased_676ll_222", Ae = "_footer_676ll_236", Be = "_burnNote_676ll_244", Ie = "_topUp_676ll_256", Ce = "_rail_676ll_310", Ee = "_railIcon_676ll_324", Pe = "_railFigure_676ll_330", Ue = "_srOnly_676ll_343", r = {
218
+ chip: ue,
219
+ header: _e,
220
+ name: me,
221
+ pill: pe,
222
+ figure: ve,
223
+ amount: ge,
224
+ unit: Ne,
225
+ retry: be,
226
+ bar: fe,
227
+ segmentGranted: ke,
228
+ segmentPurchased: ye,
229
+ details: we,
230
+ split: xe,
231
+ splitRow: $e,
232
+ splitLabel: Le,
233
+ splitAmount: Se,
234
+ dotGranted: Me,
235
+ dotPurchased: He,
236
+ footer: Ae,
237
+ burnNote: Be,
238
+ topUp: Ie,
239
+ rail: Ce,
240
+ railIcon: Ee,
241
+ railFigure: Pe,
242
+ srOnly: Ue
243
+ };
244
+ function N(t) {
245
+ return t.toLocaleString("en-US");
246
+ }
247
+ function S(t) {
248
+ return `${(t / 1e3).toFixed(1)}k`;
249
+ }
250
+ function Te({ className: t }) {
251
+ return /* @__PURE__ */ a("svg", { className: t, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
252
+ /* @__PURE__ */ e("rect", { x: "3", y: "6", width: "18", height: "12", rx: "2" }),
253
+ /* @__PURE__ */ e("path", { d: "M3 10h18" })
254
+ ] });
255
+ }
256
+ function Ge({ className: t }) {
257
+ return /* @__PURE__ */ a("svg", { className: t, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
258
+ /* @__PURE__ */ e("path", { d: "M21 12a9 9 0 1 1-2.64-6.36" }),
259
+ /* @__PURE__ */ e("path", { d: "M21 3v4h-4" })
260
+ ] });
261
+ }
262
+ const We = 0.8;
263
+ function M({ granted: t, purchased: n, cycleTotal: l, tone: i }) {
264
+ const o = (d) => `${d / l * 100}%`, s = t + n, c = `${N(t)} granted and ${N(n)} purchased of ${N(s)} remaining`;
265
+ return /* @__PURE__ */ a("div", { className: r.bar, "data-tone": i, role: "img", "aria-label": c, children: [
266
+ /* @__PURE__ */ e("span", { className: r.segmentGranted, "data-testid": "segment-granted", style: { width: o(t) } }),
267
+ /* @__PURE__ */ e("span", { className: r.segmentPurchased, "data-testid": "segment-purchased", style: { width: o(n) } })
268
+ ] });
269
+ }
270
+ function dt({ granted: t, purchased: n, grantCycle: l, grantExpires: i, burnNote: o, unknown: s = !1, collapsed: c = !1, onTopUp: d }) {
271
+ const h = t + n, _ = l + n, b = l > 0 ? (l - t) / l : 0, u = !s && b >= We;
272
+ return c ? /* @__PURE__ */ a("div", { className: r.rail, role: "group", "aria-label": s ? "Wallet balance unknown" : `Wallet balance ${S(h)}`, tabIndex: 0, children: [
273
+ /* @__PURE__ */ e(Te, { className: r.railIcon }),
274
+ /* @__PURE__ */ e("span", { className: r.railFigure, children: s ? "—" : S(h) }),
275
+ s ? /* @__PURE__ */ e("span", { className: r.srOnly, children: "unknown" }) : /* @__PURE__ */ e(M, { granted: t, purchased: n, cycleTotal: _, tone: u ? "threshold" : "rest" })
276
+ ] }) : /* @__PURE__ */ a("div", { className: r.chip, role: "group", "aria-label": "Wallet balance", tabIndex: 0, "data-threshold": u ? "true" : void 0, "data-unknown": s ? "true" : void 0, children: [
277
+ /* @__PURE__ */ a("div", { className: r.header, children: [
278
+ /* @__PURE__ */ e("span", { className: r.name, children: "Wallet" }),
279
+ u && /* @__PURE__ */ a("span", { className: r.pill, children: [
280
+ Math.round(b * 100),
281
+ "%"
282
+ ] })
283
+ ] }),
284
+ s ? /* @__PURE__ */ a("div", { className: r.figure, children: [
285
+ /* @__PURE__ */ e("span", { className: r.amount, "aria-hidden": "true", children: "—" }),
286
+ /* @__PURE__ */ e(Ge, { className: r.retry }),
287
+ /* @__PURE__ */ e("span", { className: r.srOnly, children: "Balance unknown" })
288
+ ] }) : /* @__PURE__ */ a("div", { className: r.figure, children: [
289
+ /* @__PURE__ */ e("span", { className: r.amount, children: N(h) }),
290
+ /* @__PURE__ */ e("span", { className: r.unit, children: "tokens" })
291
+ ] }),
292
+ s ? /* @__PURE__ */ e("div", { className: r.bar, "data-tone": "unknown", "aria-hidden": "true" }) : /* @__PURE__ */ e(M, { granted: t, purchased: n, cycleTotal: _, tone: u ? "threshold" : "rest" }),
293
+ !s && !u && /* @__PURE__ */ a("div", { className: r.details, children: [
294
+ /* @__PURE__ */ a("dl", { className: r.split, children: [
295
+ /* @__PURE__ */ a("div", { className: r.splitRow, children: [
296
+ /* @__PURE__ */ a("dt", { className: r.splitLabel, children: [
297
+ /* @__PURE__ */ e("span", { className: r.dotGranted, "aria-hidden": "true" }),
298
+ "Granted"
299
+ ] }),
300
+ /* @__PURE__ */ e("dd", { className: r.splitAmount, children: N(t) })
301
+ ] }),
302
+ /* @__PURE__ */ a("div", { className: r.splitRow, children: [
303
+ /* @__PURE__ */ a("dt", { className: r.splitLabel, children: [
304
+ /* @__PURE__ */ e("span", { className: r.dotPurchased, "aria-hidden": "true" }),
305
+ "Purchased"
306
+ ] }),
307
+ /* @__PURE__ */ e("dd", { className: r.splitAmount, children: N(n) })
308
+ ] })
309
+ ] }),
310
+ i && /* @__PURE__ */ a("p", { className: r.footer, children: [
311
+ "Granted expires ",
312
+ i,
313
+ " · burns first"
314
+ ] })
315
+ ] }),
316
+ u && o && /* @__PURE__ */ e("p", { className: r.burnNote, children: o }),
317
+ !s && /* @__PURE__ */ e("button", { type: "button", className: r.topUp, "data-grown": u ? "true" : void 0, onClick: d, children: "Top up" })
318
+ ] });
319
+ }
320
+ const je = "_bar_r1jjn_6", Re = "_breadcrumb_r1jjn_19", Ve = "_actions_r1jjn_32", $ = {
321
+ bar: je,
322
+ breadcrumb: Re,
323
+ actions: Ve
324
+ };
325
+ function Oe({ breadcrumb: t, actions: n }) {
326
+ return /* @__PURE__ */ a("div", { className: $.bar, children: [
327
+ /* @__PURE__ */ e("div", { className: $.breadcrumb, children: t }),
328
+ n != null && /* @__PURE__ */ e("div", { className: $.actions, children: n })
329
+ ] });
330
+ }
331
+ const Fe = "_header_18p6i_13", Ze = "_headings_18p6i_23", De = "_title_18p6i_30", Xe = "_subtitle_18p6i_39", qe = "_actions_18p6i_47", f = {
332
+ header: Fe,
333
+ headings: Ze,
334
+ title: De,
335
+ subtitle: Xe,
336
+ actions: qe
337
+ };
338
+ function Ke({ title: t, subtitle: n, actions: l }) {
339
+ return /* @__PURE__ */ a("header", { className: f.header, children: [
340
+ /* @__PURE__ */ a("div", { className: f.headings, children: [
341
+ /* @__PURE__ */ e("h1", { className: f.title, children: t }),
342
+ n != null && /* @__PURE__ */ e("p", { className: f.subtitle, children: n })
343
+ ] }),
344
+ l != null && /* @__PURE__ */ e("div", { className: f.actions, children: l })
345
+ ] });
346
+ }
347
+ const Ye = "_shell_1xurb_10", Je = "_wordmark_1xurb_20", Qe = "_switcher_1xurb_26", ze = "_foot_1xurb_33", et = "_nav_1xurb_45", tt = "_main_1xurb_54", nt = "_content_1xurb_64", v = {
348
+ shell: Ye,
349
+ wordmark: Je,
350
+ switcher: Qe,
351
+ foot: ze,
352
+ nav: et,
353
+ main: tt,
354
+ content: nt
355
+ };
356
+ function ht({
357
+ product: t,
358
+ nav: n,
359
+ topBar: l,
360
+ pageHeader: i,
361
+ mark: o,
362
+ switcher: s,
363
+ wallet: c,
364
+ footer: d,
365
+ collapsed: h = !1,
366
+ children: _
367
+ }) {
368
+ return /* @__PURE__ */ a("div", { className: v.shell, children: [
369
+ /* @__PURE__ */ a(j, { label: t, collapsed: h, children: [
370
+ /* @__PURE__ */ e("div", { className: v.wordmark, children: /* @__PURE__ */ e(q, { mark: o, collapsed: h }) }),
371
+ s != null && /* @__PURE__ */ e("div", { className: v.switcher, children: s }),
372
+ /* @__PURE__ */ e("nav", { "aria-label": t, className: v.nav, children: n.map((b, u) => (
373
+ // No room for a header at 56px, so on the rail every section renders
374
+ // headerless -- its rows collapse to their icons and line up.
375
+ /* @__PURE__ */ e(F, { label: h ? void 0 : b.label, children: b.items.map((g, A) => /* @__PURE__ */ e(
376
+ he,
377
+ {
378
+ icon: g.icon,
379
+ active: g.active,
380
+ collapsed: h,
381
+ count: g.count,
382
+ countUrgent: g.countUrgent,
383
+ href: g.href,
384
+ children: g.label
385
+ },
386
+ A
387
+ )) }, u)
388
+ )) }),
389
+ (c != null || d != null) && /* @__PURE__ */ a("div", { className: v.foot, children: [
390
+ c,
391
+ d
392
+ ] })
393
+ ] }),
394
+ /* @__PURE__ */ a("div", { className: v.main, children: [
395
+ /* @__PURE__ */ e(Oe, { ...l }),
396
+ /* @__PURE__ */ e(Ke, { ...i }),
397
+ /* @__PURE__ */ e("div", { className: v.content, children: _ })
398
+ ] })
399
+ ] });
400
+ }
32
401
  export {
33
- N as NAV_HEIGHT_BUDGET_PX,
34
- w as NavItem,
35
- H as checkNavHeightBudget,
36
- U as measureNavHeight
402
+ ht as AppShell,
403
+ ot as BOOST33_MARK_MIN_LEGIBLE_PX,
404
+ it as Boost33Mark,
405
+ H as NAV_HEIGHT_BUDGET_PX,
406
+ E as NAV_ITEM_PX,
407
+ C as NAV_SECTION_HEADER_PX,
408
+ P as NAV_SHELL_FIXED_PX,
409
+ he as NavItem,
410
+ F as NavSection,
411
+ ne as PRODUCT_SLUGS,
412
+ Ke as PageHeader,
413
+ ct as ProductMark,
414
+ j as Sidebar,
415
+ Oe as TopBar,
416
+ dt as WalletChip,
417
+ q as Wordmark,
418
+ U as checkNavHeightBudget,
419
+ T as measureNavHeight,
420
+ st as navBudgetForViewport,
421
+ lt as navContentHeight
37
422
  };
38
423
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/components/NavItem/NavItem.tsx","../src/nav-height-budget.ts"],"sourcesContent":["import type { ReactNode } from 'react'\nimport styles from './NavItem.module.css'\n\nexport interface NavItemProps {\n /** The nav label. Truncates with an ellipsis rather than wrapping. */\n children: ReactNode\n /** Marks this row as the current page. Sets `aria-current=\"page\"`. */\n active?: boolean\n /** Sidebar-on-the-rail state. Hides the label and trailing slot visually; the label stays in the accessibility tree. */\n collapsed?: boolean\n /** Trailing slot content. Omit to leave the slot empty -- it still reserves its width so counts line up across rows. */\n count?: number\n /** Renders `count` in the primary color instead of muted. */\n countUrgent?: boolean\n href?: string\n onClick?: () => void\n}\n\nexport function NavItem({ children, active = false, collapsed = false, count, countUrgent = false, href, onClick }: NavItemProps) {\n const dataProps = {\n 'data-active': active ? ('true' as const) : undefined,\n 'data-collapsed': collapsed ? ('true' as const) : undefined,\n 'aria-current': active ? ('page' as const) : undefined,\n }\n\n const content = (\n <>\n <span className={styles.label}>{children}</span>\n <span className={styles.trailingSlot}>\n {count != null && <span className={countUrgent ? styles.countUrgent : styles.count}>{count}</span>}\n </span>\n </>\n )\n\n if (href) {\n return (\n <a className={styles.row} href={href} onClick={onClick} {...dataProps}>\n {content}\n </a>\n )\n }\n\n return (\n <button type=\"button\" className={styles.row} onClick={onClick} {...dataProps}>\n {content}\n </button>\n )\n}\n","// The spec's rough budget: a sidebar's nav has to fit above the wallet chip\n// and footer without scrolling. Boost Outbound's admin role has already blown\n// past it once, so this is a real check, not a hypothetical.\nexport const NAV_HEIGHT_BUDGET_PX = 700\n\nexport interface NavHeightBudget {\n usedPx: number\n budgetPx: number\n fits: boolean\n overBy: number\n}\n\nexport function checkNavHeightBudget(usedPx: number, budgetPx: number = NAV_HEIGHT_BUDGET_PX): NavHeightBudget {\n return {\n usedPx,\n budgetPx,\n fits: usedPx <= budgetPx,\n overBy: Math.max(0, usedPx - budgetPx),\n }\n}\n\nexport function measureNavHeight(nav: HTMLElement): number {\n return nav.scrollHeight\n}\n"],"names":["NavItem","children","active","collapsed","count","countUrgent","href","onClick","dataProps","content","jsxs","Fragment","jsx","styles","NAV_HEIGHT_BUDGET_PX","checkNavHeightBudget","usedPx","budgetPx","measureNavHeight","nav"],"mappings":";;;;;;;;AAkBO,SAASA,EAAQ,EAAE,UAAAC,GAAU,QAAAC,IAAS,IAAO,WAAAC,IAAY,IAAO,OAAAC,GAAO,aAAAC,IAAc,IAAO,MAAAC,GAAM,SAAAC,EAAA,GAAyB;AAChI,QAAMC,IAAY;AAAA,IAChB,eAAeN,IAAU,SAAmB;AAAA,IAC5C,kBAAkBC,IAAa,SAAmB;AAAA,IAClD,gBAAgBD,IAAU,SAAmB;AAAA,EAAA,GAGzCO,IACJ,gBAAAC,EAAAC,GAAA,EACE,UAAA;AAAA,IAAA,gBAAAC,EAAC,QAAA,EAAK,WAAWC,EAAO,OAAQ,UAAAZ,GAAS;AAAA,sBACxC,QAAA,EAAK,WAAWY,EAAO,cACrB,eAAS,QAAQ,gBAAAD,EAAC,QAAA,EAAK,WAAWP,IAAcQ,EAAO,cAAcA,EAAO,OAAQ,aAAM,EAAA,CAC7F;AAAA,EAAA,GACF;AAGF,SAAIP,IAEA,gBAAAM,EAAC,OAAE,WAAWC,EAAO,KAAK,MAAAP,GAAY,SAAAC,GAAmB,GAAGC,GACzD,UAAAC,EAAA,CACH,IAKF,gBAAAG,EAAC,UAAA,EAAO,MAAK,UAAS,WAAWC,EAAO,KAAK,SAAAN,GAAmB,GAAGC,GAChE,UAAAC,EAAA,CACH;AAEJ;AC5CO,MAAMK,IAAuB;AAS7B,SAASC,EAAqBC,GAAgBC,IAAmBH,GAAuC;AAC7G,SAAO;AAAA,IACL,QAAAE;AAAA,IACA,UAAAC;AAAA,IACA,MAAMD,KAAUC;AAAA,IAChB,QAAQ,KAAK,IAAI,GAAGD,IAASC,CAAQ;AAAA,EAAA;AAEzC;AAEO,SAASC,EAAiBC,GAA0B;AACzD,SAAOA,EAAI;AACb;"}
1
+ {"version":3,"file":"index.js","sources":["../src/nav-height-budget.ts","../src/components/Sidebar/Sidebar.tsx","../src/components/NavSection/NavSection.tsx","../src/components/Wordmark/Wordmark.tsx","../src/components/Boost33Mark/Boost33Mark.tsx","../src/components/ProductMark/ProductMark.tsx","../src/components/NavItem/NavItem.tsx","../src/components/WalletChip/format.ts","../src/components/WalletChip/icons.tsx","../src/components/WalletChip/WalletChip.tsx","../src/components/TopBar/TopBar.tsx","../src/components/PageHeader/PageHeader.tsx","../src/components/AppShell/AppShell.tsx"],"sourcesContent":["// The spec's rough budget: a sidebar's nav has to fit above the wallet chip\n// and footer without scrolling. Boost Outbound's admin role has already blown\n// past it once, so this is a real check, not a hypothetical.\n//\n// Sidebar calls checkNavHeightBudget and measureNavHeight itself now, and warns\n// outside a production build when a nav does not fit the column it was given\n// (B33-500, ADR-0005). These stay exported for the other half of the job:\n// checking a nav's shape from its data, before it is ever rendered.\nexport const NAV_HEIGHT_BUDGET_PX = 700\n\n// The spec's per-row costs. A section header (the small uppercase label) takes\n// 40px of vertical run; a nav item takes 32px. Kept as named constants so a\n// nav's height can be computed from its data -- how many sections, how many\n// items -- before anything renders.\nexport const NAV_SECTION_HEADER_PX = 40\nexport const NAV_ITEM_PX = 32\n\n// The sidebar's fixed furniture above and below the nav -- the wordmark, the\n// app switcher, the wallet chip, the footer -- takes about 198px, so a 900px\n// viewport leaves roughly the 700px budget for the nav itself.\nexport const NAV_SHELL_FIXED_PX = 198\n\n/** One section of a nav, described by its shape rather than its rendered rows. */\nexport interface NavSectionShape {\n /** Whether the section carries a header label (its 40px cost). */\n hasHeader: boolean\n /** How many nav items the section holds (32px each). */\n itemCount: number\n}\n\n/**\n * The vertical space a nav takes, summed from its sections' shapes. Lets the\n * budget be checked from the nav's data alone, before it is rendered into a\n * sidebar that might not fit it -- Boost Outbound's admin role has overflowed\n * this once already.\n */\nexport function navContentHeight(sections: NavSectionShape[]): number {\n return sections.reduce(\n (used, section) => used + (section.hasHeader ? NAV_SECTION_HEADER_PX : 0) + section.itemCount * NAV_ITEM_PX,\n 0,\n )\n}\n\n/** The nav's budget at a given viewport: what is left once the fixed shell parts are taken. */\nexport function navBudgetForViewport(viewportPx: number): number {\n return viewportPx - NAV_SHELL_FIXED_PX\n}\n\nexport interface NavHeightBudget {\n usedPx: number\n budgetPx: number\n fits: boolean\n overBy: number\n}\n\nexport function checkNavHeightBudget(usedPx: number, budgetPx: number = NAV_HEIGHT_BUDGET_PX): NavHeightBudget {\n return {\n usedPx,\n budgetPx,\n fits: usedPx <= budgetPx,\n overBy: Math.max(0, usedPx - budgetPx),\n }\n}\n\nexport function measureNavHeight(nav: HTMLElement): number {\n return nav.scrollHeight\n}\n","import { useEffect, useRef, type ReactNode, type RefObject } from 'react'\nimport { NAV_HEIGHT_BUDGET_PX, checkNavHeightBudget, measureNavHeight } from '../../nav-height-budget'\nimport styles from './Sidebar.module.css'\n\nexport interface SidebarProps {\n /** Names the sidebar landmark, so it stays distinct from other regions on the page. */\n label: string\n /** Sidebar-on-the-rail state. Narrows to the 56px rail; the pieces inside collapse themselves. */\n collapsed?: boolean\n /**\n * What the sidebar holds -- wordmark, switcher, nav sections, wallet chip, footer. Passed in, never hardcoded.\n *\n * **Put the nav in a `<nav>` element that is a direct child of the sidebar.**\n * That element is the sidebar's scroll region: a nav longer than the column\n * scrolls inside itself, and everything above and below it -- the wordmark,\n * the switcher, the wallet chip, the footer -- stays pinned. A nav nested\n * inside another element does not get that, and says so in development. See\n * ADR-0005.\n */\n children: ReactNode\n}\n\n// The sidebar frame: a 264px column with a hairline down its right edge,\n// collapsing to a 56px rail. It is only the container -- what stacks inside it\n// is data the consumer passes, so a product never hardcodes its own rows here.\nexport function Sidebar({ label, collapsed = false, children }: SidebarProps) {\n const column = useRef<HTMLElement>(null)\n useNavHeightBudgetWarning(column, label, children)\n\n return (\n <aside ref={column} className={styles.sidebar} aria-label={label} data-collapsed={collapsed ? 'true' : undefined}>\n {children}\n </aside>\n )\n}\n\n/*\n * The development-mode half of the nav budget.\n *\n * Sidebar.module.css stops an over-long nav from painting past the column, but\n * silently: the consumer sees a scrollbar and never learns their nav is too\n * long. The budget helpers in nav-height-budget.ts were written for exactly\n * that -- Boost Outbound's admin role has exceeded the budget once already --\n * and until this hook nothing in the shipped components ever called them, so a\n * consumer was protected only if they remembered to check by hand.\n *\n * `process.env.NODE_ENV` rather than an `import.meta` flag because every\n * bundler a consumer might use replaces that exact expression at build time\n * and drops the branch, which is how React itself ships its own dev warnings.\n *\n * Best effort by design: it measures once per render of the children, after\n * paint. A nav that only overflows after a font swap or a window resize is not\n * caught, and does not need to be -- the case this exists for is a nav that is\n * too long for the column the moment it is rendered.\n */\nfunction useNavHeightBudgetWarning(column: RefObject<HTMLElement | null>, label: string, children: ReactNode): void {\n useEffect(() => {\n if (process.env.NODE_ENV === 'production') return\n\n const element = column.current\n if (!element) return\n\n const nav = element.querySelector(':scope > nav')\n if (!(nav instanceof HTMLElement)) {\n // Only worth saying when there is a nav and it is in the wrong place.\n // A sidebar that holds no nav at all is an ordinary thing to render.\n if (element.querySelector('nav')) {\n console.warn(\n `@boost33/ui Sidebar (\"${label}\"): the nav is nested inside another element, so it is not the sidebar's scroll ` +\n `region. Make the <nav> a direct child of the Sidebar, or a nav longer than the column will paint past its ` +\n `bottom edge instead of scrolling. See ADR-0005.`,\n )\n }\n return\n }\n\n const budget = checkNavHeightBudget(measureNavHeight(nav), nav.clientHeight)\n if (budget.fits) return\n\n console.warn(\n `@boost33/ui Sidebar (\"${label}\"): the nav needs ${budget.usedPx}px and the column has ${budget.budgetPx}px for it, ` +\n `so it is ${budget.overBy}px over and scrolling. Nothing is lost -- the wallet chip and footer stay pinned -- but ` +\n `the shell's nav budget is about ${NAV_HEIGHT_BUDGET_PX}px at a 900px viewport, and a nav past it is a nav worth ` +\n `cutting. navContentHeight() and checkNavHeightBudget(), exported from this package, check a nav's shape before ` +\n `it renders.`,\n )\n }, [column, label, children])\n}\n","import { useId, type ReactNode } from 'react'\nimport styles from './NavSection.module.css'\n\nexport interface NavSectionProps {\n /**\n * The section header -- the small uppercase label above the rows. Omit for a\n * section with no header (the sidebar's first section has none). Passed in\n * its natural case; the uppercasing is presentational.\n */\n label?: string\n /** The section's rows -- NavItems, passed in as data, never hardcoded here. */\n children: ReactNode\n}\n\n// A group of nav rows under an optional header. A labelled section is an\n// accessible group named by its header, so the rows announce with their\n// section; a headerless section is just the run of rows.\nexport function NavSection({ label, children }: NavSectionProps) {\n const headerId = useId()\n\n if (label == null) {\n return <div className={styles.section}>{children}</div>\n }\n\n return (\n <div className={styles.section} role=\"group\" aria-labelledby={headerId}>\n <p id={headerId} className={styles.header}>\n {label}\n </p>\n {children}\n </div>\n )\n}\n","import type { ReactNode } from 'react'\nimport styles from './Wordmark.module.css'\n\nexport interface WordmarkProps {\n /**\n * The Boost33 mark -- the logo artwork, owned by its own ticket. Passed in\n * rather than hardcoded here so the wordmark does not carry a second copy of\n * the mark. On the rail it is all that shows.\n */\n mark?: ReactNode\n /** Sidebar-on-the-rail state. Hides the \"Boost33\" name; the mark stays. */\n collapsed?: boolean\n}\n\n// The lockup at the top of the sidebar: the Boost33 mark and the \"Boost33\"\n// name beside it. On the rail the name drops to the accessibility tree and the\n// mark stands alone.\nexport function Wordmark({ mark, collapsed = false }: WordmarkProps) {\n return (\n <div className={styles.wordmark} data-collapsed={collapsed ? 'true' : undefined}>\n {mark != null && (\n <span className={styles.mark} aria-hidden=\"true\">\n {mark}\n </span>\n )}\n <span className={styles.name}>Boost33</span>\n </div>\n )\n}\n","import { useId } from 'react'\nimport styles from './Boost33Mark.module.css'\n\n/**\n * The size the mark renders at unless a caller says otherwise. 24px is the\n * Wallet's own default at both of its call sites, and it is the size\n * tests/baselines/mark-boost33.png, wordmark.png and wordmark-rail.png were\n * captured at.\n */\nconst DEFAULT_SIZE = 24\n\n/**\n * The smallest size the artwork survives, in px. Recorded, not enforced — the\n * charte's brand guide warns the monogram stops working below about 20px: the\n * bowls of the B and the hole in the o close up and the whole thing reads as a\n * dark blob. Anything smaller needs a simplified variant from the parent, not\n * this scaled down. Nothing in this package ships below it.\n *\n * This floor is the **monogram's alone**. The product marks have the opposite\n * requirement — they are drawn on a grid chosen so one path renders correctly\n * from 96px down to 20px — so do not generalise this number into a shared rule\n * when the second mark lands.\n */\nexport const BOOST33_MARK_MIN_LEGIBLE_PX = 20\n\nexport interface Boost33MarkProps {\n /**\n * Rendered width and height in px. Read BOOST33_MARK_MIN_LEGIBLE_PX before\n * going small.\n */\n size?: number\n /**\n * An accessible name. Omit it — the default — and the mark is decorative:\n * `aria-hidden`, contributing nothing to the accessibility tree. That is the\n * right answer in the lockup, where the word \"Boost33\" sits beside it and a\n * name here would only make a screen reader say it twice. Pass one only where\n * the mark stands alone as the sole carrier of meaning.\n */\n title?: string\n /** Escape hatch for a consumer that needs to position the mark. */\n className?: string\n}\n\n/**\n * The Boost33 mark — the \"Bo\" squircle monogram.\n *\n * **This artwork is not ours.** The monogram and the wordmark are set by the\n * Boost33 charte graphique and a product cannot change them; the parent's brand\n * documentation lists the monogram as \"black tile, orange letterform\" under what\n * it owns. The paths below are lifted verbatim out of the official lockup at\n * `docs/brand/logo-of-noir-orange.svg` in the Wallet repo (`boostledger`), by way\n * of its `src/ui/primitives/icons.tsx` — reproduced, not redrawn. If the mark\n * ever changes, it changes in that SVG first and this is re-extracted from it.\n * Nobody here gets to nudge a curve.\n *\n * What stood in for it in this package until B33-556 was a Stage A placeholder in\n * the specimen: an orange tile with a `<text>B</text>` typed on top. The Wallet\n * carried the same mistake — tile and letterform the wrong way round — until\n * 9 Sep 2026, which is why the original fidelity baselines showed it too. It had\n * been wrong against the charte the whole time, not just against Paper.\n *\n * How it is built, since the fills are not obvious from the path list: the first\n * path is the black squircle with the B, the o and the underscore knocked out of\n * it as counter-wound subpaths, so those three show through as white. The orange\n * ring is then drawn into the o, and the last three paths fill the B's two bowls\n * and the o's centre back to black. **Order matters between the ring and the\n * centre** — swap them and the o fills solid orange. Nothing else overlaps.\n *\n * The two `fill: none` paths in the source SVG are omitted. They are export\n * artefacts that render nothing, and dropping them does not affect the winding of\n * the path above them.\n *\n * The viewBox is the monogram's own bounding box **inside the lockup**, which is\n * why it is an offset rectangle rather than starting at the origin. Do not \"tidy\"\n * it to `0 0 …` without re-deriving the paths; they carry absolute coordinates\n * and translating one without the other silently crops the mark.\n */\nexport function Boost33Mark({ size = DEFAULT_SIZE, title, className }: Boost33MarkProps) {\n const titleId = useId()\n const decorative = title == null\n\n return (\n <svg\n className={className != null ? `${styles.mark} ${className}` : styles.mark}\n width={size}\n height={size}\n viewBox=\"402.17 34.21 234.9 235\"\n role={decorative ? undefined : 'img'}\n aria-hidden={decorative ? 'true' : undefined}\n aria-labelledby={decorative ? undefined : titleId}\n >\n {!decorative && <title id={titleId}>{title}</title>}\n {/* Tile, with the B / o / underscore knocked out of it. */}\n <path\n className={styles.tile}\n d=\"M404.19,97c3.79-36.53,38.18-62.45,74.88-62.79q40-.37,80,0c43.66.4,77.69,34.58,78,78.28q.27,38.63,0,77.27c-.35,45.47-34.21,79.19-79.71,79.45q-38.62.23-77.24,0c-43.46-.34-77.66-34.79-77.95-78.34-.09-13,0-26.07,0-39.1m42.93-56V207c23.4,0,46.31.89,69.1-.33,14.41-.77,25.11-13.75,26.36-28.3,1.37-16-6.92-26.34-20.61-33.54,1.15-2.88,2.34-5.37,3.16-8C529,118.35,517,97.73,497.72,96,480.55,94.49,463.15,95.7,445.11,95.7Zm127.65,63.44a31.89,31.89,0,1,0-31.21-32.47A31.87,31.87,0,0,0,572.76,159.14ZM597.12,183V168.11H550.05V183Z\"\n />\n {/* The o, and the only orange in the mark. */}\n <path\n className={styles.ring}\n d=\"M572.76,159.14a31.89,31.89,0,1,1,32.47-31.31A31.62,31.62,0,0,1,572.76,159.14Zm.56-47.42a15.53,15.53,0,1,0,15.61,16A15.44,15.44,0,0,0,573.32,111.72Z\"\n />\n {/* The B's two bowls and the o's centre, returned to the tile's black. */}\n <path\n className={styles.tile}\n d=\"M460.7,190.94V160.11c17.05,0,33.68-.53,50.25.23,8.11.37,13.22,7.7,13,15.5s-5.75,14.69-13.73,14.94C493.9,191.3,477.58,190.94,460.7,190.94Z\"\n />\n <path\n className={styles.tile}\n d=\"M460.65,142.62V112.36c12.1,0,24.4-.89,36.5.33,7.46.76,11.4,7.3,11.05,15-.37,8.1-5,14-12.95,14.64C484,143.3,472.52,142.62,460.65,142.62Z\"\n />\n <path\n className={styles.tile}\n d=\"M573.32,111.72a15.53,15.53,0,1,1,0,31.06,15.53,15.53,0,1,1,0-31.06Z\"\n />\n </svg>\n )\n}\n","import { useId, type ReactNode } from 'react'\nimport styles from './ProductMark.module.css'\n\n/**\n * The size the mark renders at unless a caller says otherwise. 20px is the\n * Wallet's own default at its call sites, and the size the four\n * tests/baselines/productmark-*.png were captured at: a 20px tile in the 24px\n * gutter the specimen crop uses either side -> 68px CSS -> 136 device px at\n * deviceScaleFactor 2. Changing it moves four apps.\n *\n * There is deliberately no minimum-legible floor here, unlike Boost33Mark. That\n * floor is the monogram's alone -- the bowls of the B close up below ~20px. The\n * product marks have the opposite property: each is one path on a 40-unit grid\n * with a quarter-side corner, chosen so it renders correctly from Paper's 96px\n * down to 20px. Do not generalise Boost33Mark's floor into a shared rule.\n */\nconst DEFAULT_SIZE = 20\n\n/**\n * The four products Identity serves, as one list. Everything else keys off it:\n * ProductSlug is derived from it, and GLYPHS is typed against it so adding a\n * slug without artwork is a compile error rather than a mark that silently\n * draws nothing. These are Identity's *slugs*, not display names -- the Wallet\n * keys its own record on `'Boost Score' | ... | 'Wallet'`, which a rename\n * breaks, and that is the exact trap this ordering avoids.\n */\nexport const PRODUCT_SLUGS = ['wallet', 'score', 'voice', 'outbound'] as const\n\n/** One of Identity's four product slugs. */\nexport type ProductSlug = (typeof PRODUCT_SLUGS)[number]\n\n/**\n * The four product glyphs, lifted verbatim from `boostledger`'s\n * `src/ui/primitives/icons.tsx` (the `GLYPHS` record, as of `837d85d`) rather\n * than redrawn from any written description. Redrawing them from\n * `docs/mintlify/system/product-marks.mdx` is how PR #4 got all four wrong and\n * reintroduced the retired one-orange-element construction; if any document\n * disagrees with that file, that file wins. See B33-562.\n *\n * Each glyph is drawn on its own full 40-unit grid and inset into the middle 20\n * of the tile by the `translate(10 10) scale(0.5)` group below. The stroke\n * widths are right on that grid -- do not \"correct\" them to compensate for the\n * scale, the scale is what puts the stroke at the weight Paper draws. Every\n * element paints in `currentColor` (the tile's white glyph ink) or `none`;\n * there is no colour of any kind in here, which is what tells the four marks\n * apart by shape and never by hue.\n *\n * Typing this `Record<ProductSlug, ReactNode>` is load-bearing: a fifth slug\n * added to PRODUCT_SLUGS without a glyph here stops the build.\n */\nconst GLYPHS: Record<ProductSlug, ReactNode> = {\n // A credit card with its magnetic stripe: an outline card (fill none, so the\n // tile shows through) plus a filled stripe across it.\n wallet: (\n <>\n <rect x=\"5\" y=\"10\" width=\"30\" height=\"20\" rx=\"4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" />\n <rect x=\"5\" y=\"16\" width=\"30\" height=\"5\" fill=\"currentColor\" />\n </>\n ),\n // A gauge with the needle swung high: an arc, the needle, and the hub it\n // pivots on. Arc and needle stroke at width 4 with round caps.\n score: (\n <>\n <path d=\"M6 26 A14 14 0 0 1 34 26\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" />\n <line x1=\"20\" y1=\"26\" x2=\"28\" y2=\"14\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" />\n <circle cx=\"20\" cy=\"26\" r=\"3\" fill=\"currentColor\" />\n </>\n ),\n // A soundwave, tall in the middle: three pill bars, all centred on y=20.\n voice: (\n <>\n <rect x=\"6\" y=\"14\" width=\"7\" height=\"12\" rx=\"3.5\" fill=\"currentColor\" />\n <rect x=\"16.5\" y=\"4\" width=\"7\" height=\"32\" rx=\"3.5\" fill=\"currentColor\" />\n <rect x=\"27\" y=\"14\" width=\"7\" height=\"12\" rx=\"3.5\" fill=\"currentColor\" />\n </>\n ),\n // An arrow leaving through the top-right: the shaft and the elbow of its head,\n // both stroke width 4 with round caps and joins.\n outbound: (\n <>\n <line x1=\"8\" y1=\"32\" x2=\"32\" y2=\"8\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n <path d=\"M16 8 H32 V24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </>\n ),\n}\n\nfunction isProductSlug(slug: string): slug is ProductSlug {\n return (PRODUCT_SLUGS as readonly string[]).includes(slug)\n}\n\nexport interface ProductMarkProps {\n /**\n * Identity's product slug. An unknown one -- a product the served registry\n * carries but this package has no artwork for yet -- draws the bare tile\n * rather than throwing (ADR-0003's safe degrade), so the registry can run\n * ahead of a mark release.\n */\n slug: string\n /** Rendered width and height in px. Defaults to 20, the Wallet's own default. */\n size?: number\n /**\n * Dims the tile for a product the account is not entitled to -- the switcher's\n * unowned rows need it. It swaps the **ground only**, to --color-neutral-300;\n * the glyph stays white. Fading the glyph too would cost the shape the one\n * thing telling the four marks apart, and it is a prop rather than a wrapper\n * `opacity-40 grayscale` filter because that lands on a mid-grey visibly\n * darker than the grey Paper draws.\n */\n muted?: boolean\n /**\n * An accessible name. Omit it -- the default -- and the mark is decorative:\n * `aria-hidden`, contributing nothing to the accessibility tree. That is the\n * right answer in the switcher, where the product's name sits beside it and a\n * name here would only make a screen reader say it twice. Pass one only where\n * the mark stands alone as the sole carrier of meaning.\n */\n title?: string\n /** Escape hatch for a consumer that needs to position the mark. */\n className?: string\n}\n\n/**\n * The tile-and-glyph mark for one Boost33 product -- a rounded --color-neutral-950\n * square with the product's glyph in --color-primary-foreground. The four marks\n * share one palette and are told apart by shape alone; there is zero brand\n * orange in any of them.\n */\nexport function ProductMark({ slug, size = DEFAULT_SIZE, muted, title, className }: ProductMarkProps) {\n const titleId = useId()\n const decorative = title == null\n const glyph = isProductSlug(slug) ? GLYPHS[slug] : null\n\n return (\n <svg\n className={className != null ? `${styles.mark} ${className}` : styles.mark}\n width={size}\n height={size}\n viewBox=\"0 0 40 40\"\n role={decorative ? undefined : 'img'}\n aria-hidden={decorative ? 'true' : undefined}\n aria-labelledby={decorative ? undefined : titleId}\n >\n {!decorative && <title id={titleId}>{title}</title>}\n {/* The tile. Fill set inline, next to the muted decision that chooses it;\n the corner radius comes from the class (see ProductMark.module.css). */}\n <rect\n className={styles.ground}\n width=\"40\"\n height=\"40\"\n style={{ fill: muted ? 'var(--color-neutral-300)' : 'var(--color-neutral-950)' }}\n />\n {glyph && (\n <g\n transform=\"translate(10 10) scale(0.5)\"\n style={{ color: 'var(--color-primary-foreground)' }}\n >\n {glyph}\n </g>\n )}\n </svg>\n )\n}\n","import type { ReactNode } from 'react'\nimport styles from './NavItem.module.css'\n\nexport interface NavItemProps {\n /** The nav label. Truncates with an ellipsis rather than wrapping. */\n children: ReactNode\n /**\n * The leading icon. Decorative -- the label carries the name, so pass an\n * `aria-hidden` glyph. On the rail it is the only thing that shows, the\n * label having collapsed to the accessibility tree.\n */\n icon?: ReactNode\n /** Marks this row as the current page. Sets `aria-current=\"page\"`. */\n active?: boolean\n /** Sidebar-on-the-rail state. Hides the label and trailing slot visually; the label stays in the accessibility tree. */\n collapsed?: boolean\n /** Trailing slot content. Omit to leave the slot empty -- it still reserves its width so counts line up across rows. */\n count?: number\n /** Renders `count` in the primary color instead of muted. */\n countUrgent?: boolean\n href?: string\n onClick?: () => void\n}\n\nexport function NavItem({ children, icon, active = false, collapsed = false, count, countUrgent = false, href, onClick }: NavItemProps) {\n const dataProps = {\n 'data-active': active ? ('true' as const) : undefined,\n 'data-collapsed': collapsed ? ('true' as const) : undefined,\n 'aria-current': active ? ('page' as const) : undefined,\n }\n\n const content = (\n <>\n {icon != null && (\n <span className={styles.icon} aria-hidden=\"true\">\n {icon}\n </span>\n )}\n <span className={styles.label}>{children}</span>\n <span className={styles.trailingSlot}>\n {count != null && <span className={countUrgent ? styles.countUrgent : styles.count}>{count}</span>}\n </span>\n </>\n )\n\n if (href) {\n return (\n <a className={styles.row} href={href} onClick={onClick} {...dataProps}>\n {content}\n </a>\n )\n }\n\n return (\n <button type=\"button\" className={styles.row} onClick={onClick} {...dataProps}>\n {content}\n </button>\n )\n}\n","// The two numbers the chip says out loud. Kept separate from the component so\n// the rounding and the separator -- the parts that carry meaning rather than\n// look -- are unit-tested without a render.\n\n/** The full balance, thousands grouped: 4600 -> \"4,600\". */\nexport function formatTokens(value: number): string {\n return value.toLocaleString('en-US')\n}\n\n/**\n * The rail's short form: one decimal thousand, always. At 56px there is no\n * room for four digits and a separator, and \"4.6k\" still answers \"am I fine?\".\n */\nexport function abbreviateThousands(value: number): string {\n return `${(value / 1000).toFixed(1)}k`\n}\n","// Small monochrome glyphs the chip draws in `currentColor` -- no fill token, so\n// the surrounding text colour carries them and the no-hardcoded-hex guard stays\n// satisfied. Decorative: the balance text is what carries meaning, so both are\n// aria-hidden.\n\n/** The card mark shown on the rail, where there is no room for the word \"Wallet\". */\nexport function WalletMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.75\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <rect x=\"3\" y=\"6\" width=\"18\" height=\"12\" rx=\"2\" />\n <path d=\"M3 10h18\" />\n </svg>\n )\n}\n\n/** The retry affordance shown when the balance could not be confirmed. */\nexport function RetryMark({ className }: { className?: string }) {\n return (\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.75\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M21 12a9 9 0 1 1-2.64-6.36\" />\n <path d=\"M21 3v4h-4\" />\n </svg>\n )\n}\n","import styles from './WalletChip.module.css'\nimport { abbreviateThousands, formatTokens } from './format'\nimport { RetryMark, WalletMark } from './icons'\n\n// Past this fraction of the cycle's grant burned, the chip stops resting. The\n// spec's threshold, in one place so the derivation and the styling agree.\nconst THRESHOLD_BURN = 0.8\n\nexport interface WalletChipProps {\n /** Remaining granted tokens. Granted burns first and expires. */\n granted: number\n /** Remaining purchased tokens. Purchased never expires. */\n purchased: number\n /**\n * The grant issued for this cycle. The denominator for \"how much has burned\",\n * and so for the threshold; drawn as the bar's full width alongside purchased\n * so the filled portion shortens as the month runs.\n */\n grantCycle: number\n /** When the current grant expires, preformatted for display, e.g. \"30 Sep\". */\n grantExpires?: string\n /**\n * The threshold state's runway line, preformatted, e.g. \"About four days left\n * at your current burn.\" Passed in rather than derived: the burn estimate is\n * the product's to make, not the chip's -- the chip renders what it is given.\n */\n burnNote?: string\n /** The balance could not be confirmed. Renders as unknown, never as zero. */\n unknown?: boolean\n /** The 56px rail. Abbreviates the balance and turns the split on its side. */\n collapsed?: boolean\n /** Top up. Wiring for the one button the sidebar is allowed to have. */\n onTopUp?: () => void\n}\n\n/** The granted/purchased bar. Granted is drawn first, because granted burns first. */\nfunction Bar({ granted, purchased, cycleTotal, tone }: { granted: number; purchased: number; cycleTotal: number; tone: 'rest' | 'threshold' }) {\n const width = (n: number) => `${(n / cycleTotal) * 100}%`\n const remaining = granted + purchased\n const label = `${formatTokens(granted)} granted and ${formatTokens(purchased)} purchased of ${formatTokens(remaining)} remaining`\n return (\n <div className={styles.bar} data-tone={tone} role=\"img\" aria-label={label}>\n <span className={styles.segmentGranted} data-testid=\"segment-granted\" style={{ width: width(granted) }} />\n <span className={styles.segmentPurchased} data-testid=\"segment-purchased\" style={{ width: width(purchased) }} />\n </div>\n )\n}\n\nexport function WalletChip({ granted, purchased, grantCycle, grantExpires, burnNote, unknown = false, collapsed = false, onTopUp }: WalletChipProps) {\n const balance = granted + purchased\n // Everything the cycle started with: the grant plus the purchased balance\n // sitting alongside it. Measuring against this is what makes the bar shorten\n // as tokens burn instead of only re-proportioning to a full width.\n const cycleTotal = grantCycle + purchased\n const burned = grantCycle > 0 ? (grantCycle - granted) / grantCycle : 0\n const isThreshold = !unknown && burned >= THRESHOLD_BURN\n\n if (collapsed) {\n return (\n <div className={styles.rail} role=\"group\" aria-label={unknown ? 'Wallet balance unknown' : `Wallet balance ${abbreviateThousands(balance)}`} tabIndex={0}>\n <WalletMark className={styles.railIcon} />\n <span className={styles.railFigure}>{unknown ? '—' : abbreviateThousands(balance)}</span>\n {unknown ? <span className={styles.srOnly}>unknown</span> : <Bar granted={granted} purchased={purchased} cycleTotal={cycleTotal} tone={isThreshold ? 'threshold' : 'rest'} />}\n </div>\n )\n }\n\n return (\n <div className={styles.chip} role=\"group\" aria-label=\"Wallet balance\" tabIndex={0} data-threshold={isThreshold ? 'true' : undefined} data-unknown={unknown ? 'true' : undefined}>\n <div className={styles.header}>\n <span className={styles.name}>Wallet</span>\n {isThreshold && <span className={styles.pill}>{Math.round(burned * 100)}%</span>}\n </div>\n\n {unknown ? (\n <div className={styles.figure}>\n <span className={styles.amount} aria-hidden=\"true\">\n {'—'}\n </span>\n <RetryMark className={styles.retry} />\n <span className={styles.srOnly}>Balance unknown</span>\n </div>\n ) : (\n <div className={styles.figure}>\n <span className={styles.amount}>{formatTokens(balance)}</span>\n <span className={styles.unit}>tokens</span>\n </div>\n )}\n\n {unknown ? (\n <div className={styles.bar} data-tone=\"unknown\" aria-hidden=\"true\" />\n ) : (\n <Bar granted={granted} purchased={purchased} cycleTotal={cycleTotal} tone={isThreshold ? 'threshold' : 'rest'} />\n )}\n\n {!unknown && !isThreshold && (\n <div className={styles.details}>\n <dl className={styles.split}>\n <div className={styles.splitRow}>\n <dt className={styles.splitLabel}>\n <span className={styles.dotGranted} aria-hidden=\"true\" />\n Granted\n </dt>\n <dd className={styles.splitAmount}>{formatTokens(granted)}</dd>\n </div>\n <div className={styles.splitRow}>\n <dt className={styles.splitLabel}>\n <span className={styles.dotPurchased} aria-hidden=\"true\" />\n Purchased\n </dt>\n <dd className={styles.splitAmount}>{formatTokens(purchased)}</dd>\n </div>\n </dl>\n {grantExpires && <p className={styles.footer}>Granted expires {grantExpires} &middot; burns first</p>}\n </div>\n )}\n\n {isThreshold && burnNote && <p className={styles.burnNote}>{burnNote}</p>}\n\n {!unknown && (\n <button type=\"button\" className={styles.topUp} data-grown={isThreshold ? 'true' : undefined} onClick={onTopUp}>\n Top up\n </button>\n )}\n </div>\n )\n}\n","import type { ReactNode } from 'react'\nimport styles from './TopBar.module.css'\n\nexport interface TopBarProps {\n /** The breadcrumb naming the current location, on the left of the bar. */\n breadcrumb: ReactNode\n /** Shell actions on the right -- search, notifications, the account. Omit to leave the right empty. */\n actions?: ReactNode\n}\n\nexport function TopBar({ breadcrumb, actions }: TopBarProps) {\n return (\n <div className={styles.bar}>\n <div className={styles.breadcrumb}>{breadcrumb}</div>\n {actions != null && <div className={styles.actions}>{actions}</div>}\n </div>\n )\n}\n","import type { ReactNode } from 'react'\nimport styles from './PageHeader.module.css'\n\nexport interface PageHeaderProps {\n /** The page's name. Renders as the page heading. */\n title: ReactNode\n /** A line under the title. Omit for a title-only header. */\n subtitle?: ReactNode\n /** Trailing controls, aligned to the right edge. Omit to leave the row empty. */\n actions?: ReactNode\n}\n\nexport function PageHeader({ title, subtitle, actions }: PageHeaderProps) {\n return (\n <header className={styles.header}>\n <div className={styles.headings}>\n <h1 className={styles.title}>{title}</h1>\n {subtitle != null && <p className={styles.subtitle}>{subtitle}</p>}\n </div>\n {actions != null && <div className={styles.actions}>{actions}</div>}\n </header>\n )\n}\n","import type { ReactNode } from 'react'\nimport { Sidebar } from '../Sidebar/Sidebar'\nimport { NavSection } from '../NavSection/NavSection'\nimport { NavItem } from '../NavItem/NavItem'\nimport { TopBar, type TopBarProps } from '../TopBar/TopBar'\nimport { PageHeader, type PageHeaderProps } from '../PageHeader/PageHeader'\nimport { Wordmark } from '../Wordmark/Wordmark'\nimport styles from './AppShell.module.css'\n\n/** One nav row, described as data. The consumer never hands the shell markup. */\nexport interface AppShellNavItem {\n /** The row's name. Truncates rather than wrapping; on the rail it collapses to the accessibility tree. */\n label: string\n /** The leading icon. Decorative -- the label carries the name. On the rail it is all that shows. */\n icon?: ReactNode\n /** Marks this row as the current page. Sets `aria-current=\"page\"`. */\n active?: boolean\n /** Trailing-slot count. Omit to leave the slot empty; it still reserves its width so counts line up. */\n count?: number\n /** Renders `count` in the primary color instead of muted. */\n countUrgent?: boolean\n /** Where the row goes. A nav row's destination is data; the shell renders it as a link. */\n href?: string\n}\n\n/** One nav section: an optional header over a run of rows. */\nexport interface AppShellNavSection {\n /** The section header. Omit for a headerless section (the nav's first section has none). On the rail every header drops. */\n label?: string\n /** The section's rows, as data. */\n items: AppShellNavItem[]\n}\n\nexport interface AppShellProps {\n /**\n * The product this shell renders for. The consumer names it once, here, and\n * the shell carries it down -- it labels the sidebar and its navigation, and\n * nothing above the nav (the Boost33 wordmark and brand) changes between\n * products. This one prop is the whole of the shell's product-awareness.\n */\n product: string\n /** The navigation, as data. No product hardcodes rows; the shell composes them. */\n nav: AppShellNavSection[]\n /** The top bar's content -- the breadcrumb and the shell actions on its right. */\n topBar: TopBarProps\n /** The page header's content -- the page title, its optional subtitle and trailing controls. */\n pageHeader: PageHeaderProps\n /** The Boost33 mark for the wordmark lockup. Passed in, the way Wordmark takes it; on the rail it is all that shows. */\n mark?: ReactNode\n /**\n * The product switcher, sitting under the wordmark. A slot: the registry it\n * draws is served by Identity (ADR-0003), not owned here. Render its own\n * collapsed form for the rail.\n */\n switcher?: ReactNode\n /**\n * The wallet chip, pinned above the footer. A slot: the balance comes from\n * `@boost33/ledger-client`, never read here. Render its own collapsed form\n * for the rail.\n */\n wallet?: ReactNode\n /** The sidebar footer, below the wallet chip. A slot -- the product's own. */\n footer?: ReactNode\n /**\n * The sidebar collapsed to the 56px rail. A state of the shell: the sidebar\n * narrows and every piece it owns collapses off this one flag. The slots\n * (switcher, wallet, footer) render their own collapsed form.\n */\n collapsed?: boolean\n /** The page body. Sits on white, below the header. */\n children: ReactNode\n}\n\n// The shell every authenticated Boost33 screen renders inside. It composes the\n// pieces built in the earlier tickets -- sidebar, wordmark, switcher, nav,\n// wallet chip, top bar, page header -- into one thing a product imports instead\n// of building a sidebar. Product-aware only through `product`; everything below\n// that is the product's own data, and nothing above it changes.\nexport function AppShell({\n product,\n nav,\n topBar,\n pageHeader,\n mark,\n switcher,\n wallet,\n footer,\n collapsed = false,\n children,\n}: AppShellProps) {\n return (\n <div className={styles.shell}>\n <Sidebar label={product} collapsed={collapsed}>\n <div className={styles.wordmark}>\n <Wordmark mark={mark} collapsed={collapsed} />\n </div>\n {switcher != null && <div className={styles.switcher}>{switcher}</div>}\n <nav aria-label={product} className={styles.nav}>\n {nav.map((section, sectionIndex) => (\n // No room for a header at 56px, so on the rail every section renders\n // headerless -- its rows collapse to their icons and line up.\n <NavSection key={sectionIndex} label={collapsed ? undefined : section.label}>\n {section.items.map((item, itemIndex) => (\n <NavItem\n key={itemIndex}\n icon={item.icon}\n active={item.active}\n collapsed={collapsed}\n count={item.count}\n countUrgent={item.countUrgent}\n href={item.href}\n >\n {item.label}\n </NavItem>\n ))}\n </NavSection>\n ))}\n </nav>\n {(wallet != null || footer != null) && (\n <div className={styles.foot}>\n {wallet}\n {footer}\n </div>\n )}\n </Sidebar>\n <div className={styles.main}>\n <TopBar {...topBar} />\n <PageHeader {...pageHeader} />\n <div className={styles.content}>{children}</div>\n </div>\n </div>\n )\n}\n"],"names":["NAV_HEIGHT_BUDGET_PX","NAV_SECTION_HEADER_PX","NAV_ITEM_PX","NAV_SHELL_FIXED_PX","navContentHeight","sections","used","section","navBudgetForViewport","viewportPx","checkNavHeightBudget","usedPx","budgetPx","measureNavHeight","nav","Sidebar","label","collapsed","children","column","useRef","useNavHeightBudgetWarning","jsx","styles","useEffect","element","budget","NavSection","headerId","useId","jsxs","Wordmark","mark","DEFAULT_SIZE","BOOST33_MARK_MIN_LEGIBLE_PX","Boost33Mark","size","title","className","titleId","decorative","PRODUCT_SLUGS","GLYPHS","Fragment","isProductSlug","slug","ProductMark","muted","glyph","NavItem","icon","active","count","countUrgent","href","onClick","dataProps","content","formatTokens","value","abbreviateThousands","WalletMark","RetryMark","THRESHOLD_BURN","Bar","granted","purchased","cycleTotal","tone","width","n","remaining","WalletChip","grantCycle","grantExpires","burnNote","unknown","onTopUp","balance","burned","isThreshold","TopBar","breadcrumb","actions","PageHeader","subtitle","AppShell","product","topBar","pageHeader","switcher","wallet","footer","sectionIndex","item","itemIndex"],"mappings":"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"}
@@ -1,4 +1,23 @@
1
1
  export declare const NAV_HEIGHT_BUDGET_PX = 700;
2
+ export declare const NAV_SECTION_HEADER_PX = 40;
3
+ export declare const NAV_ITEM_PX = 32;
4
+ export declare const NAV_SHELL_FIXED_PX = 198;
5
+ /** One section of a nav, described by its shape rather than its rendered rows. */
6
+ export interface NavSectionShape {
7
+ /** Whether the section carries a header label (its 40px cost). */
8
+ hasHeader: boolean;
9
+ /** How many nav items the section holds (32px each). */
10
+ itemCount: number;
11
+ }
12
+ /**
13
+ * The vertical space a nav takes, summed from its sections' shapes. Lets the
14
+ * budget be checked from the nav's data alone, before it is rendered into a
15
+ * sidebar that might not fit it -- Boost Outbound's admin role has overflowed
16
+ * this once already.
17
+ */
18
+ export declare function navContentHeight(sections: NavSectionShape[]): number;
19
+ /** The nav's budget at a given viewport: what is left once the fixed shell parts are taken. */
20
+ export declare function navBudgetForViewport(viewportPx: number): number;
2
21
  export interface NavHeightBudget {
3
22
  usedPx: number;
4
23
  budgetPx: number;
package/dist/style.css CHANGED
@@ -1 +1 @@
1
- :root{--color-neutral-0: #FFFFFF;--color-neutral-50: #F7F7F8;--color-neutral-100: #EFEFF1;--color-neutral-200: #E5E5EA;--color-neutral-300: #D9D9D9;--color-neutral-400: #C1C1C2;--color-neutral-500: #8E8E93;--color-neutral-600: #737373;--color-neutral-700: #525252;--color-neutral-800: #3A3A3C;--color-neutral-900: #292929;--color-neutral-950: #1D1D1B;--color-neutral-1000: #000000;--color-orange-50: #FDF3EE;--color-orange-100: #FBE2D5;--color-orange-200: #F7C3A8;--color-orange-300: #F0A173;--color-orange-400: #EB7147;--color-orange-500: #E9521E;--color-orange-600: #C2410C;--color-orange-700: #9A3412;--color-sand-100: #FBF3E4;--color-sand-300: #F3D6AC;--color-sand-600: #8A6510;--color-green-100: #DDF0E4;--color-green-600: #157F3D;--color-crimson-100: #F7DEE1;--color-crimson-600: #A3182B;--color-blue-100: #DEE6FA;--color-blue-600: #2A5BD7;--color-background: var(--color-neutral-50);--color-surface: var(--color-neutral-0);--color-surface-muted: var(--color-neutral-100);--color-border: var(--color-neutral-200);--color-border-strong: var(--color-neutral-300);--color-foreground: var(--color-neutral-950);--color-foreground-secondary: var(--color-neutral-700);--color-foreground-muted: var(--color-neutral-600);--color-foreground-disabled: var(--color-neutral-400);--color-primary: var(--color-orange-500);--color-primary-hover: var(--color-orange-600);--color-primary-foreground: var(--color-neutral-0);--color-primary-subtle: var(--color-orange-50);--color-primary-border: var(--color-orange-200);--color-success: var(--color-green-600);--color-success-subtle: var(--color-green-100);--color-warning: var(--color-sand-600);--color-warning-subtle: var(--color-sand-300);--color-danger: var(--color-crimson-600);--color-danger-subtle: var(--color-crimson-100);--color-info: var(--color-blue-600);--color-info-subtle: var(--color-blue-100);--color-grant: var(--color-sand-600);--color-purchased: var(--color-neutral-950);--font-display: Outfit;--font-sans: Inter;--font-mono: Geist Mono;--text-2xs: 11px;--text-xs: 12px;--text-sm: 13px;--text-base: 15px;--text-md: 16px;--text-lg: 18px;--text-xl: 20px;--text-2xl: 24px;--text-3xl: 30px;--text-4xl: 36px;--text-5xl: 46px;--text-6xl: 60px;--text-7xl: 78px;--text-nav-label: 14px;--font-weight-light: 300;--font-weight-regular: 400;--font-weight-medium: 500;--font-weight-semibold: 600;--font-weight-bold: 700;--tracking-tighter: -.03em;--tracking-tight: -.02em;--tracking-normal: 0em;--tracking-wide: .04em;--tracking-wider: .09em;--leading-none: 100%;--leading-tight: 120%;--leading-snug: 135%;--leading-normal: 150%;--leading-relaxed: 170%;--breakpoint-mobile: 390px;--breakpoint-desktop: 1440px;--container-sidebar: 264px;--container-content: 1440px;--spacing-1: 4px;--spacing-2: 8px;--spacing-3: 12px;--spacing-4: 16px;--spacing-5: 20px;--spacing-6: 24px;--spacing-8: 32px;--spacing-10: 40px;--spacing-12: 48px;--spacing-16: 64px;--spacing-20: 80px;--spacing-2_5: 10px;--spacing-5_5: 22px;--radius-none: 0px;--radius-xs: 2px;--radius-sm: 3px;--radius-md: 4px;--radius-lg: 6px;--radius-xl: 8px;--radius-2xl: 12px;--radius-full: 9999px}:root{--duration-fast: .12s;--easing-standard: cubic-bezier(.2, 0, 0, 1)}._row_5f12t_4{display:flex;align-items:center;width:100%;gap:var(--spacing-2_5);padding:var(--spacing-2) var(--spacing-2_5);border:none;border-radius:var(--radius-lg);background:transparent;font-family:var(--font-sans);text-align:left;text-decoration:none;cursor:pointer;transition:background-color var(--duration-fast) var(--easing-standard)}._row_5f12t_4:hover{background:var(--color-surface-muted)}._row_5f12t_4[data-active=true]{background:var(--color-surface)}._row_5f12t_4:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}._label_5f12t_36{flex:1 1 auto;min-width:0;overflow:hidden;color:var(--color-foreground-secondary);font-size:var(--text-nav-label);font-weight:var(--font-weight-regular);text-overflow:ellipsis;white-space:nowrap}._row_5f12t_4[data-active=true] ._label_5f12t_36{color:var(--color-foreground);font-weight:var(--font-weight-semibold)}._row_5f12t_4[data-collapsed=true] ._label_5f12t_36{position:absolute;overflow:hidden;clip-path:inset(50%);width:1px;height:1px;white-space:nowrap}._trailingSlot_5f12t_62{display:flex;flex:0 0 var(--spacing-5_5);align-items:center;justify-content:center;width:var(--spacing-5_5);font-family:var(--font-sans);font-size:var(--text-nav-label)}._row_5f12t_4[data-collapsed=true] ._trailingSlot_5f12t_62{display:none}._count_5f12t_76{color:var(--color-foreground-muted)}._countUrgent_5f12t_80{color:var(--color-primary)}@media(prefers-reduced-motion:reduce){._row_5f12t_4{transition:none}}
1
+ ._sidebar_17kel_7{display:flex;flex-direction:column;box-sizing:border-box;width:var(--container-sidebar);height:100%;padding-inline:var(--spacing-3);border-right:1px solid var(--color-border);background:var(--color-background);font-family:var(--font-sans)}._sidebar_17kel_7[data-collapsed=true]{width:var(--container-sidebar-rail)}._sidebar_17kel_7>nav{flex-shrink:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;margin:calc(-1 * var(--spacing-1));padding:var(--spacing-1);scroll-padding:var(--spacing-1)}@media(prefers-reduced-motion:reduce){._sidebar_17kel_7>nav{scroll-behavior:auto}}._section_3sxsg_5{display:flex;flex-direction:column}._header_3sxsg_13{margin:0;padding:var(--spacing-2) var(--spacing-2_5);color:var(--color-foreground-muted);font-family:var(--font-sans);font-size:var(--text-2xs);font-weight:var(--font-weight-semibold);letter-spacing:var(--tracking-caps);text-transform:uppercase}._wordmark_1k88r_4{display:flex;align-items:center;gap:var(--spacing-2_5);font-family:var(--font-display)}._mark_1k88r_17{display:inline-flex;flex:0 0 var(--spacing-6);align-items:center;justify-content:center;width:var(--spacing-6);height:var(--spacing-6)}._mark_1k88r_17 svg{width:100%;height:100%}._name_1k88r_35{color:var(--color-foreground);font-family:var(--font-display);font-size:var(--text-lg);font-weight:var(--font-weight-semibold);letter-spacing:var(--tracking-tight)}._wordmark_1k88r_4[data-collapsed=true] ._name_1k88r_35{position:absolute;overflow:hidden;clip-path:inset(50%);width:1px;height:1px;white-space:nowrap}._wordmark_1k88r_4[data-collapsed=true]{justify-content:center}._tile_5vhrh_12{fill:var(--color-neutral-950)}._ring_5vhrh_16{fill:var(--color-primary)}._mark_5vhrh_24{display:block}._mark_1jt5g_6{display:block}._ground_1jt5g_17{rx:var(--radius-product-tile)}._row_16hr7_4{display:flex;align-items:center;width:100%;gap:var(--spacing-2_5);padding:var(--spacing-2) var(--spacing-2_5);border:none;border-radius:var(--radius-lg);background:transparent;font-family:var(--font-sans);text-align:left;text-decoration:none;cursor:pointer;transition:background-color var(--duration-fast) var(--easing-standard)}._row_16hr7_4:hover{background:var(--color-surface-muted)}._row_16hr7_4[data-active=true]{background:var(--color-surface)}._row_16hr7_4:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}._icon_16hr7_41{display:inline-flex;flex:0 0 var(--spacing-4);align-items:center;justify-content:center;width:var(--spacing-4);height:var(--spacing-4);color:var(--color-foreground-secondary)}._icon_16hr7_41 svg{width:100%;height:100%}._row_16hr7_4[data-active=true] ._icon_16hr7_41{color:var(--color-foreground)}._row_16hr7_4[data-collapsed=true]{justify-content:center}._label_16hr7_66{flex:1 1 auto;min-width:0;overflow:hidden;color:var(--color-foreground-secondary);font-size:var(--text-nav-label);font-weight:var(--font-weight-regular);text-overflow:ellipsis;white-space:nowrap}._row_16hr7_4[data-active=true] ._label_16hr7_66{color:var(--color-foreground);font-weight:var(--font-weight-semibold)}._row_16hr7_4[data-collapsed=true] ._label_16hr7_66{position:absolute;overflow:hidden;clip-path:inset(50%);width:1px;height:1px;white-space:nowrap}._trailingSlot_16hr7_92{display:flex;flex:0 0 var(--spacing-5_5);align-items:center;justify-content:center;width:var(--spacing-5_5);font-family:var(--font-sans);font-size:var(--text-nav-label)}._row_16hr7_4[data-collapsed=true] ._trailingSlot_16hr7_92{display:none}._count_16hr7_106{color:var(--color-foreground-muted)}._countUrgent_16hr7_110{color:var(--color-primary)}@media(prefers-reduced-motion:reduce){._row_16hr7_4{transition:none}}:root{--duration-fast: .12s;--easing-standard: cubic-bezier(.2, 0, 0, 1)}:root{--shadow-overlay: 0px 16px 40px -10px var(--shadow-overlay-color)}._chip_676ll_37{--chip-lift: 0px;position:relative;box-sizing:border-box;width:100%;padding:calc(var(--spacing-3) + var(--chip-lift));border:1px solid var(--color-border);border-radius:var(--radius-2xl);background:transparent;font-family:var(--font-sans)}._chip_676ll_37:not([data-unknown]):hover,._chip_676ll_37:not([data-unknown]):focus-within,._chip_676ll_37[data-threshold]{--chip-lift: var(--spacing-0_5);box-shadow:var(--shadow-overlay)}._chip_676ll_37:not([data-threshold]):not([data-unknown]):hover,._chip_676ll_37:not([data-threshold]):not([data-unknown]):focus-within{background:var(--color-surface)}._chip_676ll_37:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}._header_676ll_74{display:flex;align-items:center;gap:var(--spacing-2)}._name_676ll_83{color:var(--color-foreground-muted);font-size:var(--text-xs);font-weight:var(--font-weight-medium);line-height:var(--leading-tight)}._pill_676ll_94{padding:var(--spacing-0_5) var(--spacing-2);border-radius:var(--radius-full);background:var(--color-warning-subtle);color:var(--color-warning);font-size:var(--text-2xs);font-weight:var(--font-weight-semibold)}._figure_676ll_103{display:flex;align-items:baseline;gap:var(--spacing-1_5);margin-top:calc(var(--spacing-2_5) + var(--chip-lift))}._amount_676ll_118{color:var(--color-foreground);font-family:var(--font-mono);font-size:var(--text-2xl);font-weight:var(--font-weight-bold);letter-spacing:var(--tracking-tight);line-height:var(--leading-none)}._unit_676ll_127{color:var(--color-foreground-muted);font-size:var(--text-xs)}._retry_676ll_132{width:var(--spacing-3);height:var(--spacing-3);color:var(--color-foreground-muted)}._bar_676ll_141{display:flex;overflow:hidden;width:100%;height:calc(var(--spacing-1_25) + var(--chip-lift));margin-top:calc(var(--spacing-2_5) + var(--chip-lift));border-radius:var(--radius-full)}._bar_676ll_141[data-tone=rest]{background:var(--color-surface-muted)}._bar_676ll_141[data-tone=threshold]{background:var(--color-warning-subtle)}._bar_676ll_141[data-tone=unknown]{background:var(--color-surface-muted)}._segmentGranted_676ll_162,._segmentPurchased_676ll_163{height:100%;transition:width var(--duration-fast) var(--easing-standard)}._segmentGranted_676ll_162{background:var(--color-grant)}._segmentPurchased_676ll_163{background:var(--color-purchased)}._details_676ll_183{display:none}._chip_676ll_37:hover ._details_676ll_183,._chip_676ll_37:focus-within ._details_676ll_183{display:block}._split_676ll_192{display:flex;flex-direction:column;gap:var(--spacing-1);margin:var(--spacing-2_5) 0 0}._splitRow_676ll_199{display:flex;align-items:center;justify-content:space-between}._splitLabel_676ll_205{display:flex;align-items:center;gap:var(--spacing-1_5);color:var(--color-foreground);font-size:var(--text-xs)}._splitAmount_676ll_213{margin:0;color:var(--color-foreground);font-family:var(--font-mono);font-size:var(--text-xs);font-variant-numeric:tabular-nums}._dotGranted_676ll_221,._dotPurchased_676ll_222{width:var(--spacing-1_5);height:var(--spacing-1_5);border-radius:var(--radius-full)}._dotGranted_676ll_221{background:var(--color-grant)}._dotPurchased_676ll_222{background:var(--color-purchased)}._footer_676ll_236{margin:var(--spacing-3) 0 0;padding-top:var(--spacing-1);border-top:1px solid var(--color-border);color:var(--color-foreground-muted);font-size:var(--text-2xs)}._burnNote_676ll_244{margin:var(--spacing-2_5) 0 0;color:var(--color-foreground-secondary);font-size:var(--text-2xs);line-height:var(--leading-snug)}._topUp_676ll_256{position:absolute;top:calc(var(--spacing-3) + var(--chip-lift));right:calc(var(--spacing-3) + var(--chip-lift));padding:0;border:none;background:transparent;color:var(--color-primary);font-family:var(--font-sans);font-size:var(--text-xs);font-weight:var(--font-weight-semibold);opacity:0;cursor:pointer;transition:opacity var(--duration-fast) var(--easing-standard)}._chip_676ll_37:hover ._topUp_676ll_256,._chip_676ll_37:focus-within ._topUp_676ll_256,._topUp_676ll_256:focus-visible{opacity:1}._topUp_676ll_256:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}._topUp_676ll_256[data-grown=true]{position:static;width:100%;margin-top:var(--spacing-2_5);padding:var(--spacing-2);border-radius:var(--radius-lg);background:var(--color-primary);color:var(--color-primary-foreground);font-size:var(--text-sm);opacity:1}._topUp_676ll_256[data-grown=true]:hover{background:var(--color-primary-hover)}._rail_676ll_310{--chip-lift: 0px;position:relative;display:flex;box-sizing:border-box;width:100%;flex-direction:column;align-items:center;gap:var(--spacing-2);padding:var(--spacing-2);font-family:var(--font-sans)}._railIcon_676ll_324{width:var(--spacing-6);height:var(--spacing-6);color:var(--color-foreground-secondary)}._railFigure_676ll_330{color:var(--color-foreground);font-family:var(--font-mono);font-size:var(--text-sm);font-weight:var(--font-weight-semibold);font-variant-numeric:tabular-nums}._rail_676ll_310 ._bar_676ll_141{height:var(--spacing-0_5);margin-top:0}._srOnly_676ll_343{position:absolute;overflow:hidden;clip-path:inset(50%);width:1px;height:1px;white-space:nowrap}@media(prefers-reduced-motion:reduce){._segmentGranted_676ll_162,._segmentPurchased_676ll_163,._topUp_676ll_256{transition:none}}._bar_r1jjn_6{display:flex;align-items:center;justify-content:space-between;gap:var(--spacing-4);box-sizing:border-box;height:var(--spacing-12);padding:0 var(--spacing-8);border-bottom:1px solid var(--color-border);background:var(--color-surface);font-family:var(--font-sans)}._breadcrumb_r1jjn_19{display:flex;min-width:0;align-items:center;gap:var(--spacing-2);overflow:hidden;color:var(--color-foreground);font-size:var(--text-sm);font-weight:var(--font-weight-medium);text-overflow:ellipsis;white-space:nowrap}._actions_r1jjn_32{display:flex;flex:0 0 auto;align-items:center;gap:var(--spacing-2)}._header_18p6i_13{display:flex;align-items:center;justify-content:space-between;gap:var(--spacing-8);padding:var(--spacing-7) var(--spacing-8) var(--spacing-6);background:var(--color-surface);font-family:var(--font-sans)}._headings_18p6i_23{display:flex;min-width:0;flex-direction:column;gap:var(--spacing-1)}._title_18p6i_30{margin:0;color:var(--color-foreground);font-family:var(--font-display);font-size:var(--text-2xl);font-weight:var(--font-weight-bold);line-height:var(--leading-tight)}._subtitle_18p6i_39{margin:0;color:var(--color-foreground-secondary);font-size:var(--text-base);font-weight:var(--font-weight-regular);line-height:var(--leading-snug)}._actions_18p6i_47{display:flex;flex:0 0 auto;align-items:center;gap:var(--spacing-2)}:root{--color-neutral-0: #FFFFFF;--color-neutral-50: #F7F7F8;--color-neutral-100: #EFEFF1;--color-neutral-200: #E5E5EA;--color-neutral-300: #D9D9D9;--color-neutral-400: #C1C1C2;--color-neutral-500: #8E8E93;--color-neutral-600: #6B6B6B;--color-neutral-700: #525252;--color-neutral-800: #3A3A3C;--color-neutral-900: #292929;--color-neutral-950: #1D1D1B;--color-neutral-1000: #000000;--color-orange-50: #FDF3EE;--color-orange-100: #FBE2D5;--color-orange-200: #F7C3A8;--color-orange-300: #F0A173;--color-orange-400: #EB7147;--color-orange-500: #E9521E;--color-orange-600: #C2410C;--color-orange-700: #9A3412;--color-sand-100: #FBF3E4;--color-sand-300: #F3D6AC;--color-sand-600: #8A6510;--color-green-100: #DDF0E4;--color-green-600: #157F3D;--color-crimson-100: #F7DEE1;--color-crimson-600: #A3182B;--color-blue-100: #DEE6FA;--color-blue-600: #2A5BD7;--color-background: var(--color-neutral-50);--color-surface: var(--color-neutral-0);--color-surface-muted: var(--color-neutral-100);--color-border: var(--color-neutral-200);--color-border-strong: var(--color-neutral-300);--color-foreground: var(--color-neutral-950);--color-foreground-secondary: var(--color-neutral-700);--color-foreground-muted: var(--color-neutral-600);--color-foreground-disabled: var(--color-neutral-400);--color-primary: var(--color-orange-500);--color-primary-hover: var(--color-orange-600);--color-primary-foreground: var(--color-neutral-0);--color-primary-subtle: var(--color-orange-50);--color-primary-border: var(--color-orange-200);--color-success: var(--color-green-600);--color-success-subtle: var(--color-green-100);--color-warning: var(--color-sand-600);--color-warning-subtle: var(--color-sand-300);--color-danger: var(--color-crimson-600);--color-danger-subtle: var(--color-crimson-100);--color-info: var(--color-blue-600);--color-info-subtle: var(--color-blue-100);--color-grant: var(--color-sand-600);--color-purchased: var(--color-neutral-950);--shadow-overlay-color: rgb(29 29 27 / 20%);--font-display: Outfit, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--font-sans: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--font-mono: "Geist Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;--text-2xs: 11px;--text-xs: 12px;--text-sm: 13px;--text-base: 15px;--text-md: 16px;--text-lg: 18px;--text-xl: 20px;--text-2xl: 24px;--text-3xl: 30px;--text-4xl: 36px;--text-5xl: 46px;--text-6xl: 60px;--text-7xl: 78px;--text-nav-label: 14px;--font-weight-light: 300;--font-weight-regular: 400;--font-weight-medium: 500;--font-weight-semibold: 600;--font-weight-bold: 700;--tracking-tighter: -.03em;--tracking-tight: -.02em;--tracking-normal: 0em;--tracking-wide: .04em;--tracking-wider: .09em;--tracking-caps: .05em;--leading-none: 100%;--leading-tight: 120%;--leading-snug: 135%;--leading-normal: 150%;--leading-relaxed: 170%;--breakpoint-mobile: 390px;--breakpoint-desktop: 1440px;--container-sidebar: 264px;--container-sidebar-rail: 56px;--container-content: 1440px;--spacing-1: 4px;--spacing-2: 8px;--spacing-3: 12px;--spacing-4: 16px;--spacing-5: 20px;--spacing-6: 24px;--spacing-7: 28px;--spacing-8: 32px;--spacing-10: 40px;--spacing-12: 48px;--spacing-16: 64px;--spacing-20: 80px;--spacing-2_5: 10px;--spacing-5_5: 22px;--spacing-1_5: 6px;--spacing-1_25: 5px;--spacing-0_5: 2px;--radius-none: 0px;--radius-xs: 2px;--radius-sm: 3px;--radius-md: 4px;--radius-lg: 6px;--radius-xl: 8px;--radius-2xl: 12px;--radius-full: 9999px;--radius-product-tile: 25%}._shell_1xurb_10{display:flex;height:100%;background:var(--color-surface);font-family:var(--font-sans)}._wordmark_1xurb_20{padding:var(--spacing-3) var(--spacing-2_5) var(--spacing-5)}._switcher_1xurb_26{padding-bottom:var(--spacing-4)}._foot_1xurb_33{display:flex;flex-direction:column;gap:var(--spacing-4);margin-block-start:auto;padding-block-start:var(--spacing-4)}._nav_1xurb_45{display:flex;flex-direction:column;gap:var(--spacing-4)}._main_1xurb_54{display:flex;min-width:0;flex:1;flex-direction:column}._content_1xurb_64{flex:1;min-height:0;overflow-y:auto;background:var(--color-surface)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boost33/ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "The app shell every Boost33 product renders inside.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -31,11 +31,13 @@
31
31
  "test": "vitest run",
32
32
  "lint": "eslint .",
33
33
  "build": "vite build",
34
+ "prepublishOnly": "npm run typecheck && npm run test && npm run lint && npm run build",
34
35
  "tokens": "npx tsx scripts/generate-tokens.mts",
35
36
  "specimen:dev": "vite --config vite.specimen.config.mts",
36
37
  "specimen:build": "vite build --config vite.specimen.config.mts",
37
38
  "specimen:preview": "vite preview --config vite.specimen.config.mts",
38
39
  "screenshot-diff": "npm run specimen:build && npx tsx scripts/screenshot-diff.mts",
40
+ "sidebar-overflow-check": "npm run specimen:build && npx tsx scripts/sidebar-overflow-check.mts",
39
41
  "afk": "npx tsx .sandcastle/main.mts"
40
42
  },
41
43
  "peerDependencies": {
@@ -45,6 +47,9 @@
45
47
  "devDependencies": {
46
48
  "@ai-hero/sandcastle": "^0.12.0",
47
49
  "@eslint/js": "^10.0.1",
50
+ "@fontsource-variable/geist-mono": "^5.3.0",
51
+ "@fontsource-variable/inter": "^5.3.0",
52
+ "@fontsource-variable/outfit": "^5.3.0",
48
53
  "@playwright/test": "^1.63.0",
49
54
  "@testing-library/jest-dom": "^6.6.3",
50
55
  "@testing-library/react": "^16.1.0",