@orangecheck/app-design 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/chrome.d.mts +5 -4
  2. package/dist/chrome.d.ts +5 -4
  3. package/dist/chrome.js +171 -40
  4. package/dist/chrome.js.map +1 -1
  5. package/dist/chrome.mjs +172 -41
  6. package/dist/chrome.mjs.map +1 -1
  7. package/dist/components.d.mts +46 -9
  8. package/dist/components.d.ts +46 -9
  9. package/dist/components.js +334 -167
  10. package/dist/components.js.map +1 -1
  11. package/dist/components.mjs +333 -170
  12. package/dist/components.mjs.map +1 -1
  13. package/dist/{docs-BB9gK4KF.d.ts → docs-ByUx4t-D.d.ts} +2 -1
  14. package/dist/{docs-D-T-nHiu.d.mts → docs-CFE5mU_6.d.mts} +2 -1
  15. package/dist/index.d.mts +4 -4
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.js +572 -244
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +570 -246
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/mdx.d.mts +1 -1
  22. package/dist/mdx.d.ts +1 -1
  23. package/dist/mdx.js +7 -2
  24. package/dist/mdx.js.map +1 -1
  25. package/dist/mdx.mjs +7 -2
  26. package/dist/mdx.mjs.map +1 -1
  27. package/dist/primitives.d.mts +4 -3
  28. package/dist/primitives.d.ts +4 -3
  29. package/dist/primitives.js +37 -1
  30. package/dist/primitives.js.map +1 -1
  31. package/dist/primitives.mjs +37 -1
  32. package/dist/primitives.mjs.map +1 -1
  33. package/dist/session.d.mts +3 -3
  34. package/dist/session.d.ts +3 -3
  35. package/dist/session.js +1 -1
  36. package/dist/session.js.map +1 -1
  37. package/dist/session.mjs +1 -1
  38. package/dist/session.mjs.map +1 -1
  39. package/dist/{tool-shell-D8Q8XX11.d.mts → tool-shell-CQZNRUgH.d.mts} +8 -4
  40. package/dist/{tool-shell-gmlj47Ie.d.ts → tool-shell-CRmceGrV.d.ts} +8 -4
  41. package/dist/{tools-DFIxIj5p.d.mts → tools-Bg1gi2HO.d.mts} +1 -1
  42. package/dist/{tools-BggNN5DV.d.ts → tools-Bw7RhO92.d.ts} +1 -1
  43. package/package.json +3 -2
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/cn.ts","../src/primitives/feedback.tsx","../src/primitives/button.tsx","../src/primitives/overlay.tsx","../src/chrome/app-shell.tsx","../src/chrome/tool-shell.tsx","../src/format/index.ts","../src/session/index.tsx"],"names":["Separator","forwardRef","Button","jsx","jsxs","DialogContent","DialogTitle","DialogDescription","SheetContent","PopoverContent","TooltipContent","DropdownMenuContent","DropdownMenuItem","DropdownMenuLabel","DropdownMenuSeparator"],"mappings":"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","file":"session.mjs","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { extendTailwindMerge } from 'tailwind-merge';\n\n// tailwind-merge would treat `oca-display-1` and `text-foreground` as the same\n// group and drop one; teach it the package's own type utilities.\nconst merge = extendTailwindMerge<'oca-type'>({\n extend: {\n classGroups: {\n 'oca-type': [\n 'oca-display-1',\n 'oca-display-2',\n 'oca-display-3',\n 'oca-title',\n 'oca-title-sm',\n 'oca-eyebrow',\n 'oca-lead',\n 'oca-body',\n 'oca-numeral',\n ],\n },\n },\n});\n\nexport function cn(...inputs: ClassValue[]): string {\n return merge(clsx(inputs));\n}\n","import * as SeparatorPrimitive from '@radix-ui/react-separator';\nimport {\n forwardRef,\n useEffect,\n useState,\n type ComponentPropsWithoutRef,\n type ElementRef,\n type HTMLAttributes,\n} from 'react';\nimport { Toaster as SonnerToaster, type ToasterProps } from 'sonner';\n\nimport { cn } from '../lib/cn';\n\nexport function Spinner({ className, label }: { className?: string; label?: string }) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role={label ? 'status' : undefined}\n aria-label={label}\n aria-hidden={label ? undefined : true}\n className={cn('size-4 animate-spin', className)}\n >\n <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"currentColor\" strokeOpacity=\"0.2\" strokeWidth=\"2.5\" />\n <path d=\"M21 12a9 9 0 0 0-9-9\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" />\n </svg>\n );\n}\n\n/** A placeholder bar, visible on the canvas and on a card in both modes. */\nexport function Skeleton({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n return (\n <div\n data-ui=\"skeleton\"\n className={cn('animate-pulse rounded-lg bg-[var(--oca-skeleton)]', className)}\n {...props}\n />\n );\n}\n\n/**\n * True once `active` has stayed true for `ms`. A loading state that never\n * ends reads as a broken page; pass this to show a way out instead.\n */\nexport function useTimedOut(active: boolean, ms = 12_000): boolean {\n const [timedOut, setTimedOut] = useState(false);\n useEffect(() => {\n if (!active) {\n setTimedOut(false);\n return;\n }\n const t = setTimeout(() => setTimedOut(true), ms);\n return () => clearTimeout(t);\n }, [active, ms]);\n return timedOut;\n}\n\nexport const Separator = forwardRef<\n ElementRef<typeof SeparatorPrimitive.Root>,\n ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(function Separator({ className, orientation = 'horizontal', decorative = true, ...props }, ref) {\n return (\n <SeparatorPrimitive.Root\n ref={ref}\n decorative={decorative}\n orientation={orientation}\n className={cn(\n 'shrink-0 bg-border',\n orientation === 'horizontal' ? 'h-px w-full' : 'h-full w-px',\n className\n )}\n {...props}\n />\n );\n});\n\nexport function Kbd({ className, ...props }: HTMLAttributes<HTMLElement>) {\n return (\n <kbd\n className={cn(\n 'inline-flex h-5 min-w-5 items-center justify-center rounded-md border border-border bg-card px-1.5 font-mono text-[11px] text-muted-foreground',\n className\n )}\n {...props}\n />\n );\n}\n\n/** Live-region status dot: a calm pulse for \"running\", solid otherwise. */\nexport function StatusDot({\n tone = 'neutral',\n pulse = false,\n className,\n}: {\n tone?: 'neutral' | 'success' | 'warning' | 'danger' | 'brand';\n pulse?: boolean;\n className?: string;\n}) {\n const color = {\n neutral: 'bg-oca-subtle',\n success: 'bg-success',\n warning: 'bg-warning',\n danger: 'bg-destructive',\n brand: 'bg-brand',\n }[tone];\n return (\n <span\n aria-hidden\n className={cn('inline-block size-2 shrink-0 rounded-full', color, pulse && 'oca-animate-pulse-dot', className)}\n />\n );\n}\n\n/** Toasts in the system's type and surfaces. Mount once. */\nexport function Toaster(props: ToasterProps) {\n return (\n <SonnerToaster\n position=\"bottom-right\"\n toastOptions={{\n classNames: {\n toast: 'oca-toast rounded-2xl border border-border bg-popover text-popover-foreground shadow-lg font-sans',\n title: 'text-[0.9375rem] font-medium',\n description: 'text-sm text-muted-foreground',\n actionButton: 'rounded-full bg-primary text-primary-foreground',\n cancelButton: 'rounded-full bg-muted text-foreground',\n },\n }}\n {...props}\n />\n );\n}\n\nexport { toast } from 'sonner';\n","import { Slot } from '@radix-ui/react-slot';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { forwardRef, type ButtonHTMLAttributes } from 'react';\n\nimport { cn } from '../lib/cn';\nimport { Spinner } from './feedback';\n\n/*\n * Pills, as the deck draws them: the black filled pill leads, the white\n * outlined pill follows. `brand` is for the orange band only; `onInk` and\n * `onInkOutline` are the same pair inverted for dark bands.\n */\nexport const buttonVariants = cva(\n [\n 'inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-full font-medium',\n 'transition-[background-color,border-color,color,box-shadow,transform] duration-150',\n 'outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',\n 'active:translate-y-px disabled:pointer-events-none',\n // Disabled is one look for every variant: a quiet grey pill, never a\n // faded black or orange slab that reads as broken.\n 'disabled:border disabled:border-border disabled:bg-muted disabled:text-oca-subtle disabled:shadow-none',\n '[&_svg]:pointer-events-none [&_svg]:size-[1.1em] [&_svg]:shrink-0',\n ],\n {\n variants: {\n variant: {\n primary: 'bg-primary text-primary-foreground hover:bg-primary/85',\n secondary:\n 'border border-border bg-secondary text-secondary-foreground hover:border-oca-hairline-strong hover:bg-accent',\n brand: 'bg-brand text-brand-foreground hover:bg-oca-orange-deep',\n ghost: 'text-foreground hover:bg-accent',\n danger: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',\n link: 'h-auto rounded-none px-0 text-foreground underline decoration-foreground/35 underline-offset-4 hover:decoration-foreground',\n onInk: 'bg-white text-oca-ink hover:bg-white/90',\n onInkOutline: 'border border-white/20 text-white hover:bg-white/10',\n },\n size: {\n sm: 'h-9 px-4 text-sm',\n md: 'h-11 px-5 text-[0.9375rem]',\n lg: 'h-14 px-7 text-[1.0625rem]',\n icon: 'size-10',\n 'icon-sm': 'size-8',\n },\n },\n compoundVariants: [{ variant: 'link', className: 'h-auto px-0' }],\n defaultVariants: { variant: 'primary', size: 'md' },\n }\n);\n\nexport interface ButtonProps\n extends ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n /** Render the child element (a Link, an <a>) with the button's styles. */\n asChild?: boolean;\n /** Shows a spinner and disables the button. */\n loading?: boolean;\n}\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n { className, variant, size, asChild = false, loading = false, disabled, children, type, ...props },\n ref\n) {\n const Comp = asChild ? Slot : 'button';\n return (\n <Comp\n ref={ref}\n data-ui=\"button\"\n className={cn(buttonVariants({ variant, size }), className)}\n disabled={asChild ? undefined : disabled || loading}\n aria-busy={loading || undefined}\n type={asChild ? undefined : (type ?? 'button')}\n {...props}\n >\n {asChild ? (\n children\n ) : (\n <>\n {loading ? <Spinner className=\"size-[1.1em]\" /> : null}\n {children}\n </>\n )}\n </Comp>\n );\n});\n","import * as DialogPrimitive from '@radix-ui/react-dialog';\nimport * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';\nimport * as PopoverPrimitive from '@radix-ui/react-popover';\nimport * as TooltipPrimitive from '@radix-ui/react-tooltip';\nimport { X } from 'lucide-react';\nimport { forwardRef, type ComponentPropsWithoutRef, type ElementRef, type HTMLAttributes } from 'react';\n\nimport { cn } from '../lib/cn';\n\n/* ── Dialog ───────────────────────────────────────────────────────────── */\n\nexport const Dialog = DialogPrimitive.Root;\nexport const DialogTrigger = DialogPrimitive.Trigger;\nexport const DialogClose = DialogPrimitive.Close;\n\nconst overlayClass =\n 'oca-animate-fade fixed inset-0 z-50 bg-[rgb(10_10_10/0.38)] backdrop-blur-[2px] data-[state=closed]:opacity-0';\n\nexport const DialogContent = forwardRef<\n ElementRef<typeof DialogPrimitive.Content>,\n ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { hideClose?: boolean }\n>(function DialogContent({ className, children, hideClose, ...props }, ref) {\n return (\n <DialogPrimitive.Portal>\n <DialogPrimitive.Overlay className={overlayClass} />\n <DialogPrimitive.Content\n ref={ref}\n className={cn(\n 'oca-animate-rise fixed left-1/2 top-1/2 z-50 w-[min(92vw,34rem)] -translate-x-1/2 -translate-y-1/2',\n 'max-h-[88vh] overflow-y-auto rounded-[1.5rem] border border-border bg-popover p-6 text-popover-foreground shadow-xl md:p-8',\n 'outline-none',\n className\n )}\n {...props}\n >\n {children}\n {hideClose ? null : (\n <DialogPrimitive.Close\n className=\"absolute right-4 top-4 inline-flex size-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground\"\n aria-label=\"Close\"\n >\n <X className=\"size-4\" />\n </DialogPrimitive.Close>\n )}\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n );\n});\n\nexport function DialogHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n return <div className={cn('mb-5 flex flex-col gap-2 pr-8', className)} {...props} />;\n}\n\nexport function DialogFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n return <div className={cn('mt-7 flex flex-col-reverse gap-3 sm:flex-row sm:justify-end', className)} {...props} />;\n}\n\nexport const DialogTitle = forwardRef<\n ElementRef<typeof DialogPrimitive.Title>,\n ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(function DialogTitle({ className, ...props }, ref) {\n return (\n <DialogPrimitive.Title\n ref={ref}\n className={cn('text-[1.5rem] font-normal leading-tight tracking-[-0.02em]', className)}\n {...props}\n />\n );\n});\n\nexport const DialogDescription = forwardRef<\n ElementRef<typeof DialogPrimitive.Description>,\n ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(function DialogDescription({ className, ...props }, ref) {\n return <DialogPrimitive.Description ref={ref} className={cn('oca-body', className)} {...props} />;\n});\n\n/* ── Sheet: a dialog from an edge ────────────────────────────────────── */\n\nexport const Sheet = DialogPrimitive.Root;\nexport const SheetTrigger = DialogPrimitive.Trigger;\nexport const SheetClose = DialogPrimitive.Close;\nexport const SheetTitle = DialogTitle;\nexport const SheetDescription = DialogDescription;\n\nconst sheetSide = {\n right: 'inset-y-0 right-0 h-full w-[min(88vw,24rem)] border-l',\n left: 'inset-y-0 left-0 h-full w-[min(88vw,24rem)] border-r',\n bottom: 'inset-x-0 bottom-0 max-h-[88vh] rounded-t-[1.5rem] border-t',\n} as const;\n\nexport const SheetContent = forwardRef<\n ElementRef<typeof DialogPrimitive.Content>,\n ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { side?: keyof typeof sheetSide }\n>(function SheetContent({ className, children, side = 'right', ...props }, ref) {\n return (\n <DialogPrimitive.Portal>\n <DialogPrimitive.Overlay className={overlayClass} />\n <DialogPrimitive.Content\n ref={ref}\n className={cn(\n 'oca-animate-fade fixed z-50 flex flex-col gap-4 overflow-y-auto border-border bg-popover p-6 text-popover-foreground shadow-xl outline-none',\n sheetSide[side],\n className\n )}\n {...props}\n >\n {children}\n <DialogPrimitive.Close\n className=\"absolute right-4 top-4 inline-flex size-9 items-center justify-center rounded-full text-muted-foreground hover:bg-accent hover:text-foreground\"\n aria-label=\"Close\"\n >\n <X className=\"size-4\" />\n </DialogPrimitive.Close>\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n );\n});\n\n/* ── Popover ──────────────────────────────────────────────────────────── */\n\nexport const Popover = PopoverPrimitive.Root;\nexport const PopoverTrigger = PopoverPrimitive.Trigger;\nexport const PopoverAnchor = PopoverPrimitive.Anchor;\n\nexport const PopoverContent = forwardRef<\n ElementRef<typeof PopoverPrimitive.Content>,\n ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(function PopoverContent({ className, align = 'center', sideOffset = 8, ...props }, ref) {\n return (\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n ref={ref}\n align={align}\n sideOffset={sideOffset}\n className={cn(\n 'oca-animate-fade z-50 w-72 rounded-2xl border border-border bg-popover p-4 text-popover-foreground shadow-lg outline-none',\n className\n )}\n {...props}\n />\n </PopoverPrimitive.Portal>\n );\n});\n\n/* ── Tooltip: ink, small, quick ───────────────────────────────────────── */\n\nexport const TooltipProvider = TooltipPrimitive.Provider;\nexport const Tooltip = TooltipPrimitive.Root;\nexport const TooltipTrigger = TooltipPrimitive.Trigger;\n\nexport const TooltipContent = forwardRef<\n ElementRef<typeof TooltipPrimitive.Content>,\n ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(function TooltipContent({ className, sideOffset = 6, ...props }, ref) {\n return (\n <TooltipPrimitive.Portal>\n <TooltipPrimitive.Content\n ref={ref}\n sideOffset={sideOffset}\n className={cn(\n 'oca-animate-fade z-50 max-w-xs rounded-lg bg-oca-ink px-2.5 py-1.5 text-[0.8125rem] leading-snug text-oca-ink-foreground',\n className\n )}\n {...props}\n />\n </TooltipPrimitive.Portal>\n );\n});\n\n/* ── Menu ─────────────────────────────────────────────────────────────── */\n\nexport const DropdownMenu = DropdownMenuPrimitive.Root;\nexport const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\nexport const DropdownMenuGroup = DropdownMenuPrimitive.Group;\n\nexport const DropdownMenuContent = forwardRef<\n ElementRef<typeof DropdownMenuPrimitive.Content>,\n ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(function DropdownMenuContent({ className, sideOffset = 8, ...props }, ref) {\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n ref={ref}\n sideOffset={sideOffset}\n className={cn(\n 'oca-animate-fade z-50 min-w-56 overflow-hidden rounded-2xl border border-border bg-popover p-1.5 text-popover-foreground shadow-lg',\n className\n )}\n {...props}\n />\n </DropdownMenuPrimitive.Portal>\n );\n});\n\nexport const DropdownMenuItem = forwardRef<\n ElementRef<typeof DropdownMenuPrimitive.Item>,\n ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { destructive?: boolean }\n>(function DropdownMenuItem({ className, destructive, ...props }, ref) {\n return (\n <DropdownMenuPrimitive.Item\n ref={ref}\n className={cn(\n 'flex h-10 cursor-default select-none items-center gap-2.5 rounded-xl px-3 text-[0.9375rem] outline-none',\n 'data-[highlighted]:bg-accent data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:text-muted-foreground',\n destructive && 'text-destructive [&_svg]:text-destructive',\n className\n )}\n {...props}\n />\n );\n});\n\nexport const DropdownMenuLabel = forwardRef<\n ElementRef<typeof DropdownMenuPrimitive.Label>,\n ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label>\n>(function DropdownMenuLabel({ className, ...props }, ref) {\n return (\n <DropdownMenuPrimitive.Label\n ref={ref}\n className={cn('px-3 pb-1.5 pt-2 text-[0.8125rem] text-muted-foreground', className)}\n {...props}\n />\n );\n});\n\nexport const DropdownMenuSeparator = forwardRef<\n ElementRef<typeof DropdownMenuPrimitive.Separator>,\n ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(function DropdownMenuSeparator({ className, ...props }, ref) {\n return <DropdownMenuPrimitive.Separator ref={ref} className={cn('my-1.5 h-px bg-border', className)} {...props} />;\n});\n","import { ChevronRight, Menu, Moon, Sun, SunMoon } from 'lucide-react';\nimport Link from 'next/link';\nimport { useTheme } from 'next-themes';\nimport { useEffect, useMemo, useState, type ReactNode } from 'react';\n\nimport { Lockup } from '../brand/mark';\nimport type { FamilySlug } from '../family';\nimport { cn } from '../lib/cn';\nimport { ShellContext } from '../lib/shell-context';\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n Sheet,\n SheetContent,\n SheetTitle,\n SheetTrigger,\n} from '../primitives/overlay';\nimport { FamilyMenu } from './family-menu';\n\nexport interface ShellNavItem {\n href: string;\n label: string;\n icon?: ReactNode;\n active?: boolean;\n /** A count or a dot beside the label (unread, needs attention). */\n badge?: ReactNode;\n}\n\nexport interface ShellNavGroup {\n /** Omit for the first, unlabeled group. */\n label?: string;\n items: ShellNavItem[];\n}\n\n/**\n * A part of the app with its own pages: \"You\", \"Build\", \"Operate\", \"Admin\".\n * The sidebar lists one section's pages at a time, under a switcher.\n */\nexport interface ShellSection {\n id: string;\n /** Short, for the switcher: one word. */\n label: string;\n /** The section's eyebrow on its pages; defaults to `label`. */\n title?: string;\n /** Where the switcher goes: the section's first page. */\n href: string;\n groups: ShellNavGroup[];\n}\n\nexport function SideNav({ groups, onNavigate }: { groups: ShellNavGroup[]; onNavigate?: () => void }) {\n return (\n <nav aria-label=\"Pages\" className=\"flex flex-col gap-5\">\n {groups.map((g, gi) => (\n <div key={g.label ?? gi} className=\"flex flex-col gap-0.5\">\n {g.label ? <p className=\"px-3 pb-1 text-[0.8125rem] text-muted-foreground\">{g.label}</p> : null}\n {g.items.map((it) => (\n <Link\n key={it.href}\n href={it.href}\n onClick={onNavigate}\n aria-current={it.active ? 'page' : undefined}\n className={cn(\n 'flex h-9 items-center gap-3 rounded-xl px-3 text-[0.9375rem] transition-colors',\n '[&_svg]:size-[1.0625rem] [&_svg]:shrink-0',\n it.active\n ? 'bg-card text-foreground shadow-sm ring-1 ring-border [&_svg]:text-foreground'\n : 'text-muted-foreground hover:bg-accent hover:text-foreground'\n )}\n >\n {it.icon}\n <span className=\"min-w-0 flex-1 truncate\">{it.label}</span>\n {it.badge ? <span className=\"shrink-0\">{it.badge}</span> : null}\n </Link>\n ))}\n </div>\n ))}\n </nav>\n );\n}\n\nfunction SectionSwitch({\n sections,\n current,\n onNavigate,\n}: {\n sections: ShellSection[];\n current: ShellSection;\n onNavigate?: () => void;\n}) {\n // Up to three fit one row of the sidebar; four go two by two, so no label\n // is ever cut short.\n const cols = sections.length <= 3 ? sections.length : 2;\n return (\n <nav\n aria-label=\"Sections\"\n className={cn('grid gap-1 bg-muted p-1', cols === sections.length ? 'rounded-full' : 'rounded-[1.25rem]')}\n style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}\n >\n {sections.map((sec) => {\n const on = sec.id === current.id;\n return (\n <Link\n key={sec.id}\n href={sec.href}\n onClick={onNavigate}\n aria-current={on ? 'true' : undefined}\n className={cn(\n 'flex h-8 min-w-0 items-center justify-center rounded-full px-1.5 text-[0.8125rem] font-medium transition-colors',\n on ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'\n )}\n >\n <span className=\"truncate\">{sec.label}</span>\n </Link>\n );\n })}\n </nav>\n );\n}\n\n/** The phone's way around a small app: every page one tap away, under the thumb. */\nexport function TabBar({ items }: { items: ShellNavItem[] }) {\n return (\n <nav\n aria-label=\"Pages\"\n data-oca-tabbar=\"\"\n className=\"fixed inset-x-0 bottom-0 z-40 border-t border-border bg-background/90 pb-[env(safe-area-inset-bottom)] backdrop-blur-md md:hidden\"\n >\n <div\n className=\"mx-auto grid h-14 max-w-[32rem] px-1.5\"\n style={{ gridTemplateColumns: `repeat(${items.length}, minmax(0, 1fr))` }}\n >\n {items.map((it) => (\n <Link\n key={it.href}\n href={it.href}\n aria-current={it.active ? 'page' : undefined}\n className={cn(\n 'flex min-w-0 flex-col items-center justify-center gap-1 rounded-2xl text-[0.6875rem] leading-none outline-none transition-colors [-webkit-tap-highlight-color:transparent] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring',\n it.active ? 'text-foreground' : 'text-muted-foreground hover:text-foreground'\n )}\n >\n <span\n className={cn(\n 'relative flex h-7 w-12 items-center justify-center rounded-full transition-[background-color,box-shadow] duration-200',\n '[&_svg]:size-[1.125rem] [&_svg]:shrink-0',\n it.active ? 'bg-card shadow-sm ring-1 ring-border [&_svg]:stroke-2' : '[&_svg]:stroke-[1.75]'\n )}\n >\n {it.icon}\n {it.badge ? (\n <span aria-hidden className=\"absolute right-2.5 top-0.5 size-2 rounded-full bg-oca-orange ring-2 ring-background\" />\n ) : null}\n </span>\n <span className=\"max-w-full truncate px-0.5\">{it.label}</span>\n {it.badge ? <span className=\"sr-only\">{it.badge}</span> : null}\n </Link>\n ))}\n </div>\n </nav>\n );\n}\n\n/** Two to five pages, each with an icon, fit a tab bar. More, or sections, keep the drawer. */\nexport function tabsFor(groups: ShellNavGroup[]): ShellNavItem[] | null {\n const items = groups.flatMap((g) => g.items);\n return items.length >= 2 && items.length <= 5 && items.every((it) => it.icon) ? items : null;\n}\n\nconst activeItem = (groups: ShellNavGroup[]) => groups.flatMap((g) => g.items).find((it) => it.active);\n\n// Bundlers replace process.env.NODE_ENV; the typeof guard covers one that doesn't.\ndeclare const process: { env: { NODE_ENV?: string } } | undefined;\n\nconst warned = new Set<string>();\n\nfunction warnDuplicateLabels(groups: ShellNavGroup[]) {\n if (typeof process === 'undefined' || process.env.NODE_ENV === 'production') return;\n const seen = new Set<string>();\n for (const it of groups.flatMap((g) => g.items)) {\n if (seen.has(it.label) && !warned.has(it.label)) {\n warned.add(it.label);\n console.warn(`AppShell: two pages are labelled \"${it.label}\"; give each its own name.`);\n }\n seen.add(it.label);\n }\n}\n\n/**\n * The signed-in frame: a quiet sidebar on the canvas, content on the right.\n *\n * Give it `sections` and the sidebar shows a switcher (only when there is more\n * than one) and the current section's pages, so it never outgrows the screen;\n * `persistent` groups (Account) follow every section. The section is the one\n * holding an active item, or `activeSection`. On phones a top bar carries the\n * wordmark and the account, a strip under it names the section and page, and\n * the sidebar opens as a sheet. `nav` (flat groups) still works for a simple\n * app, and when it holds two to five pages, each with an icon, phones get a\n * tab bar instead: the pages sit under the thumb, the strip and the menu\n * button go (the sheet stays for a `footer`, without the pages), and tablets\n * keep the sheet. Pages get the section through context: PageHeader uses it\n * as its eyebrow. `family` puts the family menu beside the lockup. The\n * account control and any banner are slots.\n */\nexport function AppShell({\n nav,\n sections,\n activeSection,\n page: pageOverride,\n persistent = [],\n home = '/',\n family,\n account,\n banner,\n footer,\n children,\n className,\n}: {\n /** Flat groups, for an app without sections. */\n nav?: ShellNavGroup[];\n sections?: ShellSection[];\n activeSection?: string;\n /** The page's name for the place strip, when it is not a nav item of its own. */\n page?: string;\n /** Groups shown under every section, after its own. */\n persistent?: ShellNavGroup[];\n home?: string;\n /** Mount the family menu beside the lockup, as SiteHeader does. Pass the site's slug to mark it. */\n family?: boolean | FamilySlug;\n account?: ReactNode;\n banner?: ReactNode;\n footer?: ReactNode;\n children: ReactNode;\n className?: string;\n}) {\n const [open, setOpen] = useState(false);\n const current =\n sections && sections.length > 0\n ? (sections.find((sec) => sec.id === activeSection) ??\n sections.find((sec) => activeItem(sec.groups)) ??\n sections[0]!)\n : undefined;\n const groups = current ? [...current.groups, ...persistent] : [...(nav ?? []), ...persistent];\n warnDuplicateLabels(groups);\n const tabs = current ? null : tabsFor(groups);\n const page = activeItem(groups);\n const place = useMemo(\n () => ({\n section: current ? (current.title ?? current.label) : undefined,\n page: pageOverride ?? page?.label,\n }),\n [current, page?.label, pageOverride]\n );\n const familyMenu = family ? <FamilyMenu current={typeof family === 'string' ? family : undefined} /> : null;\n const switcher =\n sections && current && sections.length > 1 ? (\n <SectionSwitch sections={sections} current={current} onNavigate={() => setOpen(false)} />\n ) : null;\n return (\n <ShellContext.Provider value={place}>\n <div\n className={cn(\n 'min-h-dvh bg-background',\n tabs && 'max-md:pb-[calc(3.5rem_+_env(safe-area-inset-bottom))]',\n className\n )}\n >\n <div className=\"sticky top-0 z-40 border-b border-border bg-background/90 backdrop-blur-md lg:hidden\">\n <div className=\"flex h-14 items-center gap-2 px-3\">\n <Sheet open={open} onOpenChange={setOpen}>\n <SheetTrigger\n className={cn(\n 'inline-flex size-10 items-center justify-center rounded-full hover:bg-accent',\n // The tab bar holds every page; the sheet is left for a footer.\n tabs && !footer && 'max-md:hidden'\n )}\n aria-label=\"Open navigation\"\n >\n <Menu className=\"size-5\" />\n </SheetTrigger>\n <SheetContent side=\"left\" className=\"flex flex-col gap-6 overflow-y-auto p-4 pt-16\">\n <SheetTitle className=\"sr-only\">Navigation</SheetTitle>\n {switcher}\n <div className={tabs ? 'max-md:hidden' : undefined}>\n <SideNav groups={groups} onNavigate={() => setOpen(false)} />\n </div>\n {footer ? <div className=\"mt-auto flex flex-col gap-3\">{footer}</div> : null}\n </SheetContent>\n </Sheet>\n <Link href={home} aria-label=\"Home\" className=\"rounded-lg p-1\">\n <Lockup size={22} />\n </Link>\n {familyMenu}\n <div className=\"ml-auto flex items-center\">{account}</div>\n </div>\n {place.page ? (\n <div\n className={cn(\n 'flex h-9 items-center gap-1.5 border-t border-border px-4 text-[0.875rem]',\n // The active tab already names the page.\n tabs && !pageOverride && 'max-md:hidden'\n )}\n >\n {place.section ? (\n <>\n <span className=\"shrink-0 text-muted-foreground\">{place.section}</span>\n <ChevronRight aria-hidden className=\"size-3.5 shrink-0 text-muted-foreground\" />\n </>\n ) : null}\n <span className=\"truncate font-medium\">{place.page}</span>\n </div>\n ) : null}\n </div>\n <div className=\"mx-auto flex w-full max-w-[90rem]\">\n <aside className=\"sticky top-0 hidden h-dvh w-[17rem] shrink-0 flex-col gap-6 overflow-y-auto px-4 py-6 lg:flex\">\n <div className=\"flex items-center gap-1\">\n <Link href={home} className=\"px-2\" aria-label=\"Home\">\n <Lockup size={26} />\n </Link>\n {familyMenu}\n </div>\n {switcher}\n <SideNav groups={groups} />\n <div className=\"mt-auto flex flex-col gap-3\">{footer}</div>\n </aside>\n <div className=\"flex min-w-0 flex-1 flex-col\">\n <div className=\"hidden h-[4.5rem] items-center justify-end gap-3 px-8 lg:flex\">{account}</div>\n {banner ? <div className=\"px-4 pb-2 pt-4 md:px-8 lg:pt-0\">{banner}</div> : null}\n <main id=\"main-content\" className=\"flex-1 px-4 pb-16 pt-6 md:px-8 lg:pt-2\">\n {/* One width for every signed-in page. */}\n <div className=\"mx-auto w-full max-w-[72rem]\">{children}</div>\n </main>\n </div>\n </div>\n {tabs ? <TabBar items={tabs} /> : null}\n </div>\n </ShellContext.Provider>\n );\n}\n\nexport interface AccountMenuItem {\n label: string;\n href?: string;\n onSelect?: () => void;\n icon?: ReactNode;\n destructive?: boolean;\n}\n\n/** The account button: initial + short id, a menu of items. */\nexport function AccountMenu({\n name,\n detail,\n items,\n nameFrom = 'sm',\n}: {\n name: string;\n /** A second line: the short did, an address. */\n detail?: string;\n items: (AccountMenuItem | 'separator')[];\n /** The width from which the name shows beside the initial. A header with\n * navigation beside it needs the room until lg. */\n nameFrom?: 'sm' | 'lg';\n}) {\n const initial = name.trim().charAt(0).toUpperCase() || '·';\n return (\n <DropdownMenu>\n <DropdownMenuTrigger\n className={cn(\n 'inline-flex h-10 items-center gap-2.5 rounded-full border border-border bg-card pl-1 pr-1 text-[0.9375rem] outline-none transition-colors hover:border-oca-hairline-strong focus-visible:ring-2 focus-visible:ring-ring',\n nameFrom === 'lg' ? 'lg:pr-3.5' : 'sm:pr-3.5'\n )}\n aria-label={`${name}, account menu`}\n >\n {/* The initial is drawn, not written: the button's text is the name it is read by. */}\n <span\n aria-hidden\n data-initial={initial}\n className=\"flex size-8 items-center justify-center rounded-full bg-foreground text-[0.8125rem] font-medium text-background before:content-[attr(data-initial)]\"\n />\n <span className={cn('max-w-[10rem] truncate', nameFrom === 'lg' ? 'max-lg:hidden' : 'max-sm:hidden')}>\n {name}\n </span>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-64\">\n <DropdownMenuLabel className=\"flex flex-col gap-0.5\">\n <span className=\"truncate text-[0.9375rem] font-medium text-foreground\">{name}</span>\n {detail ? <span className=\"truncate font-mono text-[0.75rem]\">{detail}</span> : null}\n </DropdownMenuLabel>\n <DropdownMenuSeparator />\n {items.map((it, i) =>\n it === 'separator' ? (\n <DropdownMenuSeparator key={`sep-${i}`} />\n ) : it.href ? (\n <DropdownMenuItem key={it.label} asChild destructive={it.destructive}>\n <Link href={it.href}>\n {it.icon}\n {it.label}\n </Link>\n </DropdownMenuItem>\n ) : (\n <DropdownMenuItem key={it.label} onSelect={it.onSelect} destructive={it.destructive}>\n {it.icon}\n {it.label}\n </DropdownMenuItem>\n )\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n\n/** Light, dark, or follow the system: one button that cycles. */\nexport function ThemeToggle({ className }: { className?: string }) {\n const { theme, setTheme } = useTheme();\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n const current = mounted ? (theme ?? 'system') : 'system';\n const next = current === 'light' ? 'dark' : current === 'dark' ? 'system' : 'light';\n const Icon = current === 'light' ? Sun : current === 'dark' ? Moon : SunMoon;\n return (\n <button\n type=\"button\"\n onClick={() => setTheme(next)}\n aria-label={`Theme: ${current}. Switch to ${next}`}\n title={`Theme: ${current}`}\n className={cn(\n 'inline-flex size-10 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground',\n className\n )}\n >\n <Icon className=\"size-[1.125rem]\" />\n </button>\n );\n}\n","import { ArrowUpRight, CircleUser, LogOut } from 'lucide-react';\nimport { useRouter } from 'next/compat/router';\nimport type { NextRouter } from 'next/router';\nimport { useSyncExternalStore, type ReactNode } from 'react';\n\nimport type { ToolItem } from '../components/tools';\nimport { protocolSite, type ProtocolSlug } from '../family';\nimport { cn } from '../lib/cn';\nimport { DensityProvider } from '../lib/density';\nimport { Button } from '../primitives/button';\nimport { StatusBadge } from '../primitives/status-badge';\nimport { AccountMenu, SideNav, TabBar, tabsFor, ThemeToggle, type AccountMenuItem } from './app-shell';\nimport { SiteFooter, SiteHeader, SkipLink } from './site';\n\n/* ── The account slot ────────────────────────────────────────────────── */\n\n/** Where the session stands: being checked, signed in, signed out, or the check failed. */\nexport type SessionState = 'checking' | 'in' | 'out' | 'unknown';\n\nexport const OCHK_APP_ACCOUNT = 'https://ochk.app/me/account';\n\n/** Back and forward; a router move re-renders the page, which reads the address again. */\nfunction onHistory(change: () => void) {\n window.addEventListener('popstate', change);\n return () => window.removeEventListener('popstate', change);\n}\n\n/** The page's path and query, never the fragment. Anything the URL parser would read as another host returns home. */\nfunction herePath(): string {\n const { href, origin, pathname, search } = window.location;\n const path = pathname + search;\n return new URL(path, href).origin === origin ? path : '/';\n}\n\n/** The route the server renders, with the base path; none for a dynamic route, whose template is no address. */\nfunction routePath(router: NextRouter | null): string | null {\n if (!router || router.pathname.includes('[')) return null;\n return router.basePath && router.pathname === '/' ? router.basePath : router.basePath + router.pathname;\n}\n\n/**\n * This site's sign-in page, set to return to the page the viewer is on:\n * `/signin?return_to=%2Fverify%3Fid%3D…`. The server renders the route\n * without its query, so hydration matches; the fragment is never read, so a\n * share link's secret stays off servers. Pass no path for none.\n */\nexport function useSignInHref(path?: string): string | undefined {\n const router = useRouter();\n const here = useSyncExternalStore(onHistory, herePath, () => routePath(router));\n if (!path) return undefined;\n return here ? `${path}?return_to=${encodeURIComponent(here)}` : path;\n}\n\n/**\n * The header's account control, for any session source: the account menu\n * when signed in, \"Sign in\" when not. While the check runs, the button holds\n * its place unseen, so the header never shifts and a signed-in person never\n * sees \"Sign in\" flash. A failed check offers the account page, which works\n * either way. SessionAccount (./session) drives it from auth-client.\n */\nexport function SessionAccountView({\n state,\n name,\n detail,\n items = [],\n signInHref,\n signInPath,\n signInLabel = 'Sign in',\n accountHref = OCHK_APP_ACCOUNT,\n onSignOut,\n nameFrom = 'lg',\n}: {\n state: SessionState;\n /** The account's name, as it chose to be shown. */\n name?: string;\n /** A second line: a short address. */\n detail?: string;\n /** This site's own items, above \"Your account\". */\n items?: (AccountMenuItem | 'separator')[];\n /** Where \"Sign in\" goes, as given. */\n signInHref?: string;\n /** This site's sign-in page (`/signin`): \"Sign in\" returns to the page the viewer is on. */\n signInPath?: string;\n signInLabel?: string;\n /** Where \"Your account\" goes: ochk.app. */\n accountHref?: string;\n onSignOut?: () => void;\n nameFrom?: 'sm' | 'lg';\n}) {\n const inPlace = useSignInHref(signInPath);\n if (state === 'in') {\n return (\n <AccountMenu\n name={name || 'Your account'}\n detail={detail}\n nameFrom={nameFrom}\n items={[\n ...items,\n ...(items.length > 0 ? (['separator'] as const) : []),\n { label: 'Your account', href: accountHref, icon: <CircleUser /> },\n 'separator',\n { label: 'Sign out', onSelect: onSignOut, icon: <LogOut /> },\n ]}\n />\n );\n }\n if (state === 'unknown') {\n return (\n <Button asChild size=\"sm\" variant=\"secondary\">\n <a href={accountHref}>Your account</a>\n </Button>\n );\n }\n return (\n // No box of its own: it sits exactly where the signed-in chip will. While checking it is only unseen.\n <span className={state === 'checking' ? 'contents invisible' : 'contents'} aria-hidden={state === 'checking' || undefined}>\n <Button asChild size=\"sm\" variant=\"secondary\" tabIndex={state === 'checking' ? -1 : undefined}>\n <a href={signInHref ?? inPlace ?? 'https://ochk.io/signin'}>{signInLabel}</a>\n </Button>\n </span>\n );\n}\n\n/* ── The shell ───────────────────────────────────────────────────────── */\n\nexport interface ToolShellProps {\n /** The protocol these tools serve. Its word, its page on ochk.io and its status come from the registry. */\n protocol: ProtocolSlug;\n /** create · verify · discover · playground, in the family's words; give each an icon for the phone's tab bar. */\n tools: readonly ToolItem[];\n /** Where the lockup and the protocol's word go. Default: the protocol's page on ochk.io. */\n home?: string;\n /** The account control: SessionAccount, or SessionAccountView. */\n account?: ReactNode;\n /** List the tools in a rail on the left from lg (a dashboard page), not in the header. */\n rail?: boolean;\n /** Under the tools in the rail: Docs, Spec. */\n railFooter?: ReactNode;\n /**\n * `tool`: pages that scroll, with a footer. `app`: the full window, no\n * footer, the page managing its own scroll (an inbox, a workspace).\n */\n variant?: 'tool' | 'app';\n /** A SiteBanner above the header. */\n banner?: ReactNode;\n /** Replace the footer, or `false` for none. Default: the compact family footer. */\n footer?: ReactNode | false;\n children: ReactNode;\n}\n\n/**\n * The frame for a protocol's tools: a slim full-width header (orangecheck ·\n * stamp, linking the protocol's page on ochk.io; the family menu, the tools,\n * \"About stamp\", the status when it is not live, the account, the theme), the\n * page, and the compact family footer. Everything inside is set at compact density. On a phone the\n * tools move to a tab bar under the thumb when there are two to five, each\n * with an icon.\n */\nexport function ToolShell({\n protocol,\n tools,\n home,\n account,\n rail = false,\n railFooter,\n variant = 'tool',\n banner,\n footer,\n children,\n}: ToolShellProps) {\n const p = protocolSite(protocol);\n const app = variant === 'app';\n const tabs = app ? null : tabsFor([{ items: [...tools] }]);\n const about = (\n <a\n href={p.pageHref}\n className=\"inline-flex items-center gap-1 rounded-full px-3 py-2 text-[0.9375rem] text-muted-foreground transition-colors hover:text-foreground\"\n >\n About {p.label}\n <ArrowUpRight aria-hidden className=\"size-3.5\" />\n </a>\n );\n return (\n <DensityProvider density=\"compact\">\n <div\n className={cn(\n 'flex flex-col bg-background',\n app ? 'h-dvh overflow-hidden' : 'min-h-dvh',\n tabs && 'max-md:pb-[calc(3.5rem_+_env(safe-area-inset-bottom))]'\n )}\n >\n <SkipLink />\n {banner}\n <SiteHeader\n variant=\"tool\"\n home={home ?? p.pageHref}\n product={p.label}\n family={protocol}\n nav={tools.map((t) => ({ href: t.href, label: t.label, active: t.active }))}\n navOnPhone={!tabs}\n menu={<div className=\"[&>a]:px-3 [&>a]:py-3 [&>a]:text-[1.0625rem]\">{about}</div>}\n // With a rail, the tools leave the header from lg.\n className={cn(rail && 'lg:[&_nav[aria-label=Main]]:hidden')}\n actions={\n <>\n <span className=\"hidden lg:contents\">{about}</span>\n {p.status !== 'live' ? <StatusBadge status={p.status} className=\"max-sm:hidden\" /> : null}\n <ThemeToggle className=\"max-sm:hidden\" />\n {account}\n </>\n }\n />\n <div className={cn('mx-auto flex w-full', app ? 'min-h-0 flex-1' : 'max-w-[90rem] flex-1')}>\n {rail ? (\n <aside className=\"sticky top-14 hidden h-[calc(100dvh-3.5rem)] w-60 shrink-0 flex-col gap-6 overflow-y-auto px-4 py-6 lg:flex\">\n <SideNav groups={[{ label: 'Tools', items: tools.map(({ href, label, icon, active }) => ({ href, label, icon, active })) }]} />\n {railFooter ? (\n <div className=\"mt-auto flex flex-col gap-0.5 px-3 text-[0.875rem] text-muted-foreground [&_a:hover]:text-foreground [&_a]:py-1.5 [&_a]:transition-colors\">\n {railFooter}\n </div>\n ) : null}\n </aside>\n ) : null}\n <main\n id=\"main-content\"\n className={cn('min-w-0 flex-1', app ? 'flex min-h-0 flex-col' : 'px-4 pb-16 pt-8 md:px-8 md:pt-10')}\n >\n {app ? children : <div className=\"mx-auto w-full max-w-[72rem]\">{children}</div>}\n </main>\n </div>\n {app || footer === false ? null : (footer ?? <SiteFooter compact family legal />)}\n {tabs ? <TabBar items={tabs} /> : null}\n </div>\n </DensityProvider>\n );\n}\n","/*\n * Formatting for the things the family shows over and over: Bitcoin\n * explorer links, shortened ids, block heights, the time to a block. No\n * React, so server code and OG renderers import it too.\n */\n\nexport type Network = 'mainnet' | 'testnet' | 'testnet4' | 'signet';\nexport type ExplorerKind = 'address' | 'tx' | 'block';\n\nconst EXPLORER = 'https://mempool.space';\n\n/** A mempool.space link for an address, a transaction, or a block (by height or hash). */\nexport function explorerUrl(kind: ExplorerKind, value: string | number, network: Network = 'mainnet'): string {\n const net = network === 'mainnet' ? '' : `/${network}`;\n return `${EXPLORER}${net}/${kind}/${encodeURIComponent(String(value))}`;\n}\n\n/** head…tail, leaving short values whole. */\nexport function shortenMiddle(value: string, head = 8, tail = 6): string {\n return value.length <= head + tail + 1 ? value : `${value.slice(0, head)}…${value.slice(-tail)}`;\n}\n\n/** 971632 → \"971,632\". */\nexport function formatBlockHeight(height: number): string {\n return Math.trunc(height).toLocaleString('en-US');\n}\n\n/**\n * How long a number of blocks takes at ten minutes each, in the largest two\n * units: \"about 40 minutes\", \"about 2 days 13 hours\". Blocks come when they\n * come, so it is always \"about\".\n */\nexport function blocksToDuration(blocks: number, minutesPerBlock = 10): string {\n const minutes = Math.max(0, Math.round(blocks * minutesPerBlock));\n if (minutes < 60) return `about ${minutes} minute${minutes === 1 ? '' : 's'}`;\n const units: [number, string][] = [\n [60 * 24 * 7, 'week'],\n [60 * 24, 'day'],\n [60, 'hour'],\n [1, 'minute'],\n ];\n const parts: string[] = [];\n let rest = minutes;\n for (const [size, word] of units) {\n const n = Math.floor(rest / size);\n if (n > 0 && parts.length < 2) {\n parts.push(`${n} ${word}${n === 1 ? '' : 's'}`);\n rest -= n * size;\n } else if (parts.length > 0) break;\n }\n return `about ${parts.join(' ')}`;\n}\n\nconst MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\n\n/**\n * A moment in UTC, the same for every reader: \"20 Oct 2026, 09:14 UTC\".\n * Takes a Date, an ISO string, or unix seconds. `date: true` drops the time.\n */\nexport function formatUtc(at: Date | string | number, opts: { date?: boolean } = {}): string {\n const d = typeof at === 'number' ? new Date(at * 1000) : new Date(at);\n if (Number.isNaN(d.getTime())) return '—';\n const day = `${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]} ${d.getUTCFullYear()}`;\n if (opts.date) return day;\n const hh = String(d.getUTCHours()).padStart(2, '0');\n const mm = String(d.getUTCMinutes()).padStart(2, '0');\n return `${day}, ${hh}:${mm} UTC`;\n}\n\n/** 482113 → \"471 KB\"; 1288490 → \"1.2 MB\". Binary steps, the way a file dialog counts. */\nexport function formatBytes(bytes: number): string {\n if (!Number.isFinite(bytes) || bytes < 0) return '—';\n if (bytes < 1024) return `${bytes} B`;\n const units = ['KB', 'MB', 'GB', 'TB'];\n let v = bytes / 1024;\n let u = 0;\n while (v >= 1024 && u < units.length - 1) {\n v /= 1024;\n u++;\n }\n return `${v < 10 ? v.toFixed(1).replace(/\\.0$/, '') : Math.round(v)} ${units[u]}`;\n}\n","import { useOptionalOcSession, type OcAccount } from '@orangecheck/auth-client';\n\nimport { OCHK_APP_ACCOUNT, SessionAccountView, type SessionState } from '../chrome/tool-shell';\nimport type { AccountMenuItem } from '../chrome/app-shell';\nimport { shortenMiddle } from '../format';\n\n/*\n * The account control on any family site, read from @orangecheck/auth-client\n * (an optional peer: install it to use this entry). Inside an\n * OcSessionProvider it shows the account menu or \"Sign in\"; with no provider\n * there is nobody to be signed in as, so it offers sign-in.\n */\n\n/** The name an account chose to show, never a raw did:oc. */\nexport function accountName(account: Pick<OcAccount, 'displayName' | 'displayIdentity'>): string {\n if (account.displayName) return account.displayName;\n const { kind, value } = account.displayIdentity;\n if (kind === 'did' || value.startsWith('did:')) return 'Your account';\n if (kind === 'email') return value;\n // An address or npub: head and tail, short enough for a header chip.\n return shortenMiddle(value, 8, 6);\n}\n\nexport function SessionAccount({\n signInHref,\n signInPath,\n signInLabel,\n accountHref = OCHK_APP_ACCOUNT,\n items,\n nameFrom = 'lg',\n}: {\n /** Default: the session's sign-in URL on the auth host. */\n signInHref?: string;\n /** This site's own sign-in page (`/signin`): sign in here, in place, and return to the page the viewer is on. */\n signInPath?: string;\n signInLabel?: string;\n /** Where \"Your account\" goes. */\n accountHref?: string;\n /** This site's own items, above \"Your account\". */\n items?: (AccountMenuItem | 'separator')[];\n nameFrom?: 'sm' | 'lg';\n}) {\n const session = useOptionalOcSession();\n const status = session?.status ?? 'anonymous';\n const state: SessionState =\n status === 'authenticated' ? 'in' : status === 'anonymous' ? 'out' : status === 'error' ? 'unknown' : 'checking';\n const account = session?.account ?? null;\n const name = account ? accountName(account) : undefined;\n // A second line only when it adds something: the address behind a chosen name.\n const detail = account?.primaryBtc && account.displayName ? shortenMiddle(account.primaryBtc, 8, 6) : undefined;\n return (\n <SessionAccountView\n state={state === 'in' && !account ? 'checking' : state}\n name={name}\n detail={detail}\n items={items}\n signInHref={signInHref ?? (signInPath ? undefined : (session?.signInUrl ?? 'https://ochk.io/signin'))}\n signInPath={signInPath}\n signInLabel={signInLabel}\n accountHref={accountHref}\n onSignOut={() => void session?.signOut()}\n nameFrom={nameFrom}\n />\n );\n}\n\nexport { SessionAccountView };\nexport type { SessionState };\n"]}
1
+ {"version":3,"sources":["../src/lib/cn.ts","../src/primitives/feedback.tsx","../src/primitives/button.tsx","../src/primitives/overlay.tsx","../src/chrome/app-shell.tsx","../src/chrome/tool-shell.tsx","../src/format/index.ts","../src/session/index.tsx"],"names":["Separator","forwardRef","Button","jsx","jsxs","DialogContent","DialogTitle","DialogDescription","SheetContent","PopoverContent","TooltipContent","DropdownMenuContent","DropdownMenuItem","DropdownMenuLabel","DropdownMenuSeparator"],"mappings":"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","file":"session.mjs","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { extendTailwindMerge } from 'tailwind-merge';\n\n// tailwind-merge would treat `oca-display-1` and `text-foreground` as the same\n// group and drop one; teach it the package's own type utilities.\nconst merge = extendTailwindMerge<'oca-type'>({\n extend: {\n classGroups: {\n 'oca-type': [\n 'oca-display-1',\n 'oca-display-2',\n 'oca-display-3',\n 'oca-title',\n 'oca-title-sm',\n 'oca-eyebrow',\n 'oca-lead',\n 'oca-body',\n 'oca-numeral',\n ],\n },\n },\n});\n\nexport function cn(...inputs: ClassValue[]): string {\n return merge(clsx(inputs));\n}\n","import * as SeparatorPrimitive from '@radix-ui/react-separator';\nimport {\n forwardRef,\n useEffect,\n useState,\n type ComponentPropsWithoutRef,\n type CSSProperties,\n type ElementRef,\n type HTMLAttributes,\n} from 'react';\nimport { useTheme } from 'next-themes';\nimport { Toaster as SonnerToaster, type ToasterProps } from 'sonner';\n\nimport { cn } from '../lib/cn';\n\nexport function Spinner({ className, label }: { className?: string; label?: string }) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n role={label ? 'status' : undefined}\n aria-label={label}\n aria-hidden={label ? undefined : true}\n className={cn('size-4 animate-spin', className)}\n >\n <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"currentColor\" strokeOpacity=\"0.2\" strokeWidth=\"2.5\" />\n <path d=\"M21 12a9 9 0 0 0-9-9\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" />\n </svg>\n );\n}\n\n/** A placeholder bar, visible on the canvas and on a card in both modes. */\nexport function Skeleton({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n return (\n <div\n data-ui=\"skeleton\"\n className={cn('animate-pulse rounded-lg bg-[var(--oca-skeleton)]', className)}\n {...props}\n />\n );\n}\n\n/**\n * True once `active` has stayed true for `ms`. A loading state that never\n * ends reads as a broken page; pass this to show a way out instead.\n */\nexport function useTimedOut(active: boolean, ms = 12_000): boolean {\n const [timedOut, setTimedOut] = useState(false);\n useEffect(() => {\n if (!active) {\n setTimedOut(false);\n return;\n }\n const t = setTimeout(() => setTimedOut(true), ms);\n return () => clearTimeout(t);\n }, [active, ms]);\n return timedOut;\n}\n\nexport const Separator = forwardRef<\n ElementRef<typeof SeparatorPrimitive.Root>,\n ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(function Separator({ className, orientation = 'horizontal', decorative = true, ...props }, ref) {\n return (\n <SeparatorPrimitive.Root\n ref={ref}\n decorative={decorative}\n orientation={orientation}\n className={cn(\n 'shrink-0 bg-border',\n orientation === 'horizontal' ? 'h-px w-full' : 'h-full w-px',\n className\n )}\n {...props}\n />\n );\n});\n\nexport function Kbd({ className, ...props }: HTMLAttributes<HTMLElement>) {\n return (\n <kbd\n className={cn(\n 'inline-flex h-5 min-w-5 items-center justify-center rounded-md border border-border bg-card px-1.5 font-mono text-[11px] text-muted-foreground',\n className\n )}\n {...props}\n />\n );\n}\n\n/** Live-region status dot: a calm pulse for \"running\", solid otherwise. */\nexport function StatusDot({\n tone = 'neutral',\n pulse = false,\n className,\n}: {\n tone?: 'neutral' | 'success' | 'warning' | 'danger' | 'brand';\n pulse?: boolean;\n className?: string;\n}) {\n const color = {\n neutral: 'bg-oca-subtle',\n success: 'bg-success',\n warning: 'bg-warning',\n danger: 'bg-destructive',\n brand: 'bg-brand',\n }[tone];\n return (\n <span\n aria-hidden\n className={cn('inline-block size-2 shrink-0 rounded-full', color, pulse && 'oca-animate-pulse-dot', className)}\n />\n );\n}\n\n/** Toasts in the system's type and surfaces. Mount once. */\n/*\n * sonner paints a toast from its own variables (white, or black in dark), and\n * they outrank a class. Set to the tokens, a toast is the popover in both modes.\n */\nconst TOAST_COLOURS = {\n '--normal-bg': 'var(--popover)',\n '--normal-text': 'var(--popover-foreground)',\n '--normal-border': 'var(--border)',\n} as CSSProperties;\n\n/** Toasts in the page's mode: it reads next-themes, so a site passes nothing for dark. */\nexport function Toaster({ style, ...props }: ToasterProps) {\n const { resolvedTheme } = useTheme();\n return (\n <SonnerToaster\n theme={resolvedTheme === 'dark' ? 'dark' : resolvedTheme === 'light' ? 'light' : 'system'}\n style={{ ...TOAST_COLOURS, ...style }}\n position=\"bottom-right\"\n toastOptions={{\n classNames: {\n toast: 'oca-toast rounded-2xl border border-border bg-popover text-popover-foreground shadow-lg font-sans',\n title: 'text-[0.9375rem] font-medium',\n description: 'text-sm text-muted-foreground',\n actionButton: 'rounded-full bg-primary text-primary-foreground',\n cancelButton: 'rounded-full bg-muted text-foreground',\n },\n }}\n {...props}\n />\n );\n}\n\nexport { toast } from 'sonner';\n","import { Slot } from '@radix-ui/react-slot';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { forwardRef, type ButtonHTMLAttributes } from 'react';\n\nimport { cn } from '../lib/cn';\nimport { Spinner } from './feedback';\n\n/*\n * Pills, as the deck draws them: the black filled pill leads, the white\n * outlined pill follows. `brand` is for the orange band only; `onInk` and\n * `onInkOutline` are the same pair inverted for dark bands.\n */\nexport const buttonVariants = cva(\n [\n 'inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-full font-medium',\n 'transition-[background-color,border-color,color,box-shadow,transform] duration-150',\n 'outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',\n 'active:translate-y-px disabled:pointer-events-none',\n // Disabled is one look for every variant: a quiet grey pill, never a\n // faded black or orange slab that reads as broken.\n 'disabled:border disabled:border-border disabled:bg-muted disabled:text-oca-subtle disabled:shadow-none',\n '[&_svg]:pointer-events-none [&_svg]:size-[1.1em] [&_svg]:shrink-0',\n ],\n {\n variants: {\n variant: {\n primary: 'bg-primary text-primary-foreground hover:bg-primary/85',\n secondary:\n 'border border-border bg-secondary text-secondary-foreground hover:border-oca-hairline-strong hover:bg-accent',\n brand: 'bg-brand text-brand-foreground hover:bg-oca-orange-deep',\n ghost: 'text-foreground hover:bg-accent',\n danger: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',\n link: 'h-auto rounded-none px-0 text-foreground underline decoration-foreground/35 underline-offset-4 hover:decoration-foreground',\n onInk: 'bg-white text-oca-ink hover:bg-white/90',\n onInkOutline: 'border border-white/20 text-white hover:bg-white/10',\n },\n size: {\n sm: 'h-9 px-4 text-sm',\n md: 'h-11 px-5 text-[0.9375rem]',\n lg: 'h-14 px-7 text-[1.0625rem]',\n icon: 'size-10',\n 'icon-sm': 'size-8',\n },\n },\n compoundVariants: [{ variant: 'link', className: 'h-auto px-0' }],\n defaultVariants: { variant: 'primary', size: 'md' },\n }\n);\n\nexport interface ButtonProps\n extends ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n /** Render the child element (a Link, an <a>) with the button's styles. */\n asChild?: boolean;\n /** Shows a spinner and disables the button. */\n loading?: boolean;\n}\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n { className, variant, size, asChild = false, loading = false, disabled, children, type, ...props },\n ref\n) {\n const Comp = asChild ? Slot : 'button';\n return (\n <Comp\n ref={ref}\n data-ui=\"button\"\n className={cn(buttonVariants({ variant, size }), className)}\n disabled={asChild ? undefined : disabled || loading}\n aria-busy={loading || undefined}\n type={asChild ? undefined : (type ?? 'button')}\n {...props}\n >\n {asChild ? (\n children\n ) : (\n <>\n {loading ? <Spinner className=\"size-[1.1em]\" /> : null}\n {children}\n </>\n )}\n </Comp>\n );\n});\n","import * as DialogPrimitive from '@radix-ui/react-dialog';\nimport * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';\nimport * as PopoverPrimitive from '@radix-ui/react-popover';\nimport * as TooltipPrimitive from '@radix-ui/react-tooltip';\nimport { X } from 'lucide-react';\nimport { forwardRef, type ComponentPropsWithoutRef, type ElementRef, type HTMLAttributes } from 'react';\n\nimport { cn } from '../lib/cn';\n\n/* ── Dialog ───────────────────────────────────────────────────────────── */\n\nexport const Dialog = DialogPrimitive.Root;\nexport const DialogTrigger = DialogPrimitive.Trigger;\nexport const DialogClose = DialogPrimitive.Close;\n\nconst overlayClass =\n 'oca-animate-fade fixed inset-0 z-50 bg-[rgb(10_10_10/0.38)] backdrop-blur-[2px] data-[state=closed]:opacity-0';\n\nexport const DialogContent = forwardRef<\n ElementRef<typeof DialogPrimitive.Content>,\n ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { hideClose?: boolean }\n>(function DialogContent({ className, children, hideClose, ...props }, ref) {\n return (\n <DialogPrimitive.Portal>\n <DialogPrimitive.Overlay className={overlayClass} />\n <DialogPrimitive.Content\n ref={ref}\n className={cn(\n 'oca-animate-rise fixed left-1/2 top-1/2 z-50 w-[min(92vw,34rem)] -translate-x-1/2 -translate-y-1/2',\n 'max-h-[88vh] overflow-y-auto rounded-[1.5rem] border border-border bg-popover p-6 text-popover-foreground shadow-xl md:p-8',\n 'outline-none',\n className\n )}\n {...props}\n >\n {children}\n {hideClose ? null : (\n <DialogPrimitive.Close\n className=\"absolute right-4 top-4 inline-flex size-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground\"\n aria-label=\"Close\"\n >\n <X className=\"size-4\" />\n </DialogPrimitive.Close>\n )}\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n );\n});\n\nexport function DialogHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n return <div className={cn('mb-5 flex flex-col gap-2 pr-8', className)} {...props} />;\n}\n\nexport function DialogFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n return <div className={cn('mt-7 flex flex-col-reverse gap-3 sm:flex-row sm:justify-end', className)} {...props} />;\n}\n\nexport const DialogTitle = forwardRef<\n ElementRef<typeof DialogPrimitive.Title>,\n ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(function DialogTitle({ className, ...props }, ref) {\n return (\n <DialogPrimitive.Title\n ref={ref}\n className={cn('text-[1.5rem] font-normal leading-tight tracking-[-0.02em]', className)}\n {...props}\n />\n );\n});\n\nexport const DialogDescription = forwardRef<\n ElementRef<typeof DialogPrimitive.Description>,\n ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(function DialogDescription({ className, ...props }, ref) {\n return <DialogPrimitive.Description ref={ref} className={cn('oca-body', className)} {...props} />;\n});\n\n/* ── Sheet: a dialog from an edge ────────────────────────────────────── */\n\nexport const Sheet = DialogPrimitive.Root;\nexport const SheetTrigger = DialogPrimitive.Trigger;\nexport const SheetClose = DialogPrimitive.Close;\nexport const SheetTitle = DialogTitle;\nexport const SheetDescription = DialogDescription;\n\nconst sheetSide = {\n right: 'inset-y-0 right-0 h-full w-[min(88vw,24rem)] border-l',\n left: 'inset-y-0 left-0 h-full w-[min(88vw,24rem)] border-r',\n bottom: 'inset-x-0 bottom-0 max-h-[88vh] rounded-t-[1.5rem] border-t',\n} as const;\n\nexport const SheetContent = forwardRef<\n ElementRef<typeof DialogPrimitive.Content>,\n ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { side?: keyof typeof sheetSide }\n>(function SheetContent({ className, children, side = 'right', ...props }, ref) {\n return (\n <DialogPrimitive.Portal>\n <DialogPrimitive.Overlay className={overlayClass} />\n <DialogPrimitive.Content\n ref={ref}\n className={cn(\n 'oca-animate-fade fixed z-50 flex flex-col gap-4 overflow-y-auto border-border bg-popover p-6 text-popover-foreground shadow-xl outline-none',\n sheetSide[side],\n className\n )}\n {...props}\n >\n {children}\n <DialogPrimitive.Close\n className=\"absolute right-4 top-4 inline-flex size-9 items-center justify-center rounded-full text-muted-foreground hover:bg-accent hover:text-foreground\"\n aria-label=\"Close\"\n >\n <X className=\"size-4\" />\n </DialogPrimitive.Close>\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n );\n});\n\n/* ── Popover ──────────────────────────────────────────────────────────── */\n\nexport const Popover = PopoverPrimitive.Root;\nexport const PopoverTrigger = PopoverPrimitive.Trigger;\nexport const PopoverAnchor = PopoverPrimitive.Anchor;\n\nexport const PopoverContent = forwardRef<\n ElementRef<typeof PopoverPrimitive.Content>,\n ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(function PopoverContent({ className, align = 'center', sideOffset = 8, ...props }, ref) {\n return (\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n ref={ref}\n align={align}\n sideOffset={sideOffset}\n className={cn(\n 'oca-animate-fade z-50 w-72 rounded-2xl border border-border bg-popover p-4 text-popover-foreground shadow-lg outline-none',\n className\n )}\n {...props}\n />\n </PopoverPrimitive.Portal>\n );\n});\n\n/* ── Tooltip: ink, small, quick ───────────────────────────────────────── */\n\nexport const TooltipProvider = TooltipPrimitive.Provider;\nexport const Tooltip = TooltipPrimitive.Root;\nexport const TooltipTrigger = TooltipPrimitive.Trigger;\n\nexport const TooltipContent = forwardRef<\n ElementRef<typeof TooltipPrimitive.Content>,\n ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(function TooltipContent({ className, sideOffset = 6, ...props }, ref) {\n return (\n <TooltipPrimitive.Portal>\n <TooltipPrimitive.Content\n ref={ref}\n sideOffset={sideOffset}\n className={cn(\n 'oca-animate-fade z-50 max-w-xs rounded-lg bg-oca-ink px-2.5 py-1.5 text-[0.8125rem] leading-snug text-oca-ink-foreground',\n className\n )}\n {...props}\n />\n </TooltipPrimitive.Portal>\n );\n});\n\n/* ── Menu ─────────────────────────────────────────────────────────────── */\n\nexport const DropdownMenu = DropdownMenuPrimitive.Root;\nexport const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\nexport const DropdownMenuGroup = DropdownMenuPrimitive.Group;\n\nexport const DropdownMenuContent = forwardRef<\n ElementRef<typeof DropdownMenuPrimitive.Content>,\n ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(function DropdownMenuContent({ className, sideOffset = 8, ...props }, ref) {\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n ref={ref}\n sideOffset={sideOffset}\n className={cn(\n 'oca-animate-fade z-50 min-w-56 overflow-hidden rounded-2xl border border-border bg-popover p-1.5 text-popover-foreground shadow-lg',\n className\n )}\n {...props}\n />\n </DropdownMenuPrimitive.Portal>\n );\n});\n\nexport const DropdownMenuItem = forwardRef<\n ElementRef<typeof DropdownMenuPrimitive.Item>,\n ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { destructive?: boolean }\n>(function DropdownMenuItem({ className, destructive, ...props }, ref) {\n return (\n <DropdownMenuPrimitive.Item\n ref={ref}\n className={cn(\n 'flex h-10 cursor-default select-none items-center gap-2.5 rounded-xl px-3 text-[0.9375rem] outline-none',\n 'data-[highlighted]:bg-accent data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:text-muted-foreground',\n destructive && 'text-destructive [&_svg]:text-destructive',\n className\n )}\n {...props}\n />\n );\n});\n\nexport const DropdownMenuLabel = forwardRef<\n ElementRef<typeof DropdownMenuPrimitive.Label>,\n ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label>\n>(function DropdownMenuLabel({ className, ...props }, ref) {\n return (\n <DropdownMenuPrimitive.Label\n ref={ref}\n className={cn('px-3 pb-1.5 pt-2 text-[0.8125rem] text-muted-foreground', className)}\n {...props}\n />\n );\n});\n\nexport const DropdownMenuSeparator = forwardRef<\n ElementRef<typeof DropdownMenuPrimitive.Separator>,\n ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(function DropdownMenuSeparator({ className, ...props }, ref) {\n return <DropdownMenuPrimitive.Separator ref={ref} className={cn('my-1.5 h-px bg-border', className)} {...props} />;\n});\n","import { ChevronRight, Ellipsis, Menu, Moon, Sun, SunMoon } from 'lucide-react';\nimport Link from 'next/link';\nimport { useTheme } from 'next-themes';\nimport { useEffect, useMemo, useState, type ReactNode } from 'react';\n\nimport { Lockup } from '../brand/mark';\nimport type { FamilySlug } from '../family';\nimport { cn } from '../lib/cn';\nimport { ShellContext } from '../lib/shell-context';\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n Sheet,\n SheetContent,\n SheetTitle,\n SheetTrigger,\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from '../primitives/overlay';\nimport { FamilyMenu } from './family-menu';\n\nexport interface ShellNavItem {\n href: string;\n label: string;\n icon?: ReactNode;\n active?: boolean;\n /** Beside the label: a number shows as a count on a tab or the rail (99+), anything else as a dot. */\n badge?: ReactNode;\n}\n\nexport interface ShellNavGroup {\n /** Omit for the first, unlabeled group. */\n label?: string;\n items: ShellNavItem[];\n}\n\n/**\n * A part of the app with its own pages: \"You\", \"Build\", \"Operate\", \"Admin\".\n * The sidebar lists one section's pages at a time, under a switcher.\n */\nexport interface ShellSection {\n id: string;\n /** Short, for the switcher: one word. */\n label: string;\n /** The section's eyebrow on its pages; defaults to `label`. */\n title?: string;\n /** Where the switcher goes: the section's first page. */\n href: string;\n /** With one on every section, phones get the sections as tabs. */\n icon?: ReactNode;\n groups: ShellNavGroup[];\n}\n\nexport function SideNav({ groups, onNavigate }: { groups: ShellNavGroup[]; onNavigate?: () => void }) {\n return (\n <nav aria-label=\"Pages\" className=\"flex flex-col gap-5\">\n {groups.map((g, gi) => (\n <div key={g.label ?? gi} className=\"flex flex-col gap-0.5\">\n {g.label ? <p className=\"px-3 pb-1 text-[0.8125rem] text-muted-foreground\">{g.label}</p> : null}\n {g.items.map((it) => (\n <Link\n key={it.href}\n href={it.href}\n onClick={onNavigate}\n aria-current={it.active ? 'page' : undefined}\n className={cn(\n 'flex h-9 items-center gap-3 rounded-xl px-3 text-[0.9375rem] transition-colors',\n '[&_svg]:size-[1.0625rem] [&_svg]:shrink-0',\n it.active\n ? 'bg-card text-foreground shadow-sm ring-1 ring-border [&_svg]:text-foreground'\n : 'text-muted-foreground hover:bg-accent hover:text-foreground'\n )}\n >\n {it.icon}\n <span className=\"min-w-0 flex-1 truncate\">{it.label}</span>\n {it.badge ? <span className=\"shrink-0\">{it.badge}</span> : null}\n </Link>\n ))}\n </div>\n ))}\n </nav>\n );\n}\n\nfunction SectionSwitch({\n sections,\n current,\n onNavigate,\n}: {\n sections: ShellSection[];\n current: ShellSection;\n onNavigate?: () => void;\n}) {\n // Up to three fit one row of the sidebar; four go two by two, so no label\n // is ever cut short.\n const cols = sections.length <= 3 ? sections.length : 2;\n return (\n <nav\n aria-label=\"Sections\"\n className={cn('grid gap-1 bg-muted p-1', cols === sections.length ? 'rounded-full' : 'rounded-[1.25rem]')}\n style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}\n >\n {sections.map((sec) => {\n const on = sec.id === current.id;\n return (\n <Link\n key={sec.id}\n href={sec.href}\n onClick={onNavigate}\n aria-current={on ? 'true' : undefined}\n className={cn(\n 'flex h-8 min-w-0 items-center justify-center rounded-full px-1.5 text-[0.8125rem] font-medium transition-colors',\n on ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'\n )}\n >\n <span className=\"truncate\">{sec.label}</span>\n </Link>\n );\n })}\n </nav>\n );\n}\n\n/** A count (99+) for a number, a dot for anything else; the badge is read with the label. */\nfunction NavBadge({ badge, className }: { badge: ReactNode; className: string }) {\n if (typeof badge === 'number') {\n return (\n <span\n aria-hidden\n className={cn(\n 'absolute flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[0.625rem] font-medium leading-none text-primary-foreground ring-2 ring-background oca-tabular',\n className\n )}\n >\n {badge > 99 ? '99+' : badge}\n </span>\n );\n }\n // The deep orange: a dot is the whole signal, so it must clear 3:1 on the background.\n return <span aria-hidden className={cn('absolute size-2 rounded-full bg-oca-orange-deep ring-2 ring-background', className)} />;\n}\n\nconst TAB =\n 'flex min-w-0 flex-col items-center justify-center gap-1 rounded-2xl text-[0.6875rem] leading-none outline-none transition-colors [-webkit-tap-highlight-color:transparent] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring';\n\nfunction TabFace({ icon, label, badge, active }: { icon: ReactNode; label: string; badge?: ReactNode; active?: boolean }) {\n return (\n <>\n <span\n className={cn(\n 'relative flex h-7 w-12 items-center justify-center rounded-full transition-[background-color,box-shadow] duration-200',\n '[&_svg]:size-[1.125rem] [&_svg]:shrink-0',\n active ? 'bg-card shadow-sm ring-1 ring-border [&_svg]:stroke-2' : '[&_svg]:stroke-[1.75]'\n )}\n >\n {icon}\n {badge ? <NavBadge badge={badge} className={typeof badge === 'number' ? '-top-1 right-0.5' : 'right-2.5 top-0.5'} /> : null}\n </span>\n <span className=\"max-w-full truncate px-0.5\">{label}</span>\n {badge ? <span className=\"sr-only\">{badge}</span> : null}\n </>\n );\n}\n\n/**\n * The phone's way around: up to five pages under the thumb, each one tap\n * away. Past five, four tabs and More, a sheet with the rest.\n */\nexport function TabBar({ items }: { items: ShellNavItem[] }) {\n const [more, setMore] = useState(false);\n const overflow = items.length > 5;\n const shown = overflow ? items.slice(0, 4) : items;\n const rest = overflow ? items.slice(4) : [];\n const restBadge = rest.find((it) => it.badge)?.badge;\n return (\n <nav\n aria-label=\"Pages\"\n data-oca-tabbar=\"\"\n className=\"fixed inset-x-0 bottom-0 z-40 border-t border-border bg-background/90 pb-[env(safe-area-inset-bottom)] backdrop-blur-md md:hidden\"\n >\n <div\n className=\"mx-auto grid h-14 max-w-[32rem] px-1.5\"\n style={{ gridTemplateColumns: `repeat(${shown.length + (overflow ? 1 : 0)}, minmax(0, 1fr))` }}\n >\n {shown.map((it) => (\n <Link\n key={it.href}\n href={it.href}\n aria-current={it.active ? 'page' : undefined}\n className={cn(TAB, it.active ? 'text-foreground' : 'text-muted-foreground hover:text-foreground')}\n >\n <TabFace icon={it.icon} label={it.label} badge={it.badge} active={it.active} />\n </Link>\n ))}\n {overflow ? (\n <Sheet open={more} onOpenChange={setMore}>\n <SheetTrigger\n className={cn(TAB, rest.some((it) => it.active) ? 'text-foreground' : 'text-muted-foreground hover:text-foreground')}\n >\n <TabFace icon={<Ellipsis />} label=\"More\" badge={restBadge ? true : undefined} active={rest.some((it) => it.active)} />\n </SheetTrigger>\n <SheetContent side=\"bottom\" className=\"pb-[calc(1.5rem+env(safe-area-inset-bottom))]\">\n <SheetTitle className=\"px-1 text-[1.0625rem] font-medium\">More</SheetTitle>\n <div className=\"mt-4 grid grid-cols-3 gap-2\">\n {rest.map((it) => (\n <Link\n key={it.href}\n href={it.href}\n onClick={() => setMore(false)}\n aria-current={it.active ? 'page' : undefined}\n className={cn(\n 'flex min-h-20 flex-col items-center justify-center gap-2 rounded-2xl border px-2 text-center text-[0.8125rem] transition-colors',\n '[&_svg]:size-5',\n it.active ? 'border-foreground bg-card text-foreground' : 'border-border text-muted-foreground hover:text-foreground'\n )}\n >\n <span className=\"relative\">\n {it.icon}\n {it.badge ? <NavBadge badge={it.badge} className={typeof it.badge === 'number' ? '-right-3 -top-2' : '-right-1 -top-0.5'} /> : null}\n </span>\n <span className=\"max-w-full truncate\">{it.label}</span>\n {it.badge ? <span className=\"sr-only\">{it.badge}</span> : null}\n </Link>\n ))}\n </div>\n </SheetContent>\n </Sheet>\n ) : null}\n </div>\n </nav>\n );\n}\n\n/** Two or more pages, each with an icon, take a tab bar (past five, with More). Without icons, the drawer. */\nexport function tabsFor(groups: ShellNavGroup[]): ShellNavItem[] | null {\n const items = groups.flatMap((g) => g.items);\n return items.length >= 2 && items.every((it) => it.icon) ? items : null;\n}\n\n/** Sections with an icon each are the phone's tabs; their pages stay in the menu. */\nfunction sectionTabs(sections: ShellSection[] | undefined, current: ShellSection | undefined): ShellNavItem[] | null {\n if (!sections || !current || sections.length < 2 || !sections.every((sec) => sec.icon)) return null;\n return sections.map((sec) => ({ href: sec.href, label: sec.label, icon: sec.icon, active: sec.id === current.id }));\n}\n\nconst activeItem = (groups: ShellNavGroup[]) => groups.flatMap((g) => g.items).find((it) => it.active);\n\n// Bundlers replace process.env.NODE_ENV; the typeof guard covers one that doesn't.\ndeclare const process: { env: { NODE_ENV?: string } } | undefined;\n\nconst warned = new Set<string>();\n\nfunction warnDuplicateLabels(groups: ShellNavGroup[]) {\n if (typeof process === 'undefined' || process.env.NODE_ENV === 'production') return;\n const seen = new Set<string>();\n for (const it of groups.flatMap((g) => g.items)) {\n if (seen.has(it.label) && !warned.has(it.label)) {\n warned.add(it.label);\n console.warn(`AppShell: two pages are labelled \"${it.label}\"; give each its own name.`);\n }\n seen.add(it.label);\n }\n}\n\n/**\n * The signed-in frame: a quiet sidebar on the canvas, content on the right.\n *\n * Give it `sections` and the sidebar shows a switcher (only when there is more\n * than one) and the current section's pages, so it never outgrows the screen;\n * `persistent` groups (Account) follow every section. The section is the one\n * holding an active item, or `activeSection`. On phones a top bar carries the\n * wordmark and the account, a strip under it names the section and page, and\n * the sidebar opens as a sheet. `nav` (flat groups) still works for a simple\n * app, and when every page has an icon, phones get a tab bar instead (past\n * five, four tabs and More): the pages sit under the thumb, the strip and the\n * menu button go (the sheet stays for a `footer`, without the pages), and\n * tablets keep the sheet. Sections with an icon each become the tabs, and\n * their pages stay in the sheet. Pages get the section through context:\n * PageHeader uses it as its eyebrow. `family` puts the family menu beside the\n * lockup. The account control and any banner are slots. `variant=\"rail\"` is\n * the messenger's frame instead.\n */\nexport function AppShell(props: AppShellProps) {\n return props.variant === 'rail' ? <RailShell {...props} /> : <SidebarShell {...props} />;\n}\n\nexport interface AppShellProps {\n /**\n * `sidebar` (default): a sidebar of pages from lg, content up to 72rem.\n * `rail`: a messenger's frame. The whole window, an icon rail from md\n * with each page's name on hover, the content full-bleed and managing\n * its own panes and scroll; `header` sits above it.\n */\n variant?: 'sidebar' | 'rail';\n /** Flat groups, for an app without sections. */\n nav?: ShellNavGroup[];\n sections?: ShellSection[];\n activeSection?: string;\n /** The page's name for the place strip, when it is not a nav item of its own. */\n page?: string;\n /** Groups shown under every section, after its own. */\n persistent?: ShellNavGroup[];\n home?: string;\n /** Mount the family menu beside the lockup, as SiteHeader does. Pass the site's slug to mark it. */\n family?: boolean | FamilySlug;\n /** The rail's: at its foot from md (an avatar linking to settings). The sidebar's: top right. */\n account?: ReactNode;\n banner?: ReactNode;\n footer?: ReactNode;\n /** `rail` only: the header above the rail and the content (a SiteHeader). */\n header?: ReactNode;\n children: ReactNode;\n className?: string;\n}\n\nfunction SidebarShell({\n nav,\n sections,\n activeSection,\n page: pageOverride,\n persistent = [],\n home = '/',\n family,\n account,\n banner,\n footer,\n children,\n className,\n}: AppShellProps) {\n const [open, setOpen] = useState(false);\n const current =\n sections && sections.length > 0\n ? (sections.find((sec) => sec.id === activeSection) ??\n sections.find((sec) => activeItem(sec.groups)) ??\n sections[0]!)\n : undefined;\n const groups = current ? [...current.groups, ...persistent] : [...(nav ?? []), ...persistent];\n warnDuplicateLabels(groups);\n const sectionTabsFor = sectionTabs(sections, current);\n const tabs = current ? sectionTabsFor : tabsFor(groups);\n const page = activeItem(groups);\n const place = useMemo(\n () => ({\n section: current ? (current.title ?? current.label) : undefined,\n page: pageOverride ?? page?.label,\n }),\n [current, page?.label, pageOverride]\n );\n const familyMenu = family ? <FamilyMenu current={typeof family === 'string' ? family : undefined} /> : null;\n const switcher =\n sections && current && sections.length > 1 ? (\n <SectionSwitch sections={sections} current={current} onNavigate={() => setOpen(false)} />\n ) : null;\n return (\n <ShellContext.Provider value={place}>\n <div\n className={cn(\n 'min-h-dvh bg-background',\n tabs && 'max-md:pb-[calc(3.5rem_+_env(safe-area-inset-bottom))]',\n className\n )}\n >\n <div className=\"sticky top-0 z-40 border-b border-border bg-background/90 backdrop-blur-md lg:hidden\">\n <div className=\"flex h-14 items-center gap-2 px-3\">\n <Sheet open={open} onOpenChange={setOpen}>\n <SheetTrigger\n className={cn(\n 'inline-flex size-10 items-center justify-center rounded-full hover:bg-accent',\n // The tab bar holds every page; the sheet is left for a footer. Section tabs leave the pages here.\n tabs && !sectionTabsFor && !footer && 'max-md:hidden'\n )}\n aria-label=\"Open navigation\"\n >\n <Menu className=\"size-5\" />\n </SheetTrigger>\n <SheetContent side=\"left\" className=\"flex flex-col gap-6 overflow-y-auto p-4 pt-16\">\n <SheetTitle className=\"sr-only\">Navigation</SheetTitle>\n {/* On a phone the section tabs are the switcher. */}\n {switcher ? <div className={sectionTabsFor ? 'max-md:hidden' : undefined}>{switcher}</div> : null}\n <div className={tabs && !sectionTabsFor ? 'max-md:hidden' : undefined}>\n <SideNav groups={groups} onNavigate={() => setOpen(false)} />\n </div>\n {footer ? <div className=\"mt-auto flex flex-col gap-3\">{footer}</div> : null}\n </SheetContent>\n </Sheet>\n <Link href={home} aria-label=\"Home\" className=\"rounded-lg p-1\">\n <Lockup size={22} />\n </Link>\n {familyMenu}\n <div className=\"ml-auto flex items-center\">{account}</div>\n </div>\n {place.page ? (\n <div\n className={cn(\n 'flex h-9 items-center gap-1.5 border-t border-border px-4 text-[0.875rem]',\n // The active tab already names the page.\n tabs && !sectionTabsFor && !pageOverride && 'max-md:hidden'\n )}\n >\n {place.section ? (\n <>\n <span className=\"shrink-0 text-muted-foreground\">{place.section}</span>\n <ChevronRight aria-hidden className=\"size-3.5 shrink-0 text-muted-foreground\" />\n </>\n ) : null}\n <span className=\"truncate font-medium\">{place.page}</span>\n </div>\n ) : null}\n </div>\n <div className=\"mx-auto flex w-full max-w-[90rem]\">\n <aside className=\"sticky top-0 hidden h-dvh w-[17rem] shrink-0 flex-col gap-6 overflow-y-auto px-4 py-6 lg:flex\">\n <div className=\"flex items-center gap-1\">\n <Link href={home} className=\"px-2\" aria-label=\"Home\">\n <Lockup size={26} />\n </Link>\n {familyMenu}\n </div>\n {switcher}\n <SideNav groups={groups} />\n <div className=\"mt-auto flex flex-col gap-3\">{footer}</div>\n </aside>\n <div className=\"flex min-w-0 flex-1 flex-col\">\n <div className=\"hidden h-[4.5rem] items-center justify-end gap-3 px-8 lg:flex\">{account}</div>\n {banner ? <div className=\"px-4 pb-2 pt-4 md:px-8 lg:pt-0\">{banner}</div> : null}\n <main id=\"main-content\" className=\"flex-1 px-4 pb-16 pt-6 md:px-8 lg:pt-2\">\n {/* One width for every signed-in page. */}\n <div className=\"mx-auto w-full max-w-[72rem]\">{children}</div>\n </main>\n </div>\n </div>\n {tabs ? <TabBar items={tabs} /> : null}\n </div>\n </ShellContext.Provider>\n );\n}\n\nfunction RailItem({ item }: { item: ShellNavItem }) {\n return (\n <Tooltip>\n <TooltipTrigger asChild>\n <Link\n href={item.href}\n aria-current={item.active ? 'page' : undefined}\n className={cn(\n 'relative flex size-10 items-center justify-center rounded-xl outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring',\n '[&_svg]:size-5 [&_svg]:shrink-0',\n item.active\n ? 'bg-card text-foreground shadow-sm ring-1 ring-border'\n : 'text-muted-foreground hover:bg-accent hover:text-foreground'\n )}\n >\n {item.icon}\n {/* The page's name, and its badge, for a screen reader; the tooltip shows the name. */}\n <span className=\"sr-only\">\n {item.label}\n {item.badge ? <>, {item.badge}</> : null}\n </span>\n {item.badge ? <NavBadge badge={item.badge} className={typeof item.badge === 'number' ? '-right-1 -top-1' : 'right-1 top-1'} /> : null}\n </Link>\n </TooltipTrigger>\n <TooltipContent side=\"right\">{item.label}</TooltipContent>\n </Tooltip>\n );\n}\n\n/** The rail frame: header, an icon rail from md, the content full-bleed, the tab bar on a phone. */\nfunction RailShell({ nav = [], persistent = [], page: pageOverride, header, account, banner, children, className }: AppShellProps) {\n const main = nav.flatMap((g) => g.items);\n const foot = persistent.flatMap((g) => g.items);\n warnDuplicateLabels([...nav, ...persistent]);\n const tabs = tabsFor([{ items: [...main, ...foot] }]);\n const active = activeItem([...nav, ...persistent]);\n const place = useMemo(() => ({ page: pageOverride ?? active?.label }), [pageOverride, active?.label]);\n return (\n <ShellContext.Provider value={place}>\n <TooltipProvider delayDuration={300}>\n <div className={cn('flex h-dvh flex-col overflow-hidden bg-background', className)}>\n {header}\n {banner}\n <div className=\"flex min-h-0 flex-1\">\n <nav aria-label=\"Pages\" className=\"hidden w-14 shrink-0 flex-col items-center gap-1 border-r border-border py-3 md:flex\">\n {main.map((it) => (\n <RailItem key={it.href} item={it} />\n ))}\n <div className=\"mt-auto flex flex-col items-center gap-1\">\n {foot.map((it) => (\n <RailItem key={it.href} item={it} />\n ))}\n {account}\n </div>\n </nav>\n <main\n id=\"main-content\"\n className={cn('flex min-h-0 min-w-0 flex-1 flex-col', tabs && 'max-md:pb-[calc(3.5rem_+_env(safe-area-inset-bottom))]')}\n >\n {children}\n </main>\n </div>\n {tabs ? <TabBar items={tabs} /> : null}\n </div>\n </TooltipProvider>\n </ShellContext.Provider>\n );\n}\n\nexport interface AccountMenuItem {\n label: string;\n href?: string;\n onSelect?: () => void;\n icon?: ReactNode;\n destructive?: boolean;\n}\n\n/** The account button: initial + short id, a menu of items. */\nexport function AccountMenu({\n name,\n detail,\n items,\n nameFrom = 'sm',\n}: {\n name: string;\n /** A second line: the short did, an address. */\n detail?: string;\n items: (AccountMenuItem | 'separator')[];\n /** The width from which the name shows beside the initial. A header with\n * navigation beside it needs the room until lg. */\n nameFrom?: 'sm' | 'lg';\n}) {\n const initial = name.trim().charAt(0).toUpperCase() || '·';\n return (\n <DropdownMenu>\n <DropdownMenuTrigger\n className={cn(\n 'inline-flex h-10 items-center gap-2.5 rounded-full border border-border bg-card pl-1 pr-1 text-[0.9375rem] outline-none transition-colors hover:border-oca-hairline-strong focus-visible:ring-2 focus-visible:ring-ring',\n nameFrom === 'lg' ? 'lg:pr-3.5' : 'sm:pr-3.5'\n )}\n aria-label={`${name}, account menu`}\n >\n {/* The initial is drawn, not written: the button's text is the name it is read by. */}\n <span\n aria-hidden\n data-initial={initial}\n className=\"flex size-8 items-center justify-center rounded-full bg-foreground text-[0.8125rem] font-medium text-background before:content-[attr(data-initial)]\"\n />\n <span className={cn('max-w-[10rem] truncate', nameFrom === 'lg' ? 'max-lg:hidden' : 'max-sm:hidden')}>\n {name}\n </span>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-64\">\n <DropdownMenuLabel className=\"flex flex-col gap-0.5\">\n <span className=\"truncate text-[0.9375rem] font-medium text-foreground\">{name}</span>\n {detail ? <span className=\"truncate font-mono text-[0.75rem]\">{detail}</span> : null}\n </DropdownMenuLabel>\n <DropdownMenuSeparator />\n {items.map((it, i) =>\n it === 'separator' ? (\n <DropdownMenuSeparator key={`sep-${i}`} />\n ) : it.href ? (\n <DropdownMenuItem key={it.label} asChild destructive={it.destructive}>\n <Link href={it.href}>\n {it.icon}\n {it.label}\n </Link>\n </DropdownMenuItem>\n ) : (\n <DropdownMenuItem key={it.label} onSelect={it.onSelect} destructive={it.destructive}>\n {it.icon}\n {it.label}\n </DropdownMenuItem>\n )\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n\n/** Light, dark, or follow the system: one button that cycles. */\nexport function ThemeToggle({ className }: { className?: string }) {\n const { theme, setTheme } = useTheme();\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n const current = mounted ? (theme ?? 'system') : 'system';\n const next = current === 'light' ? 'dark' : current === 'dark' ? 'system' : 'light';\n const Icon = current === 'light' ? Sun : current === 'dark' ? Moon : SunMoon;\n return (\n <button\n type=\"button\"\n onClick={() => setTheme(next)}\n aria-label={`Theme: ${current}. Switch to ${next}`}\n title={`Theme: ${current}`}\n className={cn(\n 'inline-flex size-10 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground',\n className\n )}\n >\n <Icon className=\"size-[1.125rem]\" />\n </button>\n );\n}\n","import { ArrowUpRight, CircleUser, LogOut } from 'lucide-react';\nimport { useRouter } from 'next/compat/router';\nimport type { NextRouter } from 'next/router';\nimport { useSyncExternalStore, type ReactNode } from 'react';\n\nimport type { ToolItem } from '../components/tools';\nimport { protocolSite, type ProtocolSlug } from '../family';\nimport { cn } from '../lib/cn';\nimport { DensityProvider } from '../lib/density';\nimport { SiteHomeContext } from '../lib/site-home';\nimport { Button } from '../primitives/button';\nimport { StatusBadge } from '../primitives/status-badge';\nimport { AccountMenu, SideNav, TabBar, tabsFor, ThemeToggle, type AccountMenuItem } from './app-shell';\nimport { SiteFooter, SiteHeader, SkipLink } from './site';\n\n/* ── The account slot ────────────────────────────────────────────────── */\n\n/** Where the session stands: being checked, signed in, signed out, or the check failed. */\nexport type SessionState = 'checking' | 'in' | 'out' | 'unknown';\n\nexport const OCHK_APP_ACCOUNT = 'https://ochk.app/me/account';\n\n/** Back and forward; a router move re-renders the page, which reads the address again. */\nfunction onHistory(change: () => void) {\n window.addEventListener('popstate', change);\n return () => window.removeEventListener('popstate', change);\n}\n\n/** The page's path and query, never the fragment. Anything the URL parser would read as another host returns home. */\nfunction herePath(): string {\n const { href, origin, pathname, search } = window.location;\n const path = pathname + search;\n return new URL(path, href).origin === origin ? path : '/';\n}\n\n/** The route the server renders, with the base path; none for a dynamic route, whose template is no address. */\nfunction routePath(router: NextRouter | null): string | null {\n if (!router || router.pathname.includes('[')) return null;\n return router.basePath && router.pathname === '/' ? router.basePath : router.basePath + router.pathname;\n}\n\n/**\n * This site's sign-in page, set to return to the page the viewer is on:\n * `/signin?return_to=%2Fverify%3Fid%3D…`. The server renders the route\n * without its query, so hydration matches; the fragment is never read, so a\n * share link's secret stays off servers. Pass no path for none.\n */\nexport function useSignInHref(path?: string): string | undefined {\n const router = useRouter();\n const here = useSyncExternalStore(onHistory, herePath, () => routePath(router));\n if (!path) return undefined;\n return here ? `${path}?return_to=${encodeURIComponent(here)}` : path;\n}\n\n/**\n * The header's account control, for any session source: the account menu\n * when signed in, \"Sign in\" when not. While the check runs, the button holds\n * its place unseen, so the header never shifts and a signed-in person never\n * sees \"Sign in\" flash. A failed check offers the account page, which works\n * either way. SessionAccount (./session) drives it from auth-client.\n */\nexport function SessionAccountView({\n state,\n name,\n detail,\n items = [],\n signInHref,\n signInPath,\n signInLabel = 'Sign in',\n accountHref = OCHK_APP_ACCOUNT,\n onSignOut,\n nameFrom = 'lg',\n}: {\n state: SessionState;\n /** The account's name, as it chose to be shown. */\n name?: string;\n /** A second line: a short address. */\n detail?: string;\n /** This site's own items, above \"Your account\". */\n items?: (AccountMenuItem | 'separator')[];\n /** Where \"Sign in\" goes, as given. */\n signInHref?: string;\n /** This site's sign-in page (`/signin`): \"Sign in\" returns to the page the viewer is on. */\n signInPath?: string;\n signInLabel?: string;\n /** Where \"Your account\" goes: ochk.app. */\n accountHref?: string;\n onSignOut?: () => void;\n nameFrom?: 'sm' | 'lg';\n}) {\n const inPlace = useSignInHref(signInPath);\n if (state === 'in') {\n return (\n <AccountMenu\n name={name || 'Your account'}\n detail={detail}\n nameFrom={nameFrom}\n items={[\n ...items,\n ...(items.length > 0 ? (['separator'] as const) : []),\n { label: 'Your account', href: accountHref, icon: <CircleUser /> },\n 'separator',\n { label: 'Sign out', onSelect: onSignOut, icon: <LogOut /> },\n ]}\n />\n );\n }\n if (state === 'unknown') {\n return (\n <Button asChild size=\"sm\" variant=\"secondary\">\n <a href={accountHref}>Your account</a>\n </Button>\n );\n }\n return (\n // No box of its own: it sits exactly where the signed-in chip will. While checking it is only unseen.\n <span className={state === 'checking' ? 'contents invisible' : 'contents'} aria-hidden={state === 'checking' || undefined}>\n <Button asChild size=\"sm\" variant=\"secondary\" tabIndex={state === 'checking' ? -1 : undefined}>\n <a href={signInHref ?? inPlace ?? 'https://ochk.io/signin'}>{signInLabel}</a>\n </Button>\n </span>\n );\n}\n\n/* ── The shell ───────────────────────────────────────────────────────── */\n\nexport interface ToolShellProps {\n /** The protocol these tools serve. Its word, its page on ochk.io and its status come from the registry. */\n protocol: ProtocolSlug;\n /** create · verify · discover · playground, in the family's words; give each an icon for the phone's tab bar. */\n tools: readonly ToolItem[];\n /** Where the lockup and the protocol's word go. Default: the protocol's page on ochk.io. */\n home?: string;\n /** The account control: SessionAccount, or SessionAccountView. */\n account?: ReactNode;\n /** List the tools in a rail on the left from lg (a dashboard page), not in the header. */\n rail?: boolean;\n /** Under the tools in the rail: Docs, Spec. */\n railFooter?: ReactNode;\n /**\n * `tool`: pages that scroll, with a footer. `app`: the full window, no\n * footer, the page managing its own scroll (an inbox, a workspace).\n */\n variant?: 'tool' | 'app';\n /** A SiteBanner above the header. */\n banner?: ReactNode;\n /** Replace the footer, or `false` for none. Default: the compact family footer. */\n footer?: ReactNode | false;\n children: ReactNode;\n}\n\n/**\n * The frame for a protocol's tools: a slim full-width header (orangecheck ·\n * stamp, linking the protocol's page on ochk.io; the family menu, the tools,\n * \"About stamp\", the status when it is not live, the account, the theme), the\n * page, and the compact family footer. Everything inside is set at compact density. On a phone the\n * tools move to a tab bar under the thumb when there are two to five, each\n * with an icon.\n */\nexport function ToolShell({\n protocol,\n tools,\n home,\n account,\n rail = false,\n railFooter,\n variant = 'tool',\n banner,\n footer,\n children,\n}: ToolShellProps) {\n const p = protocolSite(protocol);\n const homeHref = home ?? p.pageHref;\n const app = variant === 'app';\n const tabs = app ? null : tabsFor([{ items: [...tools] }]);\n const about = (\n <a\n href={p.pageHref}\n className=\"inline-flex items-center gap-1 rounded-full px-3 py-2 text-[0.9375rem] text-muted-foreground transition-colors hover:text-foreground\"\n >\n About {p.label}\n <ArrowUpRight aria-hidden className=\"size-3.5\" />\n </a>\n );\n return (\n <DensityProvider density=\"compact\">\n <div\n className={cn(\n 'flex flex-col bg-background',\n app ? 'h-dvh overflow-hidden' : 'min-h-dvh',\n tabs && 'max-md:pb-[calc(3.5rem_+_env(safe-area-inset-bottom))]'\n )}\n >\n <SkipLink />\n {banner}\n <SiteHeader\n variant=\"tool\"\n home={homeHref}\n product={p.label}\n family={protocol}\n nav={tools.map((t) => ({ href: t.href, label: t.label, active: t.active }))}\n navOnPhone={!tabs}\n menu={<div className=\"[&>a]:px-3 [&>a]:py-3 [&>a]:text-[1.0625rem]\">{about}</div>}\n // With a rail, the tools leave the header from lg.\n className={cn(rail && 'lg:[&_nav[aria-label=Main]]:hidden')}\n actions={\n <>\n <span className=\"hidden lg:contents\">{about}</span>\n {p.status !== 'live' ? <StatusBadge status={p.status} className=\"max-sm:hidden\" /> : null}\n <ThemeToggle className=\"max-sm:hidden\" />\n {account}\n </>\n }\n />\n <div className={cn('mx-auto flex w-full', app ? 'min-h-0 flex-1' : 'max-w-[90rem] flex-1')}>\n {rail ? (\n <aside className=\"sticky top-14 hidden h-[calc(100dvh-3.5rem)] w-60 shrink-0 flex-col gap-6 overflow-y-auto px-4 py-6 lg:flex\">\n <SideNav groups={[{ label: 'Tools', items: tools.map(({ href, label, icon, active }) => ({ href, label, icon, active })) }]} />\n {railFooter ? (\n <div className=\"mt-auto flex flex-col gap-0.5 px-3 text-[0.875rem] text-muted-foreground [&_a:hover]:text-foreground [&_a]:py-1.5 [&_a]:transition-colors\">\n {railFooter}\n </div>\n ) : null}\n </aside>\n ) : null}\n <main\n id=\"main-content\"\n className={cn('min-w-0 flex-1', app ? 'flex min-h-0 flex-col' : 'px-4 pb-16 pt-8 md:px-8 md:pt-10')}\n >\n {/* An ErrorPage inside goes home to the protocol's page, as the lockup does. */}\n <SiteHomeContext.Provider value={homeHref}>\n {app ? children : <div className=\"mx-auto w-full max-w-[72rem]\">{children}</div>}\n </SiteHomeContext.Provider>\n </main>\n </div>\n {app || footer === false ? null : (footer ?? <SiteFooter compact family legal />)}\n {tabs ? <TabBar items={tabs} /> : null}\n </div>\n </DensityProvider>\n );\n}\n","/*\n * Formatting for the things the family shows over and over: Bitcoin\n * explorer links, shortened ids, block heights, the time to a block. No\n * React, so server code and OG renderers import it too.\n */\n\nexport type Network = 'mainnet' | 'testnet' | 'testnet4' | 'signet';\nexport type ExplorerKind = 'address' | 'tx' | 'block';\n\nconst EXPLORER = 'https://mempool.space';\n\n/** A mempool.space link for an address, a transaction, or a block (by height or hash). */\nexport function explorerUrl(kind: ExplorerKind, value: string | number, network: Network = 'mainnet'): string {\n const net = network === 'mainnet' ? '' : `/${network}`;\n return `${EXPLORER}${net}/${kind}/${encodeURIComponent(String(value))}`;\n}\n\n/** head…tail, leaving short values whole. */\nexport function shortenMiddle(value: string, head = 8, tail = 6): string {\n return value.length <= head + tail + 1 ? value : `${value.slice(0, head)}…${value.slice(-tail)}`;\n}\n\n/** 971632 → \"971,632\". */\nexport function formatBlockHeight(height: number): string {\n return Math.trunc(height).toLocaleString('en-US');\n}\n\n/**\n * How long a number of blocks takes at ten minutes each, in the largest two\n * units: \"about 40 minutes\", \"about 2 days 13 hours\". Blocks come when they\n * come, so it is always \"about\".\n */\nexport function blocksToDuration(blocks: number, minutesPerBlock = 10): string {\n const minutes = Math.max(0, Math.round(blocks * minutesPerBlock));\n if (minutes < 60) return `about ${minutes} minute${minutes === 1 ? '' : 's'}`;\n const units: [number, string][] = [\n [60 * 24 * 7, 'week'],\n [60 * 24, 'day'],\n [60, 'hour'],\n [1, 'minute'],\n ];\n const parts: string[] = [];\n let rest = minutes;\n for (const [size, word] of units) {\n const n = Math.floor(rest / size);\n if (n > 0 && parts.length < 2) {\n parts.push(`${n} ${word}${n === 1 ? '' : 's'}`);\n rest -= n * size;\n } else if (parts.length > 0) break;\n }\n return `about ${parts.join(' ')}`;\n}\n\nconst MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\n\n/**\n * A moment in UTC, the same for every reader: \"20 Oct 2026, 09:14 UTC\".\n * Takes a Date, an ISO string, or unix seconds. `date: true` drops the time.\n */\nexport function formatUtc(at: Date | string | number, opts: { date?: boolean } = {}): string {\n const d = typeof at === 'number' ? new Date(at * 1000) : new Date(at);\n if (Number.isNaN(d.getTime())) return '—';\n const day = `${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]} ${d.getUTCFullYear()}`;\n if (opts.date) return day;\n const hh = String(d.getUTCHours()).padStart(2, '0');\n const mm = String(d.getUTCMinutes()).padStart(2, '0');\n return `${day}, ${hh}:${mm} UTC`;\n}\n\n/** 482113 → \"471 KB\"; 1288490 → \"1.2 MB\". Binary steps, the way a file dialog counts. */\nexport function formatBytes(bytes: number): string {\n if (!Number.isFinite(bytes) || bytes < 0) return '—';\n if (bytes < 1024) return `${bytes} B`;\n const units = ['KB', 'MB', 'GB', 'TB'];\n let v = bytes / 1024;\n let u = 0;\n while (v >= 1024 && u < units.length - 1) {\n v /= 1024;\n u++;\n }\n return `${v < 10 ? v.toFixed(1).replace(/\\.0$/, '') : Math.round(v)} ${units[u]}`;\n}\n","import { useOptionalOcSession, type OcAccount } from '@orangecheck/auth-client';\n\nimport { OCHK_APP_ACCOUNT, SessionAccountView, type SessionState } from '../chrome/tool-shell';\nimport type { AccountMenuItem } from '../chrome/app-shell';\nimport { shortenMiddle } from '../format';\n\n/*\n * The account control on any family site, read from @orangecheck/auth-client\n * (an optional peer: install it to use this entry). Inside an\n * OcSessionProvider it shows the account menu or \"Sign in\"; with no provider\n * there is nobody to be signed in as, so it offers sign-in.\n */\n\n/** The name an account chose to show, never a raw did:oc. */\nexport function accountName(account: Pick<OcAccount, 'displayName' | 'displayIdentity'>): string {\n if (account.displayName) return account.displayName;\n const { kind, value } = account.displayIdentity;\n if (kind === 'did' || value.startsWith('did:')) return 'Your account';\n if (kind === 'email') return value;\n // An address or npub: head and tail, short enough for a header chip.\n return shortenMiddle(value, 8, 6);\n}\n\nexport function SessionAccount({\n signInHref,\n signInPath,\n signInLabel,\n accountHref = OCHK_APP_ACCOUNT,\n items,\n nameFrom = 'lg',\n}: {\n /** Default: the session's sign-in URL on the auth host. */\n signInHref?: string;\n /** This site's own sign-in page (`/signin`): sign in here, in place, and return to the page the viewer is on. */\n signInPath?: string;\n signInLabel?: string;\n /** Where \"Your account\" goes. */\n accountHref?: string;\n /** This site's own items, above \"Your account\". */\n items?: (AccountMenuItem | 'separator')[];\n nameFrom?: 'sm' | 'lg';\n}) {\n const session = useOptionalOcSession();\n const status = session?.status ?? 'anonymous';\n const state: SessionState =\n status === 'authenticated' ? 'in' : status === 'anonymous' ? 'out' : status === 'error' ? 'unknown' : 'checking';\n const account = session?.account ?? null;\n const name = account ? accountName(account) : undefined;\n // A second line only when it adds something: the address behind a chosen name.\n const detail = account?.primaryBtc && account.displayName ? shortenMiddle(account.primaryBtc, 8, 6) : undefined;\n return (\n <SessionAccountView\n state={state === 'in' && !account ? 'checking' : state}\n name={name}\n detail={detail}\n items={items}\n signInHref={signInHref ?? (signInPath ? undefined : (session?.signInUrl ?? 'https://ochk.io/signin'))}\n signInPath={signInPath}\n signInLabel={signInLabel}\n accountHref={accountHref}\n onSignOut={() => void session?.signOut()}\n nameFrom={nameFrom}\n />\n );\n}\n\nexport { SessionAccountView };\nexport type { SessionState };\n"]}
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { i as ToolItem } from './tools-DFIxIj5p.mjs';
3
+ import { i as ToolItem } from './tools-Bg1gi2HO.mjs';
4
4
  import { d as FamilySlug, j as ProtocolSlug } from './index-DmfyPYe5.mjs';
5
5
 
6
6
  interface ShellNavItem {
@@ -19,9 +19,12 @@ interface ShellSection {
19
19
  label: string;
20
20
  title?: string;
21
21
  href: string;
22
+ icon?: ReactNode;
22
23
  groups: ShellNavGroup[];
23
24
  }
24
- declare function AppShell({ nav, sections, activeSection, page: pageOverride, persistent, home, family, account, banner, footer, children, className, }: {
25
+ declare function AppShell(props: AppShellProps): react.JSX.Element;
26
+ interface AppShellProps {
27
+ variant?: 'sidebar' | 'rail';
25
28
  nav?: ShellNavGroup[];
26
29
  sections?: ShellSection[];
27
30
  activeSection?: string;
@@ -32,9 +35,10 @@ declare function AppShell({ nav, sections, activeSection, page: pageOverride, pe
32
35
  account?: ReactNode;
33
36
  banner?: ReactNode;
34
37
  footer?: ReactNode;
38
+ header?: ReactNode;
35
39
  children: ReactNode;
36
40
  className?: string;
37
- }): react.JSX.Element;
41
+ }
38
42
  interface AccountMenuItem {
39
43
  label: string;
40
44
  href?: string;
@@ -81,4 +85,4 @@ interface ToolShellProps {
81
85
  }
82
86
  declare function ToolShell({ protocol, tools, home, account, rail, railFooter, variant, banner, footer, children, }: ToolShellProps): react.JSX.Element;
83
87
 
84
- export { AccountMenu as A, OCHK_APP_ACCOUNT as O, SessionAccountView as S, ThemeToggle as T, type AccountMenuItem as a, AppShell as b, type SessionState as c, type ShellNavGroup as d, type ShellNavItem as e, type ShellSection as f, ToolShell as g, type ToolShellProps as h, useSignInHref as u };
88
+ export { AccountMenu as A, OCHK_APP_ACCOUNT as O, SessionAccountView as S, ThemeToggle as T, type AccountMenuItem as a, AppShell as b, type AppShellProps as c, type SessionState as d, type ShellNavGroup as e, type ShellNavItem as f, type ShellSection as g, ToolShell as h, type ToolShellProps as i, useSignInHref as u };
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { i as ToolItem } from './tools-BggNN5DV.js';
3
+ import { i as ToolItem } from './tools-Bw7RhO92.js';
4
4
  import { d as FamilySlug, j as ProtocolSlug } from './index-DmfyPYe5.js';
5
5
 
6
6
  interface ShellNavItem {
@@ -19,9 +19,12 @@ interface ShellSection {
19
19
  label: string;
20
20
  title?: string;
21
21
  href: string;
22
+ icon?: ReactNode;
22
23
  groups: ShellNavGroup[];
23
24
  }
24
- declare function AppShell({ nav, sections, activeSection, page: pageOverride, persistent, home, family, account, banner, footer, children, className, }: {
25
+ declare function AppShell(props: AppShellProps): react.JSX.Element;
26
+ interface AppShellProps {
27
+ variant?: 'sidebar' | 'rail';
25
28
  nav?: ShellNavGroup[];
26
29
  sections?: ShellSection[];
27
30
  activeSection?: string;
@@ -32,9 +35,10 @@ declare function AppShell({ nav, sections, activeSection, page: pageOverride, pe
32
35
  account?: ReactNode;
33
36
  banner?: ReactNode;
34
37
  footer?: ReactNode;
38
+ header?: ReactNode;
35
39
  children: ReactNode;
36
40
  className?: string;
37
- }): react.JSX.Element;
41
+ }
38
42
  interface AccountMenuItem {
39
43
  label: string;
40
44
  href?: string;
@@ -81,4 +85,4 @@ interface ToolShellProps {
81
85
  }
82
86
  declare function ToolShell({ protocol, tools, home, account, rail, railFooter, variant, banner, footer, children, }: ToolShellProps): react.JSX.Element;
83
87
 
84
- export { AccountMenu as A, OCHK_APP_ACCOUNT as O, SessionAccountView as S, ThemeToggle as T, type AccountMenuItem as a, AppShell as b, type SessionState as c, type ShellNavGroup as d, type ShellNavItem as e, type ShellSection as f, ToolShell as g, type ToolShellProps as h, useSignInHref as u };
88
+ export { AccountMenu as A, OCHK_APP_ACCOUNT as O, SessionAccountView as S, ThemeToggle as T, type AccountMenuItem as a, AppShell as b, type AppShellProps as c, type SessionState as d, type ShellNavGroup as e, type ShellNavItem as f, type ShellSection as g, ToolShell as h, type ToolShellProps as i, useSignInHref as u };
@@ -93,7 +93,7 @@ declare function Meter({ value, max, ghost, label, figure, tone, size, className
93
93
  ghost?: number;
94
94
  label: ReactNode;
95
95
  figure?: ReactNode;
96
- tone?: 'ink' | 'brand';
96
+ tone?: 'ink' | 'brand' | 'good' | 'caution' | 'bad';
97
97
  size?: 'md' | 'sm';
98
98
  className?: string;
99
99
  }): react.JSX.Element;
@@ -93,7 +93,7 @@ declare function Meter({ value, max, ghost, label, figure, tone, size, className
93
93
  ghost?: number;
94
94
  label: ReactNode;
95
95
  figure?: ReactNode;
96
- tone?: 'ink' | 'brand';
96
+ tone?: 'ink' | 'brand' | 'good' | 'caution' | 'bad';
97
97
  size?: 'md' | 'sm';
98
98
  className?: string;
99
99
  }): react.JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orangecheck/app-design",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "The ochk.app design system: tokens, type, the isometric check, halftone photography, primitives and app chrome. Its own system, scoped under data-oc-app=\"ochk\" so it never reaches the .ochk.io family sites.",
5
5
  "keywords": [
6
6
  "orangecheck",
@@ -91,7 +91,7 @@
91
91
  "scripts": {
92
92
  "build": "tsup && yarn copy:css",
93
93
  "copy:css": "rm -rf dist/styles && mkdir -p dist/styles && cp src/styles.css dist/styles.css && cp -R src/styles/. dist/styles/",
94
- "test": "node scripts/check-contrast.mjs && node scripts/check-isolation.mjs && node scripts/check-shell.mjs && node scripts/check-mirror.mjs && node scripts/check-code.mjs && node scripts/check-entries.mjs && node scripts/check-render.mjs && node scripts/check-next.mjs",
94
+ "test": "node scripts/check-contrast.mjs && node scripts/check-isolation.mjs && node scripts/check-shell.mjs && node scripts/check-mirror.mjs && node scripts/check-code.mjs && node scripts/check-entries.mjs && node scripts/check-render.mjs && node scripts/check-dom.mjs && node scripts/check-next.mjs",
95
95
  "dev": "tsup --watch",
96
96
  "type-check": "tsc --noEmit",
97
97
  "clean": "rm -rf dist"
@@ -137,6 +137,7 @@
137
137
  "@types/qrcode": "^1.5.5",
138
138
  "@types/react": "^19.0.0",
139
139
  "@types/react-dom": "^19.0.0",
140
+ "jsdom": "^29.0.2",
140
141
  "lucide-react": "^0.544.0",
141
142
  "mermaid": "^11.14.0",
142
143
  "next": "^16.3.8",