@evinvest/uikit 0.23.0 → 0.25.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.
Files changed (36) hide show
  1. package/README.md +151 -4
  2. package/dist/components/app-shell.js +85 -0
  3. package/dist/components/app-shell.js.map +1 -0
  4. package/dist/components/bottom-tab-bar.js +91 -0
  5. package/dist/components/bottom-tab-bar.js.map +1 -0
  6. package/dist/components/mobile-app-bar.js +51 -0
  7. package/dist/components/mobile-app-bar.js.map +1 -0
  8. package/dist/components/nav-badge.js +61 -0
  9. package/dist/components/nav-badge.js.map +1 -0
  10. package/dist/components/nav-link.js +50 -0
  11. package/dist/components/nav-link.js.map +1 -0
  12. package/dist/components/nav-model.js +55 -0
  13. package/dist/components/nav-model.js.map +1 -0
  14. package/dist/components/page-frame.js +92 -0
  15. package/dist/components/page-frame.js.map +1 -0
  16. package/dist/components/resource-error.js +47 -0
  17. package/dist/components/resource-error.js.map +1 -0
  18. package/dist/components/section-nav.js +86 -0
  19. package/dist/components/section-nav.js.map +1 -0
  20. package/dist/components/settled.js +19 -0
  21. package/dist/components/settled.js.map +1 -0
  22. package/dist/components/shell-icons.js +57 -0
  23. package/dist/components/shell-icons.js.map +1 -0
  24. package/dist/components/shell-nav.js +141 -0
  25. package/dist/components/shell-nav.js.map +1 -0
  26. package/dist/components/system-banner.js +53 -0
  27. package/dist/components/system-banner.js.map +1 -0
  28. package/dist/components/use-shell-nav.js +46 -0
  29. package/dist/components/use-shell-nav.js.map +1 -0
  30. package/dist/generated/button.js +4 -2
  31. package/dist/generated/button.js.map +1 -1
  32. package/dist/index.d.ts +421 -1
  33. package/dist/index.js +12 -0
  34. package/package.json +1 -1
  35. package/styles/theme.css +155 -1
  36. package/styles/tokens.css +157 -2
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/nav-model.ts"],"sourcesContent":["import type * as React from \"react\";\n\n/**\n * How a {@link NavItem} decides it is the current one.\n *\n * - `\"prefix\"` (default) — the path is the item's own or lies beneath it, on a\n * segment boundary: `/invest` claims `/invest/arb` but not `/investors`. An\n * item at `/` is exact, or it would claim every page.\n * - `\"exact\"` — the path is the item's own and nothing beneath it.\n * - a function — whatever it answers for the path.\n */\nexport type NavMatch = \"exact\" | \"prefix\" | ((pathname: string) => boolean);\n\n/** State a row is rendered in; what `trailing` and `renderItem` are told. */\nexport interface NavItemState {\n active: boolean;\n /** Marked by a click whose navigation has not landed yet. */\n pending: boolean;\n}\n\n/** One destination, shared by the rail ({@link ShellNav}) and the tab bar. */\nexport interface NavItem {\n /** Stable key; also what `onNavigate` / `onItemIntent` hand back. */\n id: string;\n href: string;\n label: React.ReactNode;\n icon?: React.ComponentType<{ className?: string }>;\n /** A count (`NavBadge`, capped at 99+, `0` draws nothing) or your own node. */\n badge?: number | React.ReactNode;\n /** Anything after the label — a status chip; a function is told the row's state. */\n trailing?: React.ReactNode | ((state: NavItemState) => React.ReactNode);\n /**\n * Leaves the app: rendered as a plain `<a>` (never `linkComponent`), never\n * current, never marked pending — a `mailto:`, a docs site.\n */\n external?: boolean;\n /** Passed to the anchor; `_blank` gets `rel=\"noopener noreferrer\"`. */\n target?: React.HTMLAttributeAnchorTarget;\n match?: NavMatch;\n /**\n * More paths this item claims, prefix-matched — the one tab standing in for\n * a whole group of rail rows (`[\"/profile\", \"/notifications\"]`).\n */\n also?: readonly string[];\n /** Rendered, not focusable, never current. */\n disabled?: boolean;\n}\n\n/** A titled run of items; the rail draws one marker per group. */\nexport interface NavGroup {\n id: string;\n /** The eyebrow above the rows; also the list's accessible name. */\n label?: React.ReactNode;\n items: readonly NavItem[];\n}\n\nfunction pathOf(href: string): string {\n const cut = href.search(/[?#]/);\n const path = cut === -1 ? href : href.slice(0, cut);\n return path.length > 1 && path.endsWith(\"/\") ? path.slice(0, -1) : path;\n}\n\nfunction underPrefix(pathname: string, prefix: string): boolean {\n if (prefix === \"/\") return pathname === \"/\";\n return pathname === prefix || pathname.startsWith(`${prefix}/`);\n}\n\n// An exact hit outranks any prefix; among prefixes the longest wins. A function\n// match ranks as a prefix hit on the item's own href.\nconst EXACT = Number.MAX_SAFE_INTEGER;\n\n/**\n * How strongly `item` claims `pathname`, or `null` when it does not. Used to\n * pick ONE current item when several match — the rail and the tab bar each\n * draw a single marker, so two current rows is not a state they can show.\n */\nexport function navItemMatch(item: NavItem, pathname: string): number | null {\n if (item.external || item.disabled) return null;\n const path = pathOf(pathname);\n const own = pathOf(item.href);\n const match = item.match ?? \"prefix\";\n let score: number | null = null;\n if (typeof match === \"function\") score = match(path) ? own.length : null;\n else if (path === own) score = EXACT;\n else if (match === \"prefix\" && underPrefix(path, own)) score = own.length;\n for (const extra of item.also ?? []) {\n const p = pathOf(extra);\n if (underPrefix(path, p) && (score === null || p.length > score)) score = p.length;\n }\n return score;\n}\n\n/** Whether `item` claims `pathname` at all, regardless of its neighbours. */\nexport function isNavItemActive(item: NavItem, pathname: string): boolean {\n return navItemMatch(item, pathname) !== null;\n}\n\n/** The single current item for `pathname`: the strongest claim, ties to the first. */\nexport function resolveActiveNavItem(\n items: Iterable<NavItem>,\n pathname: string,\n): NavItem | undefined {\n let best: NavItem | undefined;\n let bestScore = -1;\n for (const item of items) {\n const score = navItemMatch(item, pathname);\n if (score !== null && score > bestScore) {\n best = item;\n bestScore = score;\n }\n }\n return best;\n}\n\n/**\n * A click the app should handle in place. A modified or non-primary click opens\n * the page elsewhere (a tab, a window, the context menu) and the router lets the\n * browser have it, so the current row stays current; `defaultPrevented` is the\n * same courtesy to anything upstream that claimed the click.\n */\nexport function isPlainLeftClick(e: React.MouseEvent): boolean {\n return !(e.defaultPrevented || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0);\n}\n\n/** Props the rail and the tab bar hand a `linkComponent`; `next/link` takes all of them. */\nexport function anchorTargetProps(\n item: Pick<NavItem, \"target\">,\n): { target?: React.HTMLAttributeAnchorTarget; rel?: string } {\n if (item.target === undefined) return {};\n return item.target === \"_blank\"\n ? { target: item.target, rel: \"noopener noreferrer\" }\n : { target: item.target };\n}\n"],"mappings":"AAwDA,SAAS,OAAO,MAAsB;AACpC,QAAM,MAAM,KAAK,OAAO,MAAM;AAC9B,QAAM,OAAO,QAAQ,KAAK,OAAO,KAAK,MAAM,GAAG,GAAG;AAClD,SAAO,KAAK,SAAS,KAAK,KAAK,SAAS,GAAG,IAAI,KAAK,MAAM,GAAG,EAAE,IAAI;AACrE;AAEA,SAAS,YAAY,UAAkB,QAAyB;AAC9D,MAAI,WAAW,IAAK,QAAO,aAAa;AACxC,SAAO,aAAa,UAAU,SAAS,WAAW,GAAG,MAAM,GAAG;AAChE;AAIA,MAAM,QAAQ,OAAO;AAOd,SAAS,aAAa,MAAe,UAAiC;AAC3E,MAAI,KAAK,YAAY,KAAK,SAAU,QAAO;AAC3C,QAAM,OAAO,OAAO,QAAQ;AAC5B,QAAM,MAAM,OAAO,KAAK,IAAI;AAC5B,QAAM,QAAQ,KAAK,SAAS;AAC5B,MAAI,QAAuB;AAC3B,MAAI,OAAO,UAAU,WAAY,SAAQ,MAAM,IAAI,IAAI,IAAI,SAAS;AAAA,WAC3D,SAAS,IAAK,SAAQ;AAAA,WACtB,UAAU,YAAY,YAAY,MAAM,GAAG,EAAG,SAAQ,IAAI;AACnE,aAAW,SAAS,KAAK,QAAQ,CAAC,GAAG;AACnC,UAAM,IAAI,OAAO,KAAK;AACtB,QAAI,YAAY,MAAM,CAAC,MAAM,UAAU,QAAQ,EAAE,SAAS,OAAQ,SAAQ,EAAE;AAAA,EAC9E;AACA,SAAO;AACT;AAGO,SAAS,gBAAgB,MAAe,UAA2B;AACxE,SAAO,aAAa,MAAM,QAAQ,MAAM;AAC1C;AAGO,SAAS,qBACd,OACA,UACqB;AACrB,MAAI;AACJ,MAAI,YAAY;AAChB,aAAW,QAAQ,OAAO;AACxB,UAAM,QAAQ,aAAa,MAAM,QAAQ;AACzC,QAAI,UAAU,QAAQ,QAAQ,WAAW;AACvC,aAAO;AACP,kBAAY;AAAA,IACd;AAAA,EACF;AACA,SAAO;AACT;AAQO,SAAS,iBAAiB,GAA8B;AAC7D,SAAO,EAAE,EAAE,oBAAoB,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW;AAClG;AAGO,SAAS,kBACd,MAC4D;AAC5D,MAAI,KAAK,WAAW,OAAW,QAAO,CAAC;AACvC,SAAO,KAAK,WAAW,WACnB,EAAE,QAAQ,KAAK,QAAQ,KAAK,sBAAsB,IAClD,EAAE,QAAQ,KAAK,OAAO;AAC5B;","names":[]}
@@ -0,0 +1,92 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { cn } from "../lib/cn.js";
4
+ function SectionLabel({ as: Comp = "p", tone = "muted", className, ...props }) {
5
+ return /* @__PURE__ */ jsx(
6
+ Comp,
7
+ {
8
+ "data-slot": "section-label",
9
+ className: cn(
10
+ "text-xs font-semibold uppercase tracking-widest",
11
+ tone === "accent" ? "text-primary-ink" : "text-ink-soft",
12
+ className
13
+ ),
14
+ ...props
15
+ }
16
+ );
17
+ }
18
+ function PageHeading({ title, description, eyebrow, actions, className, ...props }) {
19
+ return /* @__PURE__ */ jsxs(
20
+ "header",
21
+ {
22
+ "data-slot": "page-heading",
23
+ className: cn("flex items-center justify-between gap-4", className),
24
+ ...props,
25
+ children: [
26
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [
27
+ eyebrow !== void 0 && /* @__PURE__ */ jsx(SectionLabel, { tone: "accent", children: eyebrow }),
28
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold leading-tight text-ink", children: title }),
29
+ description !== void 0 && /* @__PURE__ */ jsx("p", { className: "text-sm text-ink-soft", children: description })
30
+ ] }),
31
+ actions !== void 0 && /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-2.5", children: actions })
32
+ ]
33
+ }
34
+ );
35
+ }
36
+ const HEADING_FROM = { md: "hidden md:flex", lg: "hidden lg:flex" };
37
+ const PAD = {
38
+ md: "gap-4 px-4 pb-6 pt-5 md:gap-6 md:px-8 md:pb-7 md:pt-6",
39
+ lg: "gap-4 px-4 pb-6 pt-5 lg:gap-6 lg:px-8 lg:pb-7 lg:pt-6"
40
+ };
41
+ function PageFrame({
42
+ title,
43
+ description,
44
+ eyebrow,
45
+ actions,
46
+ appBar,
47
+ breakpoint = "lg",
48
+ width = "full",
49
+ headingClassName,
50
+ className,
51
+ style,
52
+ children,
53
+ ...props
54
+ }) {
55
+ const hasBar = appBar !== void 0 && appBar !== null && appBar !== false;
56
+ const enter = {
57
+ "--enter-step": "var(--ev-stagger-section, 50ms)",
58
+ ...hasBar ? { "--enter-delay": "var(--ev-stagger-section, 50ms)" } : {}
59
+ };
60
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
61
+ appBar,
62
+ /* @__PURE__ */ jsxs(
63
+ "div",
64
+ {
65
+ "data-slot": "page-frame",
66
+ "data-enter": "stagger",
67
+ className: cn("flex flex-col", PAD[breakpoint], width === "content" && "max-w-5xl", className),
68
+ style: { ...enter, ...style },
69
+ ...props,
70
+ children: [
71
+ title !== void 0 && /* @__PURE__ */ jsx(
72
+ PageHeading,
73
+ {
74
+ title,
75
+ description,
76
+ eyebrow,
77
+ actions,
78
+ className: cn(hasBar && HEADING_FROM[breakpoint], headingClassName)
79
+ }
80
+ ),
81
+ children
82
+ ]
83
+ }
84
+ )
85
+ ] });
86
+ }
87
+ export {
88
+ PageFrame,
89
+ PageHeading,
90
+ SectionLabel
91
+ };
92
+ //# sourceMappingURL=page-frame.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/page-frame.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../lib/cn\";\nimport type { ShellBreakpoint } from \"./app-shell\";\n\nexport type SectionLabelTone = \"muted\" | \"accent\";\n\nexport interface SectionLabelProps extends React.HTMLAttributes<HTMLElement> {\n as?: React.ElementType;\n /** `muted` labels a group or a section; `accent` announces a hero figure. */\n tone?: SectionLabelTone;\n}\n\n/** The tracked-uppercase label above a figure, a group or a section. */\nexport function SectionLabel({ as: Comp = \"p\", tone = \"muted\", className, ...props }: SectionLabelProps) {\n return (\n <Comp\n data-slot=\"section-label\"\n className={cn(\n \"text-xs font-semibold uppercase tracking-widest\",\n tone === \"accent\" ? \"text-primary-ink\" : \"text-ink-soft\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport interface PageHeadingProps extends Omit<React.ComponentProps<\"header\">, \"title\"> {\n title: React.ReactNode;\n /** What the screen is for, in a sentence. */\n description?: React.ReactNode;\n eyebrow?: React.ReactNode;\n /** The screen's own controls, on the right. */\n actions?: React.ReactNode;\n}\n\n/** The title row: eyebrow → title → description on the left, actions on the right. */\nexport function PageHeading({ title, description, eyebrow, actions, className, ...props }: PageHeadingProps) {\n return (\n <header\n data-slot=\"page-heading\"\n className={cn(\"flex items-center justify-between gap-4\", className)}\n {...props}\n >\n <div className=\"flex min-w-0 flex-col gap-1\">\n {eyebrow !== undefined && <SectionLabel tone=\"accent\">{eyebrow}</SectionLabel>}\n <h1 className=\"text-2xl font-semibold leading-tight text-ink\">{title}</h1>\n {description !== undefined && <p className=\"text-sm text-ink-soft\">{description}</p>}\n </div>\n {actions !== undefined && <div className=\"flex shrink-0 items-center gap-2.5\">{actions}</div>}\n </header>\n );\n}\n\nexport type PageFrameWidth = \"full\" | \"content\";\n\nexport interface PageFrameProps extends Omit<React.ComponentProps<\"div\">, \"title\"> {\n /** Omit when the screen's heading is its own composition. */\n title?: React.ReactNode;\n description?: React.ReactNode;\n eyebrow?: React.ReactNode;\n actions?: React.ReactNode;\n /**\n * The bar titling the screen on a phone (`MobileAppBar`). With one, the heading\n * shows from `breakpoint` up only and the sections arrive a step after the bar.\n */\n appBar?: React.ReactNode;\n /** Default `lg`. */\n breakpoint?: ShellBreakpoint;\n /** `full` fills the column (the rail already bounds it); `content` caps it at a reading measure. */\n width?: PageFrameWidth;\n headingClassName?: string;\n}\n\nconst HEADING_FROM = { md: \"hidden md:flex\", lg: \"hidden lg:flex\" } as const;\nconst PAD = {\n md: \"gap-4 px-4 pb-6 pt-5 md:gap-6 md:px-8 md:pb-7 md:pt-6\",\n lg: \"gap-4 px-4 pb-6 pt-5 lg:gap-6 lg:px-8 lg:pb-7 lg:pt-6\",\n} as const;\n\n/**\n * The frame every screen sits in: one inset, one section rhythm, one title scale,\n * so moving between screens never reads as moving between products.\n *\n * Its sections arrive in sequence by CSS alone (`data-enter=\"stagger\"`, see\n * motion.css): every direct child fades and rises `--ev-rise`, one\n * `--ev-stagger-section` after the previous, the heading first. Server-safe —\n * no motion library, no hydration needed for the entrance.\n */\nexport function PageFrame({\n title,\n description,\n eyebrow,\n actions,\n appBar,\n breakpoint = \"lg\",\n width = \"full\",\n headingClassName,\n className,\n style,\n children,\n ...props\n}: PageFrameProps) {\n const hasBar = appBar !== undefined && appBar !== null && appBar !== false;\n const enter = {\n \"--enter-step\": \"var(--ev-stagger-section, 50ms)\",\n ...(hasBar ? { \"--enter-delay\": \"var(--ev-stagger-section, 50ms)\" } : {}),\n };\n return (\n <>\n {appBar}\n <div\n data-slot=\"page-frame\"\n data-enter=\"stagger\"\n className={cn(\"flex flex-col\", PAD[breakpoint], width === \"content\" && \"max-w-5xl\", className)}\n style={{ ...enter, ...style }}\n {...props}\n >\n {title !== undefined && (\n <PageHeading\n title={title}\n description={description}\n eyebrow={eyebrow}\n actions={actions}\n className={cn(hasBar && HEADING_FROM[breakpoint], headingClassName)}\n />\n )}\n {children}\n </div>\n </>\n );\n}\n"],"mappings":"AAeI,SA8FA,UA9FA,KA6BE,YA7BF;AAfJ,YAAY,WAAW;AACvB,SAAS,UAAU;AAYZ,SAAS,aAAa,EAAE,IAAI,OAAO,KAAK,OAAO,SAAS,WAAW,GAAG,MAAM,GAAsB;AACvG,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA,SAAS,WAAW,qBAAqB;AAAA,QACzC;AAAA,MACF;AAAA,MACC,GAAG;AAAA;AAAA,EACN;AAEJ;AAYO,SAAS,YAAY,EAAE,OAAO,aAAa,SAAS,SAAS,WAAW,GAAG,MAAM,GAAqB;AAC3G,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,2CAA2C,SAAS;AAAA,MACjE,GAAG;AAAA,MAEJ;AAAA,6BAAC,SAAI,WAAU,+BACZ;AAAA,sBAAY,UAAa,oBAAC,gBAAa,MAAK,UAAU,mBAAQ;AAAA,UAC/D,oBAAC,QAAG,WAAU,iDAAiD,iBAAM;AAAA,UACpE,gBAAgB,UAAa,oBAAC,OAAE,WAAU,yBAAyB,uBAAY;AAAA,WAClF;AAAA,QACC,YAAY,UAAa,oBAAC,SAAI,WAAU,sCAAsC,mBAAQ;AAAA;AAAA;AAAA,EACzF;AAEJ;AAsBA,MAAM,eAAe,EAAE,IAAI,kBAAkB,IAAI,iBAAiB;AAClE,MAAM,MAAM;AAAA,EACV,IAAI;AAAA,EACJ,IAAI;AACN;AAWO,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAmB;AACjB,QAAM,SAAS,WAAW,UAAa,WAAW,QAAQ,WAAW;AACrE,QAAM,QAAQ;AAAA,IACZ,gBAAgB;AAAA,IAChB,GAAI,SAAS,EAAE,iBAAiB,kCAAkC,IAAI,CAAC;AAAA,EACzE;AACA,SACE,iCACG;AAAA;AAAA,IACD;AAAA,MAAC;AAAA;AAAA,QACC,aAAU;AAAA,QACV,cAAW;AAAA,QACX,WAAW,GAAG,iBAAiB,IAAI,UAAU,GAAG,UAAU,aAAa,aAAa,SAAS;AAAA,QAC7F,OAAO,EAAE,GAAG,OAAO,GAAG,MAAM;AAAA,QAC3B,GAAG;AAAA,QAEH;AAAA,oBAAU,UACT;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA,WAAW,GAAG,UAAU,aAAa,UAAU,GAAG,gBAAgB;AAAA;AAAA,UACpE;AAAA,UAED;AAAA;AAAA;AAAA,IACH;AAAA,KACF;AAEJ;","names":[]}
@@ -0,0 +1,47 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { Alert, AlertDescription, AlertTitle } from "./alert.js";
6
+ import { Button } from "./button.js";
7
+ import { RefreshIcon, TriangleAlertIcon } from "./shell-icons.js";
8
+ function ResourceError(props) {
9
+ if (props.variant === "alert") {
10
+ const { message: message2, title, labels: _labels, variant: _variant2, className: className2, ...rest2 } = props;
11
+ return /* @__PURE__ */ jsx("div", { "data-slot": "resource-error", "data-variant": "alert", className: className2, ...rest2, children: /* @__PURE__ */ jsxs(Alert, { variant: "destructive", children: [
12
+ /* @__PURE__ */ jsx(TriangleAlertIcon, { className: "size-4" }),
13
+ title !== void 0 && /* @__PURE__ */ jsx(AlertTitle, { children: title }),
14
+ /* @__PURE__ */ jsx(AlertDescription, { children: message2 })
15
+ ] }) });
16
+ }
17
+ const { message, onRetry, retrying = false, labels = {}, variant: _variant, title: _title, className, ...rest } = props;
18
+ const line = /* @__PURE__ */ jsxs("p", { className: "flex min-w-0 items-center gap-2 text-sm text-accent-error", children: [
19
+ /* @__PURE__ */ jsx(TriangleAlertIcon, { className: "size-4 shrink-0" }),
20
+ " ",
21
+ message
22
+ ] });
23
+ return /* @__PURE__ */ jsxs(
24
+ "div",
25
+ {
26
+ "data-slot": "resource-error",
27
+ "data-variant": "inline",
28
+ role: "alert",
29
+ className: cn(
30
+ onRetry && "flex flex-wrap items-center gap-3 rounded-lg border border-accent-error/40 bg-accent-error/10 px-3 py-2.5",
31
+ className
32
+ ),
33
+ ...rest,
34
+ children: [
35
+ line,
36
+ onRetry && /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: retrying, onClick: onRetry, children: [
37
+ /* @__PURE__ */ jsx(RefreshIcon, { className: cn("size-4", retrying && "motion-safe:animate-spin") }),
38
+ labels.retry ?? "Try again"
39
+ ] })
40
+ ]
41
+ }
42
+ );
43
+ }
44
+ export {
45
+ ResourceError
46
+ };
47
+ //# sourceMappingURL=resource-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/resource-error.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { Alert, AlertDescription, AlertTitle } from \"./alert\";\nimport { Button } from \"./button\";\nimport { RefreshIcon, TriangleAlertIcon } from \"./shell-icons\";\n\nexport interface ResourceErrorLabels {\n /** The retry button. Default `\"Try again\"`. */\n retry?: string;\n}\n\ninterface ResourceErrorBase extends Omit<React.ComponentProps<\"div\">, \"title\" | \"children\"> {\n /** What failed, already in the reader's words — the kit formats no errors. */\n message: React.ReactNode;\n labels?: ResourceErrorLabels;\n}\n\n// The two forms take different props and the union says so: an `alert` has no\n// retry and an `inline` line has no heading, so asking for either is a type\n// error rather than a prop that silently does nothing.\nexport type ResourceErrorProps = ResourceErrorBase &\n (\n | {\n /** One line under the page header — for dense screens where a block would push everything down. */\n variant?: \"inline\";\n title?: never;\n /** Re-run the read; boxes the line so the button and the message read as one thing. */\n onRetry?: () => void;\n /** The retry is in flight: the button is disabled and its icon spins. */\n retrying?: boolean;\n }\n | {\n /** The kit's `Alert` with a heading — for screens that address the reader. */\n variant: \"alert\";\n title?: React.ReactNode;\n onRetry?: never;\n retrying?: never;\n }\n );\n\n/**\n * What a screen shows when a read it needed never arrived. Whether there is\n * anything to report stays with the caller — only it knows whether stale data is\n * still on screen, and a failed refresh over figures the reader can see belongs\n * beside them, not in place of them.\n */\nexport function ResourceError(props: ResourceErrorProps) {\n if (props.variant === \"alert\") {\n const { message, title, labels: _labels, variant: _variant, className, ...rest } = props;\n return (\n <div data-slot=\"resource-error\" data-variant=\"alert\" className={className} {...rest}>\n <Alert variant=\"destructive\">\n <TriangleAlertIcon className=\"size-4\" />\n {title !== undefined && <AlertTitle>{title}</AlertTitle>}\n <AlertDescription>{message}</AlertDescription>\n </Alert>\n </div>\n );\n }\n const { message, onRetry, retrying = false, labels = {}, variant: _variant, title: _title, className, ...rest } = props;\n const line = (\n <p className=\"flex min-w-0 items-center gap-2 text-sm text-accent-error\">\n {/* Holds its size: beside the button it is the first thing squeezed, and a\n half-width triangle reads as a rendering fault. */}\n <TriangleAlertIcon className=\"size-4 shrink-0\" /> {message}\n </p>\n );\n return (\n <div\n data-slot=\"resource-error\"\n data-variant=\"inline\"\n role=\"alert\"\n className={cn(\n onRetry &&\n \"flex flex-wrap items-center gap-3 rounded-lg border border-accent-error/40 bg-accent-error/10 px-3 py-2.5\",\n className,\n )}\n {...rest}\n >\n {line}\n {onRetry && (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={retrying} onClick={onRetry}>\n <RefreshIcon className={cn(\"size-4\", retrying && \"motion-safe:animate-spin\")} />\n {labels.retry ?? \"Try again\"}\n </Button>\n )}\n </div>\n );\n}\n"],"mappings":";AAqDQ,SACE,KADF;AAnDR,YAAY,WAAW;AACvB,SAAS,UAAU;AACnB,SAAS,OAAO,kBAAkB,kBAAkB;AACpD,SAAS,cAAc;AACvB,SAAS,aAAa,yBAAyB;AA0CxC,SAAS,cAAc,OAA2B;AACvD,MAAI,MAAM,YAAY,SAAS;AAC7B,UAAM,EAAE,SAAAA,UAAS,OAAO,QAAQ,SAAS,SAASC,WAAU,WAAAC,YAAW,GAAGC,MAAK,IAAI;AACnF,WACE,oBAAC,SAAI,aAAU,kBAAiB,gBAAa,SAAQ,WAAWD,YAAY,GAAGC,OAC7E,+BAAC,SAAM,SAAQ,eACb;AAAA,0BAAC,qBAAkB,WAAU,UAAS;AAAA,MACrC,UAAU,UAAa,oBAAC,cAAY,iBAAM;AAAA,MAC3C,oBAAC,oBAAkB,UAAAH,UAAQ;AAAA,OAC7B,GACF;AAAA,EAEJ;AACA,QAAM,EAAE,SAAS,SAAS,WAAW,OAAO,SAAS,CAAC,GAAG,SAAS,UAAU,OAAO,QAAQ,WAAW,GAAG,KAAK,IAAI;AAClH,QAAM,OACJ,qBAAC,OAAE,WAAU,6DAGX;AAAA,wBAAC,qBAAkB,WAAU,mBAAkB;AAAA,IAAE;AAAA,IAAE;AAAA,KACrD;AAEF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,gBAAa;AAAA,MACb,MAAK;AAAA,MACL,WAAW;AAAA,QACT,WACE;AAAA,QACF;AAAA,MACF;AAAA,MACC,GAAG;AAAA,MAEH;AAAA;AAAA,QACA,WACC,qBAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,UAAU,UAAU,SAAS,SAC7E;AAAA,8BAAC,eAAY,WAAW,GAAG,UAAU,YAAY,0BAA0B,GAAG;AAAA,UAC7E,OAAO,SAAS;AAAA,WACnB;AAAA;AAAA;AAAA,EAEJ;AAEJ;","names":["message","_variant","className","rest"]}
@@ -0,0 +1,86 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { SectionLabel } from "./page-frame.js";
6
+ const ITEM = "flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50";
7
+ function SectionNav({
8
+ groups,
9
+ value,
10
+ onValueChange,
11
+ linkComponent,
12
+ labels = {},
13
+ className,
14
+ ...props
15
+ }) {
16
+ return /* @__PURE__ */ jsx(
17
+ "nav",
18
+ {
19
+ "data-slot": "section-nav",
20
+ "aria-label": labels.nav ?? "Sections",
21
+ className: cn("flex w-60 shrink-0 flex-col gap-5", className),
22
+ ...props,
23
+ children: groups.map((group) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
24
+ (group.label !== void 0 || group.description !== void 0) && /* @__PURE__ */ jsxs("div", { className: "mb-1 flex flex-col gap-0.5 px-3", children: [
25
+ group.label !== void 0 && /* @__PURE__ */ jsx(SectionLabel, { children: group.label }),
26
+ group.description !== void 0 && /* @__PURE__ */ jsx("p", { className: "text-xs leading-snug text-ink-soft", children: group.description })
27
+ ] }),
28
+ group.items.map((item) => /* @__PURE__ */ jsx(
29
+ SectionNavRow,
30
+ {
31
+ item,
32
+ active: item.id === value,
33
+ onSelect: onValueChange,
34
+ linkComponent
35
+ },
36
+ item.id
37
+ ))
38
+ ] }, group.id))
39
+ }
40
+ );
41
+ }
42
+ function SectionNavRow({
43
+ item,
44
+ active,
45
+ onSelect,
46
+ linkComponent
47
+ }) {
48
+ const Icon = item.icon;
49
+ const className = cn(
50
+ ITEM,
51
+ active ? "bg-primary-ink/15 font-semibold text-primary-ink" : "text-ink hover:bg-hover"
52
+ );
53
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
54
+ Icon && /* @__PURE__ */ jsx(Icon, { className: "size-4.5 shrink-0" }),
55
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label }),
56
+ item.trailing
57
+ ] });
58
+ if (item.href !== void 0 && !item.disabled) {
59
+ const Link = linkComponent ?? "a";
60
+ return /* @__PURE__ */ jsx(
61
+ Link,
62
+ {
63
+ href: item.href,
64
+ "aria-current": active ? "page" : void 0,
65
+ className,
66
+ onClick: () => onSelect?.(item.id),
67
+ children: content
68
+ }
69
+ );
70
+ }
71
+ return /* @__PURE__ */ jsx(
72
+ "button",
73
+ {
74
+ type: "button",
75
+ disabled: item.disabled,
76
+ "aria-current": active ? "true" : void 0,
77
+ className,
78
+ onClick: () => onSelect?.(item.id),
79
+ children: content
80
+ }
81
+ );
82
+ }
83
+ export {
84
+ SectionNav
85
+ };
86
+ //# sourceMappingURL=section-nav.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/section-nav.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { SectionLabel } from \"./page-frame\";\n\nexport interface SectionNavItem {\n id: string;\n label: React.ReactNode;\n icon?: React.ComponentType<{ className?: string }>;\n /** A link (`?section=…`, a sub-route) instead of a button calling `onValueChange`. */\n href?: string;\n /** After the label — a count, a status chip. */\n trailing?: React.ReactNode;\n disabled?: boolean;\n}\n\nexport interface SectionNavGroup {\n id: string;\n label?: React.ReactNode;\n /** One line under the label saying what kind of thing lives here. */\n description?: React.ReactNode;\n items: readonly SectionNavItem[];\n}\n\nexport interface SectionNavLabels {\n /** Accessible name of the nav. Default `\"Sections\"`. */\n nav?: string;\n}\n\nexport interface SectionNavProps extends Omit<React.ComponentProps<\"nav\">, \"children\"> {\n groups: readonly SectionNavGroup[];\n /** The current section's id. */\n value: string;\n /** Called by button items (those without `href`), and by link items on click. */\n onValueChange?: (id: string) => void;\n linkComponent?: React.ElementType;\n labels?: SectionNavLabels;\n}\n\nconst ITEM =\n \"flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50\";\n\n/**\n * A screen's own section rail — settings panes, a document's chapters: grouped\n * rows, one current (`aria-current`), each a button or a link. The shell's rail\n * is `ShellNav`; this one lives inside a page, so it carries a tint rather than\n * the rail's solid marker and does not slide.\n */\nexport function SectionNav({\n groups,\n value,\n onValueChange,\n linkComponent,\n labels = {},\n className,\n ...props\n}: SectionNavProps) {\n return (\n <nav\n data-slot=\"section-nav\"\n aria-label={labels.nav ?? \"Sections\"}\n className={cn(\"flex w-60 shrink-0 flex-col gap-5\", className)}\n {...props}\n >\n {groups.map((group) => (\n <div key={group.id} className=\"flex flex-col gap-1\">\n {(group.label !== undefined || group.description !== undefined) && (\n <div className=\"mb-1 flex flex-col gap-0.5 px-3\">\n {group.label !== undefined && <SectionLabel>{group.label}</SectionLabel>}\n {group.description !== undefined && (\n <p className=\"text-xs leading-snug text-ink-soft\">{group.description}</p>\n )}\n </div>\n )}\n {group.items.map((item) => (\n <SectionNavRow\n key={item.id}\n item={item}\n active={item.id === value}\n onSelect={onValueChange}\n linkComponent={linkComponent}\n />\n ))}\n </div>\n ))}\n </nav>\n );\n}\n\nfunction SectionNavRow({\n item,\n active,\n onSelect,\n linkComponent,\n}: {\n item: SectionNavItem;\n active: boolean;\n onSelect: ((id: string) => void) | undefined;\n linkComponent: React.ElementType | undefined;\n}) {\n const Icon = item.icon;\n const className = cn(\n ITEM,\n active ? \"bg-primary-ink/15 font-semibold text-primary-ink\" : \"text-ink hover:bg-hover\",\n );\n const content = (\n <>\n {Icon && <Icon className=\"size-4.5 shrink-0\" />}\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n {item.trailing}\n </>\n );\n if (item.href !== undefined && !item.disabled) {\n const Link = linkComponent ?? \"a\";\n return (\n <Link\n href={item.href}\n aria-current={active ? \"page\" : undefined}\n className={className}\n onClick={() => onSelect?.(item.id)}\n >\n {content}\n </Link>\n );\n }\n return (\n <button\n type=\"button\"\n disabled={item.disabled}\n aria-current={active ? \"true\" : undefined}\n className={className}\n onClick={() => onSelect?.(item.id)}\n >\n {content}\n </button>\n );\n}\n"],"mappings":";AAoEY,SAuCR,UAtCwC,KADhC;AAlEZ,YAAY,WAAW;AACvB,SAAS,UAAU;AACnB,SAAS,oBAAoB;AAoC7B,MAAM,OACJ;AAQK,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS,CAAC;AAAA,EACV;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,cAAY,OAAO,OAAO;AAAA,MAC1B,WAAW,GAAG,qCAAqC,SAAS;AAAA,MAC3D,GAAG;AAAA,MAEH,iBAAO,IAAI,CAAC,UACX,qBAAC,SAAmB,WAAU,uBAC1B;AAAA,eAAM,UAAU,UAAa,MAAM,gBAAgB,WACnD,qBAAC,SAAI,WAAU,mCACZ;AAAA,gBAAM,UAAU,UAAa,oBAAC,gBAAc,gBAAM,OAAM;AAAA,UACxD,MAAM,gBAAgB,UACrB,oBAAC,OAAE,WAAU,sCAAsC,gBAAM,aAAY;AAAA,WAEzE;AAAA,QAED,MAAM,MAAM,IAAI,CAAC,SAChB;AAAA,UAAC;AAAA;AAAA,YAEC;AAAA,YACA,QAAQ,KAAK,OAAO;AAAA,YACpB,UAAU;AAAA,YACV;AAAA;AAAA,UAJK,KAAK;AAAA,QAKZ,CACD;AAAA,WAjBO,MAAM,EAkBhB,CACD;AAAA;AAAA,EACH;AAEJ;AAEA,SAAS,cAAc;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,OAAO,KAAK;AAClB,QAAM,YAAY;AAAA,IAChB;AAAA,IACA,SAAS,qDAAqD;AAAA,EAChE;AACA,QAAM,UACJ,iCACG;AAAA,YAAQ,oBAAC,QAAK,WAAU,qBAAoB;AAAA,IAC7C,oBAAC,UAAK,WAAU,2BAA2B,eAAK,OAAM;AAAA,IACrD,KAAK;AAAA,KACR;AAEF,MAAI,KAAK,SAAS,UAAa,CAAC,KAAK,UAAU;AAC7C,UAAM,OAAO,iBAAiB;AAC9B,WACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAM,KAAK;AAAA,QACX,gBAAc,SAAS,SAAS;AAAA,QAChC;AAAA,QACA,SAAS,MAAM,WAAW,KAAK,EAAE;AAAA,QAEhC;AAAA;AAAA,IACH;AAAA,EAEJ;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,UAAU,KAAK;AAAA,MACf,gBAAc,SAAS,SAAS;AAAA,MAChC;AAAA,MACA,SAAS,MAAM,WAAW,KAAK,EAAE;AAAA,MAEhC;AAAA;AAAA,EACH;AAEJ;","names":[]}
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ function Settled({ loading, skeleton, children, ...props }) {
5
+ const [wasLoading, setWasLoading] = React.useState(loading);
6
+ const [reveals, setReveals] = React.useState(0);
7
+ if (wasLoading !== loading) {
8
+ setWasLoading(loading);
9
+ if (wasLoading) setReveals((n) => n + 1);
10
+ }
11
+ if (loading) {
12
+ return /* @__PURE__ */ jsx("div", { "data-slot": "settled", "data-state": "loading", "aria-busy": "true", ...props, children: skeleton });
13
+ }
14
+ return /* @__PURE__ */ jsx("div", { "data-slot": "settled", "data-state": reveals > 0 ? "revealed" : "ready", ...props, children }, reveals);
15
+ }
16
+ export {
17
+ Settled
18
+ };
19
+ //# sourceMappingURL=settled.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/settled.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\nexport interface SettledProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n /** True while the data this surface needs is in flight. */\n loading: boolean;\n /** What stands in meanwhile — a `Skeleton` or a group of them. */\n skeleton: React.ReactNode;\n /** The content; also the empty and error states, guarded on the same condition. */\n children: React.ReactNode;\n}\n\n/**\n * The skeleton → content handover. Swapped in a single frame, a skeleton reads\n * as a flicker — the eye sees that something changed, not what. Here the\n * content fades and rises the last `--ev-rise` into place\n * (`data-state=\"revealed\"`, motion.css).\n *\n * Deliberately not a cross-fade: the two would have to overlap, which means\n * taking one out of flow and animating a height, and the surface would jump\n * when the content is a different size. And no entrance when no skeleton was\n * shown — data already present on the first render cuts straight in; fading it\n * would invent a delay the data never had.\n *\n * `data-state`: `loading` → `revealed` after a skeleton, `ready` without one.\n */\nexport function Settled({ loading, skeleton, children, ...props }: SettledProps) {\n // Adjusted during render, not in an effect, so the entrance plays on the very\n // first render after `loading` drops: an effect would paint the content opaque\n // once and only then start the fade. `reveals` keys the wrapper, so a later\n // refetch that shows the skeleton again gets a handover of its own.\n const [wasLoading, setWasLoading] = React.useState(loading);\n const [reveals, setReveals] = React.useState(0);\n if (wasLoading !== loading) {\n setWasLoading(loading);\n if (wasLoading) setReveals((n) => n + 1);\n }\n if (loading) {\n return (\n <div data-slot=\"settled\" data-state=\"loading\" aria-busy=\"true\" {...props}>\n {skeleton}\n </div>\n );\n }\n return (\n <div key={reveals} data-slot=\"settled\" data-state={reveals > 0 ? \"revealed\" : \"ready\"} {...props}>\n {children}\n </div>\n );\n}\n"],"mappings":";AAwCM;AAtCN,YAAY,WAAW;AAyBhB,SAAS,QAAQ,EAAE,SAAS,UAAU,UAAU,GAAG,MAAM,GAAiB;AAK/E,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,OAAO;AAC1D,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,CAAC;AAC9C,MAAI,eAAe,SAAS;AAC1B,kBAAc,OAAO;AACrB,QAAI,WAAY,YAAW,CAAC,MAAM,IAAI,CAAC;AAAA,EACzC;AACA,MAAI,SAAS;AACX,WACE,oBAAC,SAAI,aAAU,WAAU,cAAW,WAAU,aAAU,QAAQ,GAAG,OAChE,oBACH;AAAA,EAEJ;AACA,SACE,oBAAC,SAAkB,aAAU,WAAU,cAAY,UAAU,IAAI,aAAa,SAAU,GAAG,OACxF,YADO,OAEV;AAEJ;","names":[]}
@@ -0,0 +1,57 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ function Glyph({ className, children }) {
4
+ return /* @__PURE__ */ jsx(
5
+ "svg",
6
+ {
7
+ className,
8
+ viewBox: "0 0 24 24",
9
+ fill: "none",
10
+ stroke: "currentColor",
11
+ strokeWidth: 2,
12
+ strokeLinecap: "round",
13
+ strokeLinejoin: "round",
14
+ "aria-hidden": true,
15
+ children
16
+ }
17
+ );
18
+ }
19
+ function ChevronLeftIcon({ className }) {
20
+ return /* @__PURE__ */ jsx(Glyph, { className, children: /* @__PURE__ */ jsx("path", { d: "m15 18-6-6 6-6" }) });
21
+ }
22
+ function TriangleAlertIcon({ className }) {
23
+ return /* @__PURE__ */ jsxs(Glyph, { className, children: [
24
+ /* @__PURE__ */ jsx("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" }),
25
+ /* @__PURE__ */ jsx("path", { d: "M12 9v4" }),
26
+ /* @__PURE__ */ jsx("path", { d: "M12 17h.01" })
27
+ ] });
28
+ }
29
+ function InfoIcon({ className }) {
30
+ return /* @__PURE__ */ jsxs(Glyph, { className, children: [
31
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
32
+ /* @__PURE__ */ jsx("path", { d: "M12 16v-4" }),
33
+ /* @__PURE__ */ jsx("path", { d: "M12 8h.01" })
34
+ ] });
35
+ }
36
+ function RefreshIcon({ className }) {
37
+ return /* @__PURE__ */ jsxs(Glyph, { className, children: [
38
+ /* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" }),
39
+ /* @__PURE__ */ jsx("path", { d: "M21 3v5h-5" }),
40
+ /* @__PURE__ */ jsx("path", { d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" }),
41
+ /* @__PURE__ */ jsx("path", { d: "M8 16H3v5" })
42
+ ] });
43
+ }
44
+ function CloseIcon({ className }) {
45
+ return /* @__PURE__ */ jsxs(Glyph, { className, children: [
46
+ /* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }),
47
+ /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })
48
+ ] });
49
+ }
50
+ export {
51
+ ChevronLeftIcon,
52
+ CloseIcon,
53
+ InfoIcon,
54
+ RefreshIcon,
55
+ TriangleAlertIcon
56
+ };
57
+ //# sourceMappingURL=shell-icons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/shell-icons.tsx"],"sourcesContent":["import * as React from \"react\";\n\n// lucide glyphs the shell needs, inlined so the kit keeps its zero-icon-dep footprint.\nfunction Glyph({ className, children }: { className?: string | undefined; children: React.ReactNode }) {\n return (\n <svg\n className={className}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden\n >\n {children}\n </svg>\n );\n}\n\nexport function ChevronLeftIcon({ className }: { className?: string | undefined }) {\n return (\n <Glyph className={className}>\n <path d=\"m15 18-6-6 6-6\" />\n </Glyph>\n );\n}\n\nexport function TriangleAlertIcon({ className }: { className?: string | undefined }) {\n return (\n <Glyph className={className}>\n <path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\" />\n <path d=\"M12 9v4\" />\n <path d=\"M12 17h.01\" />\n </Glyph>\n );\n}\n\nexport function InfoIcon({ className }: { className?: string | undefined }) {\n return (\n <Glyph className={className}>\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path d=\"M12 16v-4\" />\n <path d=\"M12 8h.01\" />\n </Glyph>\n );\n}\n\nexport function RefreshIcon({ className }: { className?: string | undefined }) {\n return (\n <Glyph className={className}>\n <path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\" />\n <path d=\"M21 3v5h-5\" />\n <path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\" />\n <path d=\"M8 16H3v5\" />\n </Glyph>\n );\n}\n\nexport function CloseIcon({ className }: { className?: string | undefined }) {\n return (\n <Glyph className={className}>\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </Glyph>\n );\n}\n"],"mappings":"AAKI,cAyBA,YAzBA;AALJ,YAAY,WAAW;AAGvB,SAAS,MAAM,EAAE,WAAW,SAAS,GAAkE;AACrG,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,QAAO;AAAA,MACP,aAAa;AAAA,MACb,eAAc;AAAA,MACd,gBAAe;AAAA,MACf,eAAW;AAAA,MAEV;AAAA;AAAA,EACH;AAEJ;AAEO,SAAS,gBAAgB,EAAE,UAAU,GAAuC;AACjF,SACE,oBAAC,SAAM,WACL,8BAAC,UAAK,GAAE,kBAAiB,GAC3B;AAEJ;AAEO,SAAS,kBAAkB,EAAE,UAAU,GAAuC;AACnF,SACE,qBAAC,SAAM,WACL;AAAA,wBAAC,UAAK,GAAE,4EAA2E;AAAA,IACnF,oBAAC,UAAK,GAAE,WAAU;AAAA,IAClB,oBAAC,UAAK,GAAE,cAAa;AAAA,KACvB;AAEJ;AAEO,SAAS,SAAS,EAAE,UAAU,GAAuC;AAC1E,SACE,qBAAC,SAAM,WACL;AAAA,wBAAC,YAAO,IAAG,MAAK,IAAG,MAAK,GAAE,MAAK;AAAA,IAC/B,oBAAC,UAAK,GAAE,aAAY;AAAA,IACpB,oBAAC,UAAK,GAAE,aAAY;AAAA,KACtB;AAEJ;AAEO,SAAS,YAAY,EAAE,UAAU,GAAuC;AAC7E,SACE,qBAAC,SAAM,WACL;AAAA,wBAAC,UAAK,GAAE,sDAAqD;AAAA,IAC7D,oBAAC,UAAK,GAAE,cAAa;AAAA,IACrB,oBAAC,UAAK,GAAE,uDAAsD;AAAA,IAC9D,oBAAC,UAAK,GAAE,aAAY;AAAA,KACtB;AAEJ;AAEO,SAAS,UAAU,EAAE,UAAU,GAAuC;AAC3E,SACE,qBAAC,SAAM,WACL;AAAA,wBAAC,UAAK,GAAE,cAAa;AAAA,IACrB,oBAAC,UAAK,GAAE,cAAa;AAAA,KACvB;AAEJ;","names":[]}
@@ -0,0 +1,141 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { NavBadge } from "./nav-badge.js";
6
+ import { labelTitle, NavLink } from "./nav-link.js";
7
+ import { resolveActiveNavItem } from "./nav-model.js";
8
+ import { usePendingNavigation, useSlideMarker } from "./use-shell-nav.js";
9
+ const NAV_FOCUS = "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-secondary";
10
+ const ROW = "flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors";
11
+ function ShellNav({
12
+ groups,
13
+ footerGroups = [],
14
+ pathname,
15
+ linkComponent,
16
+ header,
17
+ footer,
18
+ renderItem,
19
+ onItemIntent,
20
+ onNavigate,
21
+ labels = {},
22
+ className,
23
+ ...props
24
+ }) {
25
+ const [pendingId, markPending] = usePendingNavigation(pathname);
26
+ const all = [...groups, ...footerGroups].flatMap((g) => g.items);
27
+ const currentId = (pendingId !== null && all.some((i) => i.id === pendingId) ? pendingId : void 0) ?? resolveActiveNavItem(all, pathname)?.id;
28
+ const section = (group) => /* @__PURE__ */ jsx(
29
+ ShellNavSection,
30
+ {
31
+ group,
32
+ currentId,
33
+ pendingId,
34
+ row: { linkComponent, markPending, onItemIntent, onNavigate, renderItem, badgeLabel: labels.badge }
35
+ },
36
+ group.id
37
+ );
38
+ return /* @__PURE__ */ jsxs(
39
+ "aside",
40
+ {
41
+ "data-slot": "shell-nav",
42
+ className: cn(
43
+ "flex h-full w-(--shell-rail-w) flex-col gap-7 overflow-y-auto border-r border-border bg-secondary px-4.5 pb-5 pt-6",
44
+ className
45
+ ),
46
+ ...props,
47
+ children: [
48
+ header,
49
+ /* @__PURE__ */ jsx("nav", { "aria-label": labels.primary ?? "Main", className: "flex flex-col gap-4.5", children: groups.map(section) }),
50
+ /* @__PURE__ */ jsx("div", { className: "flex-1" }),
51
+ footerGroups.length > 0 && /* @__PURE__ */ jsx("nav", { "aria-label": labels.footer ?? "Account", className: "flex flex-col gap-4.5", children: footerGroups.map(section) }),
52
+ footer
53
+ ]
54
+ }
55
+ );
56
+ }
57
+ function ShellNavSection({
58
+ group,
59
+ currentId,
60
+ pendingId,
61
+ row
62
+ }) {
63
+ const root = React.useRef(null);
64
+ const labelId = React.useId();
65
+ const at = group.items.some((i) => i.id === currentId) ? currentId ?? null : null;
66
+ useSlideMarker(root, at);
67
+ return /* @__PURE__ */ jsxs(
68
+ "div",
69
+ {
70
+ ref: root,
71
+ "data-slot": "shell-nav-section",
72
+ className: cn("relative isolate flex flex-col gap-1", group.label !== void 0 && "pl-1"),
73
+ children: [
74
+ at !== null && /* @__PURE__ */ jsx(
75
+ "span",
76
+ {
77
+ "data-slot": "shell-nav-marker",
78
+ "aria-hidden": true,
79
+ className: "pointer-events-none absolute left-0 top-0 -z-10 rounded-lg bg-primary"
80
+ }
81
+ ),
82
+ group.label !== void 0 && /* @__PURE__ */ jsx("p", { id: labelId, className: "mb-0.5 text-xs font-semibold uppercase tracking-widest text-ink-soft", children: group.label }),
83
+ /* @__PURE__ */ jsx(
84
+ "ul",
85
+ {
86
+ "aria-labelledby": group.label !== void 0 ? labelId : void 0,
87
+ className: "flex flex-col gap-1",
88
+ children: group.items.map((item) => /* @__PURE__ */ jsx("li", { className: "flex flex-col", children: /* @__PURE__ */ jsx(
89
+ ShellNavRow,
90
+ {
91
+ item,
92
+ state: { active: item.id === currentId, pending: item.id === pendingId },
93
+ row
94
+ }
95
+ ) }, item.id))
96
+ }
97
+ )
98
+ ]
99
+ }
100
+ );
101
+ }
102
+ function ShellNavRow({ item, state, row }) {
103
+ const Icon = item.icon;
104
+ const trailing = typeof item.trailing === "function" ? item.trailing(state) : item.trailing;
105
+ return /* @__PURE__ */ jsx(
106
+ NavLink,
107
+ {
108
+ item,
109
+ current: state.active,
110
+ pending: state.pending,
111
+ linkComponent: row.linkComponent,
112
+ markPending: row.markPending,
113
+ onItemIntent: row.onItemIntent,
114
+ onNavigate: row.onNavigate,
115
+ title: labelTitle(item.label),
116
+ className: cn(
117
+ ROW,
118
+ NAV_FOCUS,
119
+ state.active ? "font-semibold text-on-primary" : "font-medium text-ink hover:bg-hover",
120
+ item.disabled && "cursor-not-allowed opacity-50 hover:bg-transparent"
121
+ ),
122
+ children: row.renderItem ? row.renderItem(item, state) : /* @__PURE__ */ jsxs(Fragment, { children: [
123
+ Icon && /* @__PURE__ */ jsx(Icon, { className: "size-4.5 shrink-0" }),
124
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label }),
125
+ typeof item.badge === "number" ? /* @__PURE__ */ jsx(
126
+ NavBadge,
127
+ {
128
+ count: item.badge,
129
+ active: state.active,
130
+ ...row.badgeLabel ? { label: row.badgeLabel } : {}
131
+ }
132
+ ) : item.badge,
133
+ trailing
134
+ ] })
135
+ }
136
+ );
137
+ }
138
+ export {
139
+ ShellNav
140
+ };
141
+ //# sourceMappingURL=shell-nav.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/shell-nav.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { NavBadge } from \"./nav-badge\";\nimport { labelTitle, NavLink, type NavLinkCallbacks } from \"./nav-link\";\nimport { resolveActiveNavItem, type NavGroup, type NavItem, type NavItemState } from \"./nav-model\";\nimport { usePendingNavigation, useSlideMarker } from \"./use-shell-nav\";\n\nexport interface ShellNavLabels {\n /** Accessible name of the nav holding `groups`. Default `\"Main\"`. */\n primary?: string;\n /** Accessible name of the nav holding `footerGroups`. Default `\"Account\"`. */\n footer?: string;\n /** Spoken text of a numeric badge, given the real count. Default `\"{n} new\"`. */\n badge?: (count: number) => string;\n}\n\nexport interface ShellNavProps\n extends Omit<React.ComponentProps<\"aside\">, \"children\">,\n NavLinkCallbacks {\n groups: readonly NavGroup[];\n /** Pinned to the bottom of the rail (profile, notifications, settings). */\n footerGroups?: readonly NavGroup[];\n /** The router's current path — `usePathname()` in Next. */\n pathname: string;\n /** Your router's link (`next/link`); defaults to a plain `<a>`. */\n linkComponent?: React.ElementType;\n /** Above the groups — the brand lock-up. */\n header?: React.ReactNode;\n /** Below the footer groups — the signed-in account, a sign-out. */\n footer?: React.ReactNode;\n /** Replaces a row's content (icon, label, badge); the link, `aria-current` and the marker stay the kit's. */\n renderItem?: (item: NavItem, state: NavItemState) => React.ReactNode;\n labels?: ShellNavLabels;\n}\n\n// The ring is the solid token, never a tint — at 50% it composites under the 3:1\n// non-text floor. The offset keeps it legible around the marked row, whose fill\n// is that same hue.\nconst NAV_FOCUS =\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-secondary\";\nconst ROW = \"flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors\";\n\n/**\n * The app's left rail: data-driven groups, a footer block pinned to the bottom,\n * one sliding marker per group, and an optimistic mark on click so the row\n * answers the click rather than the router's round-trip.\n *\n * Exactly one row across the rail is current — the strongest claim on the path\n * (see {@link resolveActiveNavItem}) — so overlapping `prefix` items are safe.\n * Visibility (roles, flags) is the caller's: pass only the items to show.\n */\nexport function ShellNav({\n groups,\n footerGroups = [],\n pathname,\n linkComponent,\n header,\n footer,\n renderItem,\n onItemIntent,\n onNavigate,\n labels = {},\n className,\n ...props\n}: ShellNavProps) {\n const [pendingId, markPending] = usePendingNavigation(pathname);\n const all = [...groups, ...footerGroups].flatMap((g) => g.items);\n const currentId =\n (pendingId !== null && all.some((i) => i.id === pendingId) ? pendingId : undefined) ??\n resolveActiveNavItem(all, pathname)?.id;\n const section = (group: NavGroup) => (\n <ShellNavSection\n key={group.id}\n group={group}\n currentId={currentId}\n pendingId={pendingId}\n row={{ linkComponent, markPending, onItemIntent, onNavigate, renderItem, badgeLabel: labels.badge }}\n />\n );\n return (\n <aside\n data-slot=\"shell-nav\"\n className={cn(\n \"flex h-full w-(--shell-rail-w) flex-col gap-7 overflow-y-auto border-r border-border bg-secondary px-4.5 pb-5 pt-6\",\n className,\n )}\n {...props}\n >\n {header}\n <nav aria-label={labels.primary ?? \"Main\"} className=\"flex flex-col gap-4.5\">\n {groups.map(section)}\n </nav>\n <div className=\"flex-1\" />\n {footerGroups.length > 0 && (\n <nav aria-label={labels.footer ?? \"Account\"} className=\"flex flex-col gap-4.5\">\n {footerGroups.map(section)}\n </nav>\n )}\n {footer}\n </aside>\n );\n}\n\ninterface RowContext extends NavLinkCallbacks {\n linkComponent: React.ElementType | undefined;\n markPending: (id: string) => void;\n renderItem: ShellNavProps[\"renderItem\"];\n badgeLabel: ((count: number) => string) | undefined;\n}\n\n// One marker node per SECTION, mounted once and moved — not one per rail: a\n// marker travelling between groups would cross headings that have nothing to do\n// with either row. Crossing groups unmounts one marker and mounts another (a\n// fade). `isolate` is load-bearing: the marker sits on a negative z-index and\n// without its own stacking context would land behind the rail's background.\nfunction ShellNavSection({\n group,\n currentId,\n pendingId,\n row,\n}: {\n group: NavGroup;\n currentId: string | undefined;\n pendingId: string | null;\n row: RowContext;\n}) {\n const root = React.useRef<HTMLDivElement>(null);\n const labelId = React.useId();\n const at = group.items.some((i) => i.id === currentId) ? (currentId ?? null) : null;\n useSlideMarker(root, at);\n return (\n <div\n ref={root}\n data-slot=\"shell-nav-section\"\n className={cn(\"relative isolate flex flex-col gap-1\", group.label !== undefined && \"pl-1\")}\n >\n {/* First in DOM order: the pre-hydration fill in motion.css reaches the marked\n row through a sibling combinator, which only looks forward. */}\n {at !== null && (\n <span\n data-slot=\"shell-nav-marker\"\n aria-hidden\n className=\"pointer-events-none absolute left-0 top-0 -z-10 rounded-lg bg-primary\"\n />\n )}\n {group.label !== undefined && (\n <p id={labelId} className=\"mb-0.5 text-xs font-semibold uppercase tracking-widest text-ink-soft\">\n {group.label}\n </p>\n )}\n <ul\n aria-labelledby={group.label !== undefined ? labelId : undefined}\n className=\"flex flex-col gap-1\"\n >\n {group.items.map((item) => (\n <li key={item.id} className=\"flex flex-col\">\n <ShellNavRow\n item={item}\n state={{ active: item.id === currentId, pending: item.id === pendingId }}\n row={row}\n />\n </li>\n ))}\n </ul>\n </div>\n );\n}\n\nfunction ShellNavRow({ item, state, row }: { item: NavItem; state: NavItemState; row: RowContext }) {\n const Icon = item.icon;\n const trailing = typeof item.trailing === \"function\" ? item.trailing(state) : item.trailing;\n return (\n <NavLink\n item={item}\n current={state.active}\n pending={state.pending}\n linkComponent={row.linkComponent}\n markPending={row.markPending}\n onItemIntent={row.onItemIntent}\n onNavigate={row.onNavigate}\n title={labelTitle(item.label)}\n className={cn(\n ROW,\n NAV_FOCUS,\n state.active ? \"font-semibold text-on-primary\" : \"font-medium text-ink hover:bg-hover\",\n item.disabled && \"cursor-not-allowed opacity-50 hover:bg-transparent\",\n )}\n >\n {row.renderItem ? (\n row.renderItem(item, state)\n ) : (\n <>\n {Icon && <Icon className=\"size-4.5 shrink-0\" />}\n {/* `min-w-0`: without it the label refuses to shrink below its own text and\n the rail widens to the longest translation instead of truncating. */}\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n {typeof item.badge === \"number\" ? (\n <NavBadge\n count={item.badge}\n active={state.active}\n {...(row.badgeLabel ? { label: row.badgeLabel } : {})}\n />\n ) : (\n item.badge\n )}\n {trailing}\n </>\n )}\n </NavLink>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,53 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { CloseIcon, InfoIcon, TriangleAlertIcon } from "./shell-icons.js";
6
+ const TONE = {
7
+ warn: "border-accent-warn/40 bg-accent-warn/10 text-accent-warn",
8
+ info: "border-border bg-card text-ink"
9
+ };
10
+ function SystemBanner({
11
+ tone = "info",
12
+ title,
13
+ icon,
14
+ onDismiss,
15
+ labels = {},
16
+ className,
17
+ children,
18
+ ...props
19
+ }) {
20
+ const glyph = icon !== void 0 ? icon : tone === "warn" ? /* @__PURE__ */ jsx(TriangleAlertIcon, { className: "size-4" }) : /* @__PURE__ */ jsx(InfoIcon, { className: "size-4" });
21
+ return /* @__PURE__ */ jsxs(
22
+ "div",
23
+ {
24
+ "data-slot": "system-banner",
25
+ "data-tone": tone,
26
+ role: "status",
27
+ className: cn("flex items-start gap-2.5 rounded-lg border px-4 py-2.5 text-sm", TONE[tone], className),
28
+ ...props,
29
+ children: [
30
+ glyph !== null && /* @__PURE__ */ jsx("span", { className: "mt-0.5 flex shrink-0", children: glyph }),
31
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
32
+ title !== void 0 && /* @__PURE__ */ jsx("span", { className: "font-semibold", children: title }),
33
+ title !== void 0 && children !== void 0 && " \u2014 ",
34
+ children !== void 0 && /* @__PURE__ */ jsx("span", { className: cn(title !== void 0 && tone === "info" && "text-ink-soft"), children })
35
+ ] }),
36
+ onDismiss && /* @__PURE__ */ jsx(
37
+ "button",
38
+ {
39
+ type: "button",
40
+ "aria-label": labels.dismiss ?? "Dismiss",
41
+ onClick: onDismiss,
42
+ className: "-mr-1 shrink-0 rounded-md p-0.5 opacity-70 outline-none transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring",
43
+ children: /* @__PURE__ */ jsx(CloseIcon, { className: "size-4" })
44
+ }
45
+ )
46
+ ]
47
+ }
48
+ );
49
+ }
50
+ export {
51
+ SystemBanner
52
+ };
53
+ //# sourceMappingURL=system-banner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/system-banner.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { CloseIcon, InfoIcon, TriangleAlertIcon } from \"./shell-icons\";\n\nexport type SystemBannerTone = \"warn\" | \"info\";\n\nexport interface SystemBannerLabels {\n /** The close button. Default `\"Dismiss\"`. */\n dismiss?: string;\n}\n\nexport interface SystemBannerProps extends Omit<React.ComponentProps<\"div\">, \"title\"> {\n /** `warn` — a platform state (maintenance, read-only); `info` — an announcement. */\n tone?: SystemBannerTone;\n /** Bold lead before the message. */\n title?: React.ReactNode;\n /** Replaces the tone's glyph; `null` drops it. */\n icon?: React.ReactNode;\n /** Shows a close button; remembering the dismissal is the caller's. */\n onDismiss?: () => void;\n labels?: SystemBannerLabels;\n}\n\nconst TONE: Record<SystemBannerTone, string> = {\n warn: \"border-accent-warn/40 bg-accent-warn/10 text-accent-warn\",\n info: \"border-border bg-card text-ink\",\n};\n\n/**\n * A slim full-width strip above the page — presentation only: where its state\n * comes from, and whether a dismissal sticks, are the caller's. Put it in\n * `AppShell`'s `banner` slot; several stack with a gap.\n */\nexport function SystemBanner({\n tone = \"info\",\n title,\n icon,\n onDismiss,\n labels = {},\n className,\n children,\n ...props\n}: SystemBannerProps) {\n const glyph =\n icon !== undefined ? icon : tone === \"warn\" ? <TriangleAlertIcon className=\"size-4\" /> : <InfoIcon className=\"size-4\" />;\n return (\n <div\n data-slot=\"system-banner\"\n data-tone={tone}\n role=\"status\"\n className={cn(\"flex items-start gap-2.5 rounded-lg border px-4 py-2.5 text-sm\", TONE[tone], className)}\n {...props}\n >\n {glyph !== null && <span className=\"mt-0.5 flex shrink-0\">{glyph}</span>}\n <div className=\"min-w-0 flex-1\">\n {title !== undefined && <span className=\"font-semibold\">{title}</span>}\n {title !== undefined && children !== undefined && \" — \"}\n {children !== undefined && (\n <span className={cn(title !== undefined && tone === \"info\" && \"text-ink-soft\")}>{children}</span>\n )}\n </div>\n {onDismiss && (\n <button\n type=\"button\"\n aria-label={labels.dismiss ?? \"Dismiss\"}\n onClick={onDismiss}\n className=\"-mr-1 shrink-0 rounded-md p-0.5 opacity-70 outline-none transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <CloseIcon className=\"size-4\" />\n </button>\n )}\n </div>\n );\n}\n"],"mappings":";AA8CkD,cAU5C,YAV4C;AA5ClD,YAAY,WAAW;AACvB,SAAS,UAAU;AACnB,SAAS,WAAW,UAAU,yBAAyB;AAqBvD,MAAM,OAAyC;AAAA,EAC7C,MAAM;AAAA,EACN,MAAM;AACR;AAOO,SAAS,aAAa;AAAA,EAC3B,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS,CAAC;AAAA,EACV;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAsB;AACpB,QAAM,QACJ,SAAS,SAAY,OAAO,SAAS,SAAS,oBAAC,qBAAkB,WAAU,UAAS,IAAK,oBAAC,YAAS,WAAU,UAAS;AACxH,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,aAAW;AAAA,MACX,MAAK;AAAA,MACL,WAAW,GAAG,kEAAkE,KAAK,IAAI,GAAG,SAAS;AAAA,MACpG,GAAG;AAAA,MAEH;AAAA,kBAAU,QAAQ,oBAAC,UAAK,WAAU,wBAAwB,iBAAM;AAAA,QACjE,qBAAC,SAAI,WAAU,kBACZ;AAAA,oBAAU,UAAa,oBAAC,UAAK,WAAU,iBAAiB,iBAAM;AAAA,UAC9D,UAAU,UAAa,aAAa,UAAa;AAAA,UACjD,aAAa,UACZ,oBAAC,UAAK,WAAW,GAAG,UAAU,UAAa,SAAS,UAAU,eAAe,GAAI,UAAS;AAAA,WAE9F;AAAA,QACC,aACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,cAAY,OAAO,WAAW;AAAA,YAC9B,SAAS;AAAA,YACT,WAAU;AAAA,YAEV,8BAAC,aAAU,WAAU,UAAS;AAAA;AAAA,QAChC;AAAA;AAAA;AAAA,EAEJ;AAEJ;","names":[]}