@bison-lab/payload-blocks 3.3.0 → 3.4.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.
@@ -1 +1 @@
1
- {"version":3,"file":"react.mjs","names":[],"sources":["../src/image.tsx","../src/link.tsx","../src/render-blocks.tsx","../src/media.ts","../src/blocks/hero/component.tsx","../src/blocks/showcase-panels/component.tsx","../src/blocks/process-steps/component.tsx","../src/blocks/faq-columns/component.tsx","../src/blocks/testimonial-masonry/component.tsx","../src/blocks/nap/component.tsx","../src/blocks/stats-band/component.tsx","../src/blocks/mega-menu/rem-width.ts","../src/blocks/mega-menu/component.tsx","../src/blocks/mega-menu/header.tsx"],"sourcesContent":["import type { ComponentType } from \"react\";\n\n/**\n * The image injection point.\n *\n * The package deliberately does not depend on `next` — a Payload site is not\n * necessarily a Next.js site, and `next/image` needs per-site configuration\n * (`remotePatterns` for the media route) that a package cannot supply. So\n * renderers never construct an image element themselves; they render the\n * component they were handed. A Next site writes one adapter around\n * `next/image` and every block gets optimisation from it.\n *\n * This is a *component type*, not a render function, on purpose: a Server\n * Component may pass a client component reference across the boundary, but not\n * a closure. `imageComponent` therefore survives the trip from a server page\n * into these client renderers, where a `renderImage` callback would not.\n */\n\nexport interface BlockImageProps {\n src: string;\n /** Always a string — see `resolveMedia`. Empty means decorative. */\n alt: string;\n width?: number;\n height?: number;\n className?: string;\n /** Passed through to `next/image`'s `sizes`; ignored by the default. */\n sizes?: string;\n /** Set on the one image above the fold, typically the hero's. */\n priority?: boolean;\n}\n\nexport type BlockImageComponent = ComponentType<BlockImageProps>;\n\n/**\n * The fallback when no `imageComponent` is supplied: a plain `<img>`. Correct\n * everywhere and optimised nowhere, which is the right default for a package\n * that cannot know what framework it landed in.\n */\nexport function DefaultBlockImage({\n src,\n alt,\n width,\n height,\n className,\n sizes,\n priority,\n}: BlockImageProps) {\n return (\n <img\n src={src}\n alt={alt}\n width={width}\n height={height}\n className={className}\n sizes={sizes}\n loading={priority ? \"eager\" : \"lazy\"}\n decoding=\"async\"\n />\n );\n}\n","import type { AnchorHTMLAttributes, ComponentType, ReactNode } from \"react\";\nimport type { RenderLink } from \"@bison-lab/ui\";\n\n/**\n * The link injection point, the same shape as `BlockImageComponent`.\n *\n * A renderer never writes `<a>` directly. A site hands in one link component\n * and every CMS link routes through it — Next's `Link`, so navigation stays\n * client-side. It is a *component type*, not a render function, for the same\n * reason the image seam is: a Server Component can pass a client component\n * reference across the boundary, but not a closure.\n *\n * `newTab` is the flag from `linkFields()`; `target` and `rel` are always\n * already expanded from it by the time the component is called, so an adapter\n * that only spreads the anchor attributes onto its router link is still safe.\n * The one thing an adapter must do is drop `newTab` before spreading, or React\n * warns about an unknown DOM attribute — see the README's `next/link` adapter.\n */\nexport interface BlockLinkProps\n extends AnchorHTMLAttributes<HTMLAnchorElement> {\n href: string;\n children: ReactNode;\n newTab?: boolean;\n className?: string;\n}\n\nexport type BlockLinkComponent = ComponentType<BlockLinkProps>;\n\n/** `target` and `rel` together, or neither: a bare `target` is a tabnabbing hole. */\nexport function newTabAttributes(\n newTab: boolean | null | undefined,\n): Pick<AnchorHTMLAttributes<HTMLAnchorElement>, \"target\" | \"rel\"> {\n return newTab ? { target: \"_blank\", rel: \"noopener noreferrer\" } : {};\n}\n\n/**\n * The fallback when no `linkComponent` is supplied: a plain `<a>`. Correct\n * everywhere and client-side nowhere, which is the right default for a package\n * that cannot know what router it landed in.\n */\nexport function DefaultBlockLink({\n href,\n newTab,\n children,\n ...rest\n}: BlockLinkProps) {\n return (\n <a href={href} {...rest} {...newTabAttributes(newTab)}>\n {children}\n </a>\n );\n}\nDefaultBlockLink.displayName = \"DefaultBlockLink\";\n\n/**\n * Adapts the seam to the `renderLink` prop every `@bison-lab/ui` block takes.\n * The ui blocks hand over `target`/`rel` already expanded, so `newTab` is\n * read back off `target` to keep the flag and the attributes in step.\n */\nexport function renderLinkWith(Link: BlockLinkComponent): RenderLink {\n return ({ href, children, ...rest }) => (\n <Link href={href} newTab={rest.target === \"_blank\"} {...rest}>\n {children}\n </Link>\n );\n}\n","import type { ComponentType, ReactElement } from \"react\";\n\nimport { type BlockImageComponent, DefaultBlockImage } from \"./image\";\nimport { type BlockLinkComponent, DefaultBlockLink } from \"./link\";\nimport { overlapsSeam } from \"./seam\";\n\n/** The minimum a row must be for the dispatch to place it. */\nexport interface BlockLike {\n blockType: string;\n id?: string | null;\n}\n\n/**\n * What every renderer is handed.\n *\n * The neighbour props let a block adapt to what sits around it: `prevType` to\n * close a seam against the block above, `runIndex` to alternate surfaces across\n * back-to-back blocks of one type, `isLast` because the final block sits\n * against the site footer, and `overlapAbove` / `overlapBelow` when the\n * neighbour on that side floats across the seam (see `seam.ts`).\n *\n * `containerClassName`, `imageComponent` and `linkComponent` are the three\n * things a package block cannot decide for itself — the site's page measure,\n * how an image gets optimised, and which router a link goes through. All three\n * arrive already defaulted, so a renderer never checks them.\n */\nexport interface BlockRendererProps<B extends BlockLike = BlockLike> {\n block: B;\n /** Position on the page, hero included. */\n index: number;\n /** The type of the block before this one; absent on the first block. */\n prevType?: string;\n /** Position within a run of consecutive same-type blocks, from 0. */\n runIndex: number;\n isLast: boolean;\n /**\n * The block before this one floats down over the seam, so this block's band\n * wrapper needs clearance at the top. Decided by `RenderBlocks`' `floats`.\n */\n overlapAbove?: boolean;\n /** The block after this one floats up over the seam; clearance at the bottom. */\n overlapBelow?: boolean;\n /** The site's page measure, for the band wrapper. */\n containerClassName: string;\n /** How this block turns a resolved image into an element. */\n imageComponent: BlockImageComponent;\n /** How this block turns an `href` into an element. */\n linkComponent: BlockLinkComponent;\n}\n\n/**\n * One renderer per block type, each typed to its own block.\n *\n * Written as a mapped type over the union so `hero` gets `HeroBlockData`, not\n * the whole union. A site writes `satisfies BlockRegistryFor<AnyBlock>` where\n * `AnyBlock` comes off its *generated* `Page` type — that is the compile-time\n * layout lock, and it has to stay in the site, because only the site has\n * generated types. The registry is a parameter here for exactly that reason.\n */\nexport type BlockRegistryFor<B extends BlockLike> = {\n [K in B[\"blockType\"]]: ComponentType<\n BlockRendererProps<Extract<B, { blockType: K }>>\n >;\n};\n\n/** Fallback page measure for a site that does not pass its own. */\nexport const DEFAULT_CONTAINER = \"mx-auto w-full max-w-7xl px-6\";\n\ntype LooseRenderer = ComponentType<BlockRendererProps<BlockLike>>;\n\n/**\n * A row saved against a block that has since left the config has no renderer.\n * It is dropped before anything is counted, so its neighbours see each other,\n * not a gap: the block after it gets the real `prevType`, a run of one type is\n * not broken by it, and the block before it is still `isLast` when it ends the\n * page.\n */\nfunction renderable(\n blocks: BlockLike[],\n registry: Record<string, LooseRenderer | undefined>,\n): { block: BlockLike; Renderer: LooseRenderer }[] {\n return blocks.flatMap((block) => {\n const Renderer = registry[block.blockType];\n return Renderer ? [{ block, Renderer }] : [];\n });\n}\n\n/** Position of each block within its run of consecutive same-type blocks. */\nfunction runIndexes(blocks: BlockLike[]): number[] {\n const out: number[] = [];\n for (let i = 0; i < blocks.length; i += 1) {\n out.push(\n i > 0 && blocks[i - 1].blockType === blocks[i].blockType\n ? out[i - 1] + 1\n : 0,\n );\n }\n return out;\n}\n\nexport interface RenderBlocksProps<B extends BlockLike> {\n /** Pass `[...page.hero, ...page.layout]`, so the hero counts as index 0. */\n blocks: B[];\n registry: BlockRegistryFor<B>;\n /** The site's page measure. Defaults to `DEFAULT_CONTAINER`. */\n containerClassName?: string;\n /** Defaults to a plain `<img>`; a Next site passes a `next/image` adapter. */\n imageComponent?: BlockImageComponent;\n /** Defaults to a plain `<a>`; a Next site passes a `next/link` adapter. */\n linkComponent?: BlockLinkComponent;\n /**\n * Which rows float across their seams, so their neighbours make room.\n * Defaults to `overlapsSeam`: the package's stats band with `overlap` on. A\n * site with its own floating block extends it:\n * `floats={(row) => overlapsSeam(row) || row.blockType === \"bookingCard\"}`.\n */\n floats?: (block: B) => boolean;\n}\n\n/**\n * Renders a page's blocks in order through the registry. A Server Component:\n * it only maps and looks up, and each renderer decides its own nature.\n */\nexport function RenderBlocks<B extends BlockLike>({\n blocks,\n registry,\n containerClassName = DEFAULT_CONTAINER,\n imageComponent = DefaultBlockImage,\n linkComponent = DefaultBlockLink,\n floats = overlapsSeam,\n}: RenderBlocksProps<B>): ReactElement {\n const rows = renderable(\n blocks,\n // The mapped registry type is per-block, and the dispatch is not; the\n // lookup below is what guarantees each renderer only ever sees its own\n // block type, and no signature can express that to the compiler.\n registry as unknown as Record<string, LooseRenderer | undefined>,\n );\n const runs = runIndexes(rows.map((row) => row.block));\n const floating = rows.map((row) => floats(row.block as B));\n return (\n <>\n {rows.map(({ block, Renderer }, index) => (\n <Renderer\n key={block.id ?? index}\n block={block}\n index={index}\n prevType={index > 0 ? rows[index - 1].block.blockType : undefined}\n runIndex={runs[index]}\n isLast={index === rows.length - 1}\n overlapAbove={index > 0 && floating[index - 1]}\n overlapBelow={index < rows.length - 1 && floating[index + 1]}\n containerClassName={containerClassName}\n imageComponent={imageComponent}\n linkComponent={linkComponent}\n />\n ))}\n </>\n );\n}\n","/**\n * The upload side of the boundary.\n *\n * A Payload upload field holds either the row's id or, once depth resolves it,\n * the whole document. Neither shape can be imported from here: `Media` is\n * generated per site, and this package must never reach for a site's\n * `payload-types`. So the doc is described structurally, and every renderer\n * goes through `resolveMedia` rather than reading `.url` itself.\n */\n\n/**\n * The subset of a Payload upload document a block renderer actually reads.\n * Every site's generated `Media` interface is assignable to this, whatever\n * else it carries.\n *\n * Deliberately no index signature: TypeScript refuses to assign an interface\n * to a type with one, and a site's generated `Media` is always an interface.\n * An index signature here would break exactly the structural match this type\n * exists to provide.\n */\nexport interface MediaDoc {\n url?: string | null;\n alt?: string | null;\n width?: number | null;\n height?: number | null;\n}\n\n/** What an upload field holds before and after `depth` populates it. */\nexport type MediaValue = number | string | MediaDoc | null | undefined;\n\n/** A resolved image, in the shape every `@bison-lab/ui` block asks for. */\nexport interface ResolvedMedia {\n src: string;\n alt: string;\n width?: number;\n height?: number;\n}\n\nfunction isMediaDoc(value: MediaValue): value is MediaDoc {\n return typeof value === \"object\" && value !== null;\n}\n\n/**\n * Narrows an upload value to something renderable, or `undefined`.\n *\n * `undefined` covers three cases a renderer must treat identically: the field\n * is empty, `depth: 0` left it as a bare id, or the document exists but has no\n * `url` yet (an upload mid-flight). A renderer that gets `undefined` omits the\n * image rather than emitting a broken `<img>`.\n *\n * `alt` is always a string: the media collection requires it, but a draft saved\n * before that validation ran can still reach a renderer, and an `alt` of\n * `undefined` would silently drop the attribute entirely.\n */\nexport function resolveMedia(value: MediaValue): ResolvedMedia | undefined {\n if (!isMediaDoc(value)) return undefined;\n const { url, alt, width, height } = value;\n if (typeof url !== \"string\" || url.length === 0) return undefined;\n return {\n src: url,\n alt: typeof alt === \"string\" ? alt : \"\",\n ...(typeof width === \"number\" ? { width } : {}),\n ...(typeof height === \"number\" ? { height } : {}),\n };\n}\n","import { Button } from \"@bison-lab/ui\";\n\nimport { cx } from \"../../cx\";\nimport { newTabAttributes } from \"../../link\";\nimport { resolveMedia } from \"../../media\";\nimport type { BlockRendererProps } from \"../../render-blocks\";\nimport { seamClasses } from \"../../seam\";\nimport type { HeroBlockData } from \"../../types\";\n\n/**\n * How each tone paints. Semantic tokens only — a site's brand reaches these\n * through its theme, never through a class named here.\n */\nconst TONES = {\n sweep: \"bg-primary text-primary-foreground\",\n dark: \"bg-foreground text-background\",\n light: \"bg-background text-foreground\",\n} as const;\n\n/**\n * The package's hero: deliberately plain.\n *\n * A hero is where a site's brand is loudest, so this exists to make a page\n * render at all, not to be the final answer. A site overrides this one registry\n * entry with its own and keeps every other block.\n */\nexport function HeroBlockRenderer({\n block,\n index,\n overlapAbove,\n overlapBelow,\n containerClassName,\n imageComponent: Image,\n linkComponent: Link,\n}: BlockRendererProps<HeroBlockData>) {\n const image = resolveMedia(block.image);\n const links = (block.links ?? []).filter((link) => link.href && link.label);\n const tone = TONES[block.tone ?? \"sweep\"];\n\n return (\n <section\n className={cx(\n \"relative isolate overflow-hidden\",\n tone,\n // Symmetric padding by default; the extra below appears only when an\n // editor drops an overlapping band under the hero.\n seamClasses({ overlapAbove, overlapBelow }),\n )}\n >\n {image ? (\n <div className=\"absolute inset-0 -z-10 opacity-25\">\n <Image\n src={image.src}\n alt=\"\"\n width={image.width}\n height={image.height}\n className=\"h-full w-full object-cover\"\n sizes=\"100vw\"\n // The hero is the page's LCP element whenever it opens the page.\n priority={index === 0}\n />\n </div>\n ) : null}\n <div className={cx(containerClassName, \"py-20 lg:py-28\")}>\n <div className=\"max-w-3xl\">\n {block.eyebrow ? (\n <p className=\"text-sm font-semibold tracking-widest uppercase opacity-80\">\n {block.eyebrow}\n </p>\n ) : null}\n <h1 className=\"mt-3 text-4xl font-bold tracking-tight text-balance lg:text-5xl\">\n {block.heading}\n </h1>\n {block.body ? (\n <p className=\"mt-6 max-w-2xl text-lg leading-relaxed opacity-90\">\n {block.body}\n </p>\n ) : null}\n {links.length > 0 ? (\n <div className=\"mt-8 flex flex-wrap gap-3\">\n {links.map((link, i) => (\n <Button\n key={`${link.href}-${i}`}\n asChild\n variant={i === 0 ? \"default\" : \"outline\"}\n >\n <Link\n href={link.href as string}\n newTab={link.newTab ?? false}\n {...newTabAttributes(link.newTab)}\n >\n {link.label}\n </Link>\n </Button>\n ))}\n </div>\n ) : null}\n </div>\n </div>\n </section>\n );\n}\n","import { ShowcasePanelsBlock, type ShowcasePanelItem } from \"@bison-lab/ui\";\n\nimport { cx } from \"../../cx\";\nimport { renderLinkWith } from \"../../link\";\nimport { resolveMedia, type ResolvedMedia } from \"../../media\";\nimport type { BlockRendererProps } from \"../../render-blocks\";\nimport { seamClasses } from \"../../seam\";\nimport type { ShowcasePanelsBlockData } from \"../../types\";\n\nexport function ShowcasePanelsBlockRenderer({\n block,\n overlapAbove,\n overlapBelow,\n containerClassName,\n imageComponent: Image,\n linkComponent,\n}: BlockRendererProps<ShowcasePanelsBlockData>) {\n // A panel without a picture has nothing to expand into, so a row whose\n // upload has not resolved is dropped rather than rendered empty. `dims`\n // stays index-aligned with `items` so the image callback can reach the\n // width and height the library's own prop shape has no room for.\n const dims: ResolvedMedia[] = [];\n const items: ShowcasePanelItem[] = [];\n for (const [i, row] of (block.items ?? []).entries()) {\n const image = resolveMedia(row.image);\n if (!image) continue;\n dims.push(image);\n items.push({\n id: row.id ?? String(i),\n title: row.title ?? \"\",\n summary: row.summary ?? \"\",\n image: { src: image.src, alt: image.alt },\n ...(row.href ? { href: row.href } : {}),\n ...(row.numeral ? { numeral: row.numeral } : {}),\n });\n }\n if (items.length === 0) return null;\n\n return (\n <section className={seamClasses({ overlapAbove, overlapBelow })}>\n <div className={cx(containerClassName, \"py-16 lg:py-24\")}>\n <ShowcasePanelsBlock\n items={items}\n spineVariant={block.spineVariant ?? \"numbered\"}\n defaultActiveIndex={Math.min(\n Math.max(block.defaultActiveIndex ?? 0, 0),\n items.length - 1,\n )}\n {...(block.watermark ? { watermark: block.watermark } : {})}\n renderLink={renderLinkWith(linkComponent)}\n renderImage={(item, i) => (\n <Image\n src={item.image.src}\n alt={item.image.alt ?? \"\"}\n width={dims[i]?.width}\n height={dims[i]?.height}\n className=\"absolute inset-0 h-full w-full object-cover\"\n sizes=\"(min-width: 1024px) 50vw, 100vw\"\n />\n )}\n />\n </div>\n </section>\n );\n}\n","import { ProcessStepsBlock, type ProcessStepItem } from \"@bison-lab/ui\";\n\nimport { cx } from \"../../cx\";\nimport { resolveMedia, type ResolvedMedia } from \"../../media\";\nimport type { BlockRendererProps } from \"../../render-blocks\";\nimport { seamClasses } from \"../../seam\";\nimport type { ProcessStepsBlockData } from \"../../types\";\n\nexport function ProcessStepsBlockRenderer({\n block,\n overlapAbove,\n overlapBelow,\n containerClassName,\n imageComponent: Image,\n}: BlockRendererProps<ProcessStepsBlockData>) {\n // Same rule as the showcase panels: a step is its picture, so an unresolved\n // upload drops the row instead of rendering a hole in the rail.\n const dims: ResolvedMedia[] = [];\n const items: ProcessStepItem[] = [];\n for (const [i, row] of (block.items ?? []).entries()) {\n const image = resolveMedia(row.image);\n if (!image) continue;\n dims.push(image);\n items.push({\n id: row.id ?? String(i),\n title: row.title ?? \"\",\n description: row.description ?? \"\",\n image: { src: image.src, alt: image.alt },\n });\n }\n if (items.length === 0) return null;\n\n return (\n <section className={seamClasses({ overlapAbove, overlapBelow })}>\n <div className={cx(containerClassName, \"py-16 lg:py-24\")}>\n <ProcessStepsBlock\n items={items}\n autoAdvance={block.autoAdvance ?? true}\n autoAdvanceDuration={block.autoAdvanceDuration ?? 6000}\n pauseOnHover={block.pauseOnHover ?? true}\n defaultActiveIndex={Math.min(\n Math.max(block.defaultActiveIndex ?? 0, 0),\n items.length - 1,\n )}\n renderImage={(item, i) => (\n <Image\n src={item.image.src}\n alt={item.image.alt ?? \"\"}\n width={dims[i]?.width}\n height={dims[i]?.height}\n className=\"size-full object-cover\"\n sizes=\"(min-width: 768px) 66vw, 100vw\"\n />\n )}\n />\n </div>\n </section>\n );\n}\n","import { FAQColumnsBlock, type FAQItem } from \"@bison-lab/ui\";\n\nimport { cx } from \"../../cx\";\nimport { renderLinkWith } from \"../../link\";\nimport type { BlockRendererProps } from \"../../render-blocks\";\nimport { seamClasses } from \"../../seam\";\nimport type { FaqColumnsBlockData } from \"../../types\";\n\nexport function FaqColumnsBlockRenderer({\n block,\n overlapAbove,\n overlapBelow,\n containerClassName,\n linkComponent,\n}: BlockRendererProps<FaqColumnsBlockData>) {\n const items: FAQItem[] = (block.items ?? [])\n .filter((row) => row.question && row.answer)\n .map((row, i) => ({\n id: row.id ?? String(i),\n question: row.question as string,\n // The field is a textarea, so blank lines are the editor's paragraph\n // breaks and have to survive into the DOM.\n answer: <span className=\"whitespace-pre-line\">{row.answer}</span>,\n }));\n if (items.length === 0) return null;\n\n const cta = block.cta;\n const hasCta = Boolean(cta?.href && cta?.linkText);\n\n return (\n <section className={seamClasses({ overlapAbove, overlapBelow })}>\n <div className={cx(containerClassName, \"py-16 lg:py-24\")}>\n <FAQColumnsBlock\n // The site measure is on the wrapper above; the block's own\n // container and vertical rhythm are reset so they cannot fight it.\n className=\"py-0 md:py-0\"\n containerClassName=\"max-w-none px-0 sm:px-0\"\n items={items}\n title={block.title ?? \"\"}\n {...(block.eyebrow ? { eyebrow: block.eyebrow } : {})}\n {...(block.description ? { description: block.description } : {})}\n sticky={block.sticky ?? true}\n type={block.type ?? \"single\"}\n collapsible={block.collapsible ?? true}\n renderLink={renderLinkWith(linkComponent)}\n {...(hasCta && cta\n ? {\n cta: {\n title: cta.title ?? \"\",\n linkText: cta.linkText as string,\n href: cta.href as string,\n newTab: cta.newTab ?? false,\n },\n }\n : {})}\n />\n </div>\n </section>\n );\n}\n","import { TestimonialMasonry, type TestimonialItem } from \"@bison-lab/ui\";\n\nimport { cx } from \"../../cx\";\nimport { renderLinkWith } from \"../../link\";\nimport { resolveMedia } from \"../../media\";\nimport type { BlockRendererProps } from \"../../render-blocks\";\nimport { seamClasses } from \"../../seam\";\nimport type { TestimonialMasonryBlockData } from \"../../types\";\n\nexport function TestimonialMasonryBlockRenderer({\n block,\n overlapAbove,\n overlapBelow,\n containerClassName,\n linkComponent,\n}: BlockRendererProps<TestimonialMasonryBlockData>) {\n const items: TestimonialItem[] = (block.items ?? [])\n .filter((row) => row.content && row.author?.name)\n .map((row, i) => {\n const avatar = resolveMedia(row.author?.avatar);\n return {\n id: row.id ?? String(i),\n content: row.content as string,\n author: {\n name: row.author?.name as string,\n ...(row.author?.title ? { title: row.author.title } : {}),\n // The library falls back to initials when this is absent, which is\n // why an unresolved upload must not become an empty string.\n ...(avatar ? { avatarUrl: avatar.src } : {}),\n },\n };\n });\n if (items.length === 0) return null;\n\n const link = block.link;\n const hasLink = Boolean(link?.href && link?.label);\n\n return (\n // The band's surface is full-bleed; only its contents sit at the measure.\n <section className={cx(\"bg-accent\", seamClasses({ overlapAbove, overlapBelow }))}>\n <div className={cx(containerClassName, \"py-16 lg:py-24\")}>\n <TestimonialMasonry\n className=\"bg-transparent px-0 lg:px-0\"\n containerClassName=\"max-w-none px-0 py-0 sm:py-0 md:px-0 md:py-0\"\n items={items}\n title={block.title ?? \"\"}\n renderLink={renderLinkWith(linkComponent)}\n {...(block.eyebrow ? { eyebrow: block.eyebrow } : {})}\n {...(block.description ? { description: block.description } : {})}\n {...(typeof block.minItemsForFade === \"number\"\n ? { minItemsForFade: block.minItemsForFade }\n : {})}\n {...(typeof block.maxVisibleRows === \"number\"\n ? { maxVisibleRows: block.maxVisibleRows }\n : {})}\n {...(hasLink && link\n ? {\n link: {\n label: link.label as string,\n href: link.href as string,\n newTab: link.newTab ?? false,\n },\n }\n : {})}\n />\n </div>\n </section>\n );\n}\n","import {\n JsonLd,\n NapBlock,\n type NapDepartment,\n type NapRecord,\n} from \"@bison-lab/ui\";\n\nimport { cx } from \"../../cx\";\nimport { renderLinkWith } from \"../../link\";\nimport type { BlockRendererProps } from \"../../render-blocks\";\nimport { seamClasses } from \"../../seam\";\nimport type { NapBlockData } from \"../../types\";\n\nexport function NapBlockRenderer({\n block,\n overlapAbove,\n overlapBelow,\n containerClassName,\n linkComponent,\n}: BlockRendererProps<NapBlockData>) {\n const address = block.address;\n const departments: NapDepartment[] = (block.departments ?? [])\n // `phoneE164` is what becomes the `tel:` href and the structured data's\n // `telephone`, so a department without one is not publishable as a NAP\n // entry at all — dropping it beats emitting an unreachable number.\n .filter((row) => row.name && row.phoneE164)\n .map((row, i) => ({\n id: row.id ?? String(i),\n name: row.name as string,\n ...(row.departmentType ? { type: row.departmentType } : {}),\n phone: {\n e164: row.phoneE164 as string,\n ...(row.phoneDisplay ? { display: row.phoneDisplay } : {}),\n },\n }));\n\n if (!block.businessName || !address?.streetAddress || departments.length === 0)\n return null;\n\n const record: NapRecord = {\n name: block.businessName,\n type: block.businessType ?? \"LocalBusiness\",\n address: {\n streetAddress: address.streetAddress,\n addressLocality: address.addressLocality ?? \"\",\n addressRegion: address.addressRegion ?? \"\",\n postalCode: address.postalCode ?? \"\",\n ...(address.addressCountry\n ? { addressCountry: address.addressCountry }\n : {}),\n },\n departments,\n ...(block.url ? { url: block.url } : {}),\n };\n\n return (\n <section className={seamClasses({ overlapAbove, overlapBelow })}>\n <div className={cx(containerClassName, \"py-16 lg:py-24\")}>\n <NapBlock\n record={record}\n showName={block.showName ?? true}\n headingLevel={block.headingLevel ?? \"h2\"}\n renderLink={renderLinkWith(linkComponent)}\n />\n {block.emitJsonLd === false ? null : <JsonLd data={record} />}\n </div>\n </section>\n );\n}\n","import {\n StatsBandBlock,\n type StatItem,\n type StatsBandColumns,\n} from \"@bison-lab/ui\";\n\nimport { cx } from \"../../cx\";\nimport { renderLinkWith } from \"../../link\";\nimport type { BlockRendererProps } from \"../../render-blocks\";\nimport { SEAM_PULL_DOWN, SEAM_PULL_UP, seamClasses } from \"../../seam\";\nimport type { StatsBandBlockData } from \"../../types\";\n\n/** The select only offers 2–6, but a site can edit the config; fall back to the design's four. */\nfunction columnsFrom(value: StatsBandBlockData[\"columns\"]): StatsBandColumns {\n const n = Number(value ?? 4);\n return (Number.isInteger(n) && n >= 2 && n <= 6 ? n : 4) as StatsBandColumns;\n}\n\nexport function StatsBandBlockRenderer({\n block,\n index,\n isLast,\n overlapAbove,\n overlapBelow,\n containerClassName,\n linkComponent,\n}: BlockRendererProps<StatsBandBlockData>) {\n // A stat is its figure: a row without one is dropped rather than rendered\n // as an empty cell, and the band needs two to be a band at all.\n const items: StatItem[] = (block.items ?? []).flatMap((row, i) =>\n row.value\n ? [\n {\n id: row.id ?? String(i),\n value: row.value,\n label: row.label ?? \"\",\n ...(row.href ? { href: row.href } : {}),\n ...(row.wide ? { wide: true } : {}),\n },\n ]\n : [],\n );\n if (items.length < 2) return null;\n\n // Floating only makes sense across a seam: first on the page, the band\n // keeps its top padding, and last on the page it keeps its bottom padding\n // rather than pulling the footer up. The neighbours clear the same\n // distance through `seamClasses` (see `seam.ts`), and so does this band\n // when the neighbour is the one floating: two floating bands in a row\n // would otherwise pull into each other, so on that side neither pulls and\n // both clear. A floating band is headless (the config hides the heading\n // fields), so any heading left over from before `overlap` was ticked is\n // not pulled up into the hero.\n const floats = Boolean(block.overlap);\n const overAbove = floats && index > 0 && !overlapAbove;\n const overBelow = floats && !isLast && !overlapBelow;\n\n return (\n <section\n className={cx(\n \"relative\",\n floats && \"z-10\",\n seamClasses({ overlapAbove, overlapBelow }),\n )}\n >\n <div\n className={cx(\n containerClassName,\n overAbove ? SEAM_PULL_UP : \"pt-16 lg:pt-24\",\n overBelow ? SEAM_PULL_DOWN : \"pb-16 lg:pb-24\",\n )}\n >\n <StatsBandBlock\n items={items}\n columns={columnsFrom(block.columns)}\n tone={block.tone ?? \"card\"}\n align={block.align ?? \"start\"}\n eyebrow={(!floats && block.eyebrow) || undefined}\n title={(!floats && block.title) || undefined}\n description={(!floats && block.description) || undefined}\n containerClassName=\"max-w-none px-0 sm:px-0\"\n renderLink={renderLinkWith(linkComponent)}\n />\n </div>\n </section>\n );\n}\n","/**\n * The one rule for a typed panel width, shared by the config's validator and\n * the renderer's reader so the two cannot drift: if the validator accepted a\n * value the renderer dropped, the editor would be back to a width that\n * silently does nothing. React-free, because `config.ts` ships from the Node\n * entry.\n */\nconst REM_WIDTH = /^\\s*(\\d+(?:\\.\\d+)?)\\s*(rem)?\\s*$/;\n\n/** \"30\" and \"30rem\" both mean 30rem; anything else is not a width. */\nexport function remWidth(\n value: string | null | undefined,\n): `${number}rem` | null {\n const match = REM_WIDTH.exec(value ?? \"\");\n return match ? (`${Number(match[1])}rem` as `${number}rem`) : null;\n}\n","import {\n MegaMenuPanel,\n type MegaMenuColumn,\n type MegaMenuIcons,\n type MegaMenuLink,\n type MegaMenuPanelData,\n type MegaMenuSection,\n type MegaMenuVariants,\n type MegaMenuWidth,\n} from \"@bison-lab/ui\";\n\nimport { renderLinkWith } from \"../../link\";\nimport type { BlockRendererProps } from \"../../render-blocks\";\nimport type { MegaMenuBlockData, MegaMenuLinkData } from \"../../types\";\nimport { remWidth } from \"./rem-width\";\n\nfunction completeLink(row: MegaMenuLinkData): MegaMenuLink | null {\n if (!row.label || !row.href) return null;\n return {\n label: row.label,\n href: row.href,\n ...(row.description ? { description: row.description } : {}),\n ...(row.variant ? { variant: row.variant } : {}),\n ...(row.icon ? { icon: row.icon } : {}),\n ...(row.newTab ? { newTab: true } : {}),\n };\n}\n\n/**\n * The row as `MegaMenuPanel` data, or `null` when there is nothing to open:\n * no columns, or columns whose links are not yet complete. Draft saves skip\n * validation, so a live preview genuinely hands this half-built rows.\n */\nexport function megaMenuPanelFromRow(\n row: MegaMenuBlockData,\n): MegaMenuPanelData | null {\n const panel = row.panel;\n if (!panel?.columns?.length) return null;\n const custom = Boolean(panel.customWidths);\n\n const columns: MegaMenuColumn[] = [];\n for (const column of panel.columns) {\n const sections: MegaMenuSection[] = [];\n for (const section of column.sections ?? []) {\n const links = (section.links ?? [])\n .map(completeLink)\n .filter((link): link is MegaMenuLink => link !== null);\n if (!links.length) continue;\n sections.push({\n ...(section.eyebrow ? { eyebrow: section.eyebrow } : {}),\n display: section.display ?? \"list\",\n hideDescriptionsOnMobile: section.hideDescriptionsOnMobile ?? true,\n links,\n });\n }\n columns.push({\n ...(custom && column.customWidth\n ? { customWidth: column.customWidth }\n : { width: Number(column.width ?? \"100\") as MegaMenuWidth }),\n ...(column.divider ? { divider: true } : {}),\n sections,\n });\n }\n if (!columns.some((column) => column.sections.length)) return null;\n\n const overview = panel.footer?.overview;\n const cta = panel.footer?.cta;\n // Independent of the column checkbox: the config gates only `customWidth`\n // on it, and the help text promises a typed panel width is used.\n const customMax = remWidth(panel.customMaxWidth);\n\n return {\n maxWidth: customMax ?? panel.maxWidth ?? \"standard\",\n columns,\n footer: {\n ...(overview?.label && overview.href\n ? {\n overview: {\n label: overview.label,\n href: overview.href,\n ...(overview.newTab ? { newTab: true } : {}),\n },\n }\n : {}),\n ...(cta?.label && cta.href\n ? {\n cta: {\n label: cta.label,\n href: cta.href,\n ...(cta.newTab ? { newTab: true } : {}),\n },\n }\n : {}),\n },\n };\n}\n\nexport interface MegaMenuBlockRendererProps\n extends BlockRendererProps<MegaMenuBlockData> {\n /** The look behind each `variants` value the site passed to `megaMenuBlock`. */\n variants?: MegaMenuVariants;\n /** The glyph behind each `icons` value the site passed to `megaMenuBlock`. */\n icons?: MegaMenuIcons;\n}\n\n/**\n * One panel, standing alone: what a live preview shows while the row is being\n * edited. A whole header goes through `headerItemsFromBlocks` instead.\n */\nexport function MegaMenuBlockRenderer({\n block,\n linkComponent,\n variants,\n icons,\n}: MegaMenuBlockRendererProps) {\n const panel = megaMenuPanelFromRow(block);\n if (!panel) return null;\n return (\n <MegaMenuPanel\n {...panel}\n variants={variants}\n icons={icons}\n renderLink={renderLinkWith(linkComponent)}\n />\n );\n}\n","import {\n megaMenuToFloatingNavItems,\n type FloatingNavItem,\n type MegaMenuIcons,\n type MegaMenuItem,\n type MegaMenuVariants,\n} from \"@bison-lab/ui\";\n\nimport { renderLinkWith, type BlockLinkComponent } from \"../../link\";\nimport type { MegaMenuBlockData, NavLinkBlockData } from \"../../types\";\nimport { megaMenuPanelFromRow } from \"./component\";\n\n/** A row of a header global's `items`: either block, or whatever else a site added. */\nexport type HeaderBlockRow = MegaMenuBlockData | NavLinkBlockData;\n\nexport interface HeaderItemsOptions {\n variants?: MegaMenuVariants;\n icons?: MegaMenuIcons;\n /** Marks the current section; receives each item's `href`. */\n isActive?: (href: string) => boolean;\n /** The site's link adapter, applied to every link inside the panels. */\n linkComponent?: BlockLinkComponent;\n}\n\n/**\n * A global's `items` as `FloatingNavBlock` items, so a site's header is one\n * call. Rows the bar cannot show are dropped rather than rendered broken: a\n * row with no label, a mega menu with nothing to open, and any block type a\n * site added that this package does not know.\n *\n * A `navLink` row's `newTab` is not carried: `FloatingNavItem` has no such\n * field, because the bar renders its own links.\n */\nexport function headerItemsFromBlocks(\n blocks: HeaderBlockRow[],\n { variants, icons, isActive, linkComponent }: HeaderItemsOptions = {},\n): FloatingNavItem[] {\n const items: MegaMenuItem[] = [];\n for (const row of blocks) {\n if (!row.label) continue;\n if (row.blockType === \"navLink\") {\n if (!row.href) continue;\n items.push({ label: row.label, href: row.href });\n } else if (row.blockType === \"megaMenu\") {\n const panel = megaMenuPanelFromRow(row);\n if (!panel) continue;\n items.push({\n label: row.label,\n ...(row.href ? { href: row.href } : {}),\n panel,\n });\n }\n }\n return megaMenuToFloatingNavItems(\n { items },\n {\n isActive,\n variants,\n icons,\n ...(linkComponent ? { renderLink: renderLinkWith(linkComponent) } : {}),\n },\n );\n}\n"],"mappings":";;;;;;;;;;AAsCA,SAAgB,kBAAkB,EAChC,KACA,KACA,OACA,QACA,WACA,OACA,YACkB;AAClB,QACE,oBAAC,OAAD;EACO;EACA;EACE;EACC;EACG;EACJ;EACP,SAAS,WAAW,UAAU;EAC9B,UAAS;EACT,CAAA;;;;;AC5BN,SAAgB,iBACd,QACiE;AACjE,QAAO,SAAS;EAAE,QAAQ;EAAU,KAAK;EAAuB,GAAG,EAAE;;;;;;;AAQvE,SAAgB,iBAAiB,EAC/B,MACA,QACA,UACA,GAAG,QACc;AACjB,QACE,oBAAC,KAAD;EAAS;EAAM,GAAI;EAAM,GAAI,iBAAiB,OAAO;EAClD;EACC,CAAA;;AAGR,iBAAiB,cAAc;;;;;;AAO/B,SAAgB,eAAe,MAAsC;AACnE,SAAQ,EAAE,MAAM,UAAU,GAAG,WAC3B,oBAAC,MAAD;EAAY;EAAM,QAAQ,KAAK,WAAW;EAAU,GAAI;EACrD;EACI,CAAA;;;;;ACGX,MAAa,oBAAoB;;;;;;;;AAWjC,SAAS,WACP,QACA,UACiD;AACjD,QAAO,OAAO,SAAS,UAAU;EAC/B,MAAM,WAAW,SAAS,MAAM;AAChC,SAAO,WAAW,CAAC;GAAE;GAAO;GAAU,CAAC,GAAG,EAAE;GAC5C;;;AAIJ,SAAS,WAAW,QAA+B;CACjD,MAAM,MAAgB,EAAE;AACxB,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,EACtC,KAAI,KACF,IAAI,KAAK,OAAO,IAAI,GAAG,cAAc,OAAO,GAAG,YAC3C,IAAI,IAAI,KAAK,IACb,EACL;AAEH,QAAO;;;;;;AA0BT,SAAgB,aAAkC,EAChD,QACA,UACA,qBAAqB,mBACrB,iBAAiB,mBACjB,gBAAgB,kBAChB,SAAS,gBAC4B;CACrC,MAAM,OAAO,WACX,QAIA,SACD;CACD,MAAM,OAAO,WAAW,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC;CACrD,MAAM,WAAW,KAAK,KAAK,QAAQ,OAAO,IAAI,MAAW,CAAC;AAC1D,QACE,oBAAA,UAAA,EAAA,UACG,KAAK,KAAK,EAAE,OAAO,YAAY,UAC9B,oBAAC,UAAD;EAES;EACA;EACP,UAAU,QAAQ,IAAI,KAAK,QAAQ,GAAG,MAAM,YAAY,KAAA;EACxD,UAAU,KAAK;EACf,QAAQ,UAAU,KAAK,SAAS;EAChC,cAAc,QAAQ,KAAK,SAAS,QAAQ;EAC5C,cAAc,QAAQ,KAAK,SAAS,KAAK,SAAS,QAAQ;EACtC;EACJ;EACD;EACf,EAXK,MAAM,MAAM,MAWjB,CACF,EACD,CAAA;;;;ACvHP,SAAS,WAAW,OAAsC;AACxD,QAAO,OAAO,UAAU,YAAY,UAAU;;;;;;;;;;;;;;AAehD,SAAgB,aAAa,OAA8C;AACzE,KAAI,CAAC,WAAW,MAAM,CAAE,QAAO,KAAA;CAC/B,MAAM,EAAE,KAAK,KAAK,OAAO,WAAW;AACpC,KAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,EAAG,QAAO,KAAA;AACxD,QAAO;EACL,KAAK;EACL,KAAK,OAAO,QAAQ,WAAW,MAAM;EACrC,GAAI,OAAO,UAAU,WAAW,EAAE,OAAO,GAAG,EAAE;EAC9C,GAAI,OAAO,WAAW,WAAW,EAAE,QAAQ,GAAG,EAAE;EACjD;;;;;;;;AClDH,MAAM,QAAQ;CACZ,OAAO;CACP,MAAM;CACN,OAAO;CACR;;;;;;;;AASD,SAAgB,kBAAkB,EAChC,OACA,OACA,cACA,cACA,oBACA,gBAAgB,OAChB,eAAe,QACqB;CACpC,MAAM,QAAQ,aAAa,MAAM,MAAM;CACvC,MAAM,SAAS,MAAM,SAAS,EAAE,EAAE,QAAQ,SAAS,KAAK,QAAQ,KAAK,MAAM;CAC3E,MAAM,OAAO,MAAM,MAAM,QAAQ;AAEjC,QACE,qBAAC,WAAD;EACE,WAAW,GACT,oCACA,MAGA,YAAY;GAAE;GAAc;GAAc,CAAC,CAC5C;YAPH,CASG,QACC,oBAAC,OAAD;GAAK,WAAU;aACb,oBAAC,OAAD;IACE,KAAK,MAAM;IACX,KAAI;IACJ,OAAO,MAAM;IACb,QAAQ,MAAM;IACd,WAAU;IACV,OAAM;IAEN,UAAU,UAAU;IACpB,CAAA;GACE,CAAA,GACJ,MACJ,oBAAC,OAAD;GAAK,WAAW,GAAG,oBAAoB,iBAAiB;aACtD,qBAAC,OAAD;IAAK,WAAU;cAAf;KACG,MAAM,UACL,oBAAC,KAAD;MAAG,WAAU;gBACV,MAAM;MACL,CAAA,GACF;KACJ,oBAAC,MAAD;MAAI,WAAU;gBACX,MAAM;MACJ,CAAA;KACJ,MAAM,OACL,oBAAC,KAAD;MAAG,WAAU;gBACV,MAAM;MACL,CAAA,GACF;KACH,MAAM,SAAS,IACd,oBAAC,OAAD;MAAK,WAAU;gBACZ,MAAM,KAAK,MAAM,MAChB,oBAAC,QAAD;OAEE,SAAA;OACA,SAAS,MAAM,IAAI,YAAY;iBAE/B,oBAAC,MAAD;QACE,MAAM,KAAK;QACX,QAAQ,KAAK,UAAU;QACvB,GAAI,iBAAiB,KAAK,OAAO;kBAEhC,KAAK;QACD,CAAA;OACA,EAXF,GAAG,KAAK,KAAK,GAAG,IAWd,CACT;MACE,CAAA,GACJ;KACA;;GACF,CAAA,CACE;;;;;AC1Fd,SAAgB,4BAA4B,EAC1C,OACA,cACA,cACA,oBACA,gBAAgB,OAChB,iBAC8C;CAK9C,MAAM,OAAwB,EAAE;CAChC,MAAM,QAA6B,EAAE;AACrC,MAAK,MAAM,CAAC,GAAG,SAAS,MAAM,SAAS,EAAE,EAAE,SAAS,EAAE;EACpD,MAAM,QAAQ,aAAa,IAAI,MAAM;AACrC,MAAI,CAAC,MAAO;AACZ,OAAK,KAAK,MAAM;AAChB,QAAM,KAAK;GACT,IAAI,IAAI,MAAM,OAAO,EAAE;GACvB,OAAO,IAAI,SAAS;GACpB,SAAS,IAAI,WAAW;GACxB,OAAO;IAAE,KAAK,MAAM;IAAK,KAAK,MAAM;IAAK;GACzC,GAAI,IAAI,OAAO,EAAE,MAAM,IAAI,MAAM,GAAG,EAAE;GACtC,GAAI,IAAI,UAAU,EAAE,SAAS,IAAI,SAAS,GAAG,EAAE;GAChD,CAAC;;AAEJ,KAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,QACE,oBAAC,WAAD;EAAS,WAAW,YAAY;GAAE;GAAc;GAAc,CAAC;YAC7D,oBAAC,OAAD;GAAK,WAAW,GAAG,oBAAoB,iBAAiB;aACtD,oBAAC,qBAAD;IACS;IACP,cAAc,MAAM,gBAAgB;IACpC,oBAAoB,KAAK,IACvB,KAAK,IAAI,MAAM,sBAAsB,GAAG,EAAE,EAC1C,MAAM,SAAS,EAChB;IACD,GAAK,MAAM,YAAY,EAAE,WAAW,MAAM,WAAW,GAAG,EAAE;IAC1D,YAAY,eAAe,cAAc;IACzC,cAAc,MAAM,MAClB,oBAAC,OAAD;KACE,KAAK,KAAK,MAAM;KAChB,KAAK,KAAK,MAAM,OAAO;KACvB,OAAO,KAAK,IAAI;KAChB,QAAQ,KAAK,IAAI;KACjB,WAAU;KACV,OAAM;KACN,CAAA;IAEJ,CAAA;GACE,CAAA;EACE,CAAA;;;;ACtDd,SAAgB,0BAA0B,EACxC,OACA,cACA,cACA,oBACA,gBAAgB,SAC4B;CAG5C,MAAM,OAAwB,EAAE;CAChC,MAAM,QAA2B,EAAE;AACnC,MAAK,MAAM,CAAC,GAAG,SAAS,MAAM,SAAS,EAAE,EAAE,SAAS,EAAE;EACpD,MAAM,QAAQ,aAAa,IAAI,MAAM;AACrC,MAAI,CAAC,MAAO;AACZ,OAAK,KAAK,MAAM;AAChB,QAAM,KAAK;GACT,IAAI,IAAI,MAAM,OAAO,EAAE;GACvB,OAAO,IAAI,SAAS;GACpB,aAAa,IAAI,eAAe;GAChC,OAAO;IAAE,KAAK,MAAM;IAAK,KAAK,MAAM;IAAK;GAC1C,CAAC;;AAEJ,KAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,QACE,oBAAC,WAAD;EAAS,WAAW,YAAY;GAAE;GAAc;GAAc,CAAC;YAC7D,oBAAC,OAAD;GAAK,WAAW,GAAG,oBAAoB,iBAAiB;aACtD,oBAAC,mBAAD;IACS;IACP,aAAa,MAAM,eAAe;IAClC,qBAAqB,MAAM,uBAAuB;IAClD,cAAc,MAAM,gBAAgB;IACpC,oBAAoB,KAAK,IACvB,KAAK,IAAI,MAAM,sBAAsB,GAAG,EAAE,EAC1C,MAAM,SAAS,EAChB;IACD,cAAc,MAAM,MAClB,oBAAC,OAAD;KACE,KAAK,KAAK,MAAM;KAChB,KAAK,KAAK,MAAM,OAAO;KACvB,OAAO,KAAK,IAAI;KAChB,QAAQ,KAAK,IAAI;KACjB,WAAU;KACV,OAAM;KACN,CAAA;IAEJ,CAAA;GACE,CAAA;EACE,CAAA;;;;AChDd,SAAgB,wBAAwB,EACtC,OACA,cACA,cACA,oBACA,iBAC0C;CAC1C,MAAM,SAAoB,MAAM,SAAS,EAAE,EACxC,QAAQ,QAAQ,IAAI,YAAY,IAAI,OAAO,CAC3C,KAAK,KAAK,OAAO;EAChB,IAAI,IAAI,MAAM,OAAO,EAAE;EACvB,UAAU,IAAI;EAGd,QAAQ,oBAAC,QAAD;GAAM,WAAU;aAAuB,IAAI;GAAc,CAAA;EAClE,EAAE;AACL,KAAI,MAAM,WAAW,EAAG,QAAO;CAE/B,MAAM,MAAM,MAAM;CAClB,MAAM,SAAS,QAAQ,KAAK,QAAQ,KAAK,SAAS;AAElD,QACE,oBAAC,WAAD;EAAS,WAAW,YAAY;GAAE;GAAc;GAAc,CAAC;YAC7D,oBAAC,OAAD;GAAK,WAAW,GAAG,oBAAoB,iBAAiB;aACtD,oBAAC,iBAAD;IAGE,WAAU;IACV,oBAAmB;IACZ;IACP,OAAO,MAAM,SAAS;IACtB,GAAK,MAAM,UAAU,EAAE,SAAS,MAAM,SAAS,GAAG,EAAE;IACpD,GAAK,MAAM,cAAc,EAAE,aAAa,MAAM,aAAa,GAAG,EAAE;IAChE,QAAQ,MAAM,UAAU;IACxB,MAAM,MAAM,QAAQ;IACpB,aAAa,MAAM,eAAe;IAClC,YAAY,eAAe,cAAc;IACzC,GAAK,UAAU,MACX,EACE,KAAK;KACH,OAAO,IAAI,SAAS;KACpB,UAAU,IAAI;KACd,MAAM,IAAI;KACV,QAAQ,IAAI,UAAU;KACvB,EACF,GACD,EAAE;IACN,CAAA;GACE,CAAA;EACE,CAAA;;;;AChDd,SAAgB,gCAAgC,EAC9C,OACA,cACA,cACA,oBACA,iBACkD;CAClD,MAAM,SAA4B,MAAM,SAAS,EAAE,EAChD,QAAQ,QAAQ,IAAI,WAAW,IAAI,QAAQ,KAAK,CAChD,KAAK,KAAK,MAAM;EACf,MAAM,SAAS,aAAa,IAAI,QAAQ,OAAO;AAC/C,SAAO;GACL,IAAI,IAAI,MAAM,OAAO,EAAE;GACvB,SAAS,IAAI;GACb,QAAQ;IACN,MAAM,IAAI,QAAQ;IAClB,GAAI,IAAI,QAAQ,QAAQ,EAAE,OAAO,IAAI,OAAO,OAAO,GAAG,EAAE;IAGxD,GAAI,SAAS,EAAE,WAAW,OAAO,KAAK,GAAG,EAAE;IAC5C;GACF;GACD;AACJ,KAAI,MAAM,WAAW,EAAG,QAAO;CAE/B,MAAM,OAAO,MAAM;CACnB,MAAM,UAAU,QAAQ,MAAM,QAAQ,MAAM,MAAM;AAElD,QAEE,oBAAC,WAAD;EAAS,WAAW,GAAG,aAAa,YAAY;GAAE;GAAc;GAAc,CAAC,CAAC;YAC9E,oBAAC,OAAD;GAAK,WAAW,GAAG,oBAAoB,iBAAiB;aACtD,oBAAC,oBAAD;IACE,WAAU;IACV,oBAAmB;IACZ;IACP,OAAO,MAAM,SAAS;IACtB,YAAY,eAAe,cAAc;IACzC,GAAK,MAAM,UAAU,EAAE,SAAS,MAAM,SAAS,GAAG,EAAE;IACpD,GAAK,MAAM,cAAc,EAAE,aAAa,MAAM,aAAa,GAAG,EAAE;IAChE,GAAK,OAAO,MAAM,oBAAoB,WAClC,EAAE,iBAAiB,MAAM,iBAAiB,GAC1C,EAAE;IACN,GAAK,OAAO,MAAM,mBAAmB,WACjC,EAAE,gBAAgB,MAAM,gBAAgB,GACxC,EAAE;IACN,GAAK,WAAW,OACZ,EACE,MAAM;KACJ,OAAO,KAAK;KACZ,MAAM,KAAK;KACX,QAAQ,KAAK,UAAU;KACxB,EACF,GACD,EAAE;IACN,CAAA;GACE,CAAA;EACE,CAAA;;;;ACrDd,SAAgB,iBAAiB,EAC/B,OACA,cACA,cACA,oBACA,iBACmC;CACnC,MAAM,UAAU,MAAM;CACtB,MAAM,eAAgC,MAAM,eAAe,EAAE,EAI1D,QAAQ,QAAQ,IAAI,QAAQ,IAAI,UAAU,CAC1C,KAAK,KAAK,OAAO;EAChB,IAAI,IAAI,MAAM,OAAO,EAAE;EACvB,MAAM,IAAI;EACV,GAAI,IAAI,iBAAiB,EAAE,MAAM,IAAI,gBAAgB,GAAG,EAAE;EAC1D,OAAO;GACL,MAAM,IAAI;GACV,GAAI,IAAI,eAAe,EAAE,SAAS,IAAI,cAAc,GAAG,EAAE;GAC1D;EACF,EAAE;AAEL,KAAI,CAAC,MAAM,gBAAgB,CAAC,SAAS,iBAAiB,YAAY,WAAW,EAC3E,QAAO;CAET,MAAM,SAAoB;EACxB,MAAM,MAAM;EACZ,MAAM,MAAM,gBAAgB;EAC5B,SAAS;GACP,eAAe,QAAQ;GACvB,iBAAiB,QAAQ,mBAAmB;GAC5C,eAAe,QAAQ,iBAAiB;GACxC,YAAY,QAAQ,cAAc;GAClC,GAAI,QAAQ,iBACR,EAAE,gBAAgB,QAAQ,gBAAgB,GAC1C,EAAE;GACP;EACD;EACA,GAAI,MAAM,MAAM,EAAE,KAAK,MAAM,KAAK,GAAG,EAAE;EACxC;AAED,QACE,oBAAC,WAAD;EAAS,WAAW,YAAY;GAAE;GAAc;GAAc,CAAC;YAC7D,qBAAC,OAAD;GAAK,WAAW,GAAG,oBAAoB,iBAAiB;aAAxD,CACE,oBAAC,UAAD;IACU;IACR,UAAU,MAAM,YAAY;IAC5B,cAAc,MAAM,gBAAgB;IACpC,YAAY,eAAe,cAAc;IACzC,CAAA,EACD,MAAM,eAAe,QAAQ,OAAO,oBAAC,QAAD,EAAQ,MAAM,QAAU,CAAA,CACzD;;EACE,CAAA;;;;;ACrDd,SAAS,YAAY,OAAwD;CAC3E,MAAM,IAAI,OAAO,SAAS,EAAE;AAC5B,QAAQ,OAAO,UAAU,EAAE,IAAI,KAAK,KAAK,KAAK,IAAI,IAAI;;AAGxD,SAAgB,uBAAuB,EACrC,OACA,OACA,QACA,cACA,cACA,oBACA,iBACyC;CAGzC,MAAM,SAAqB,MAAM,SAAS,EAAE,EAAE,SAAS,KAAK,MAC1D,IAAI,QACA,CACE;EACE,IAAI,IAAI,MAAM,OAAO,EAAE;EACvB,OAAO,IAAI;EACX,OAAO,IAAI,SAAS;EACpB,GAAI,IAAI,OAAO,EAAE,MAAM,IAAI,MAAM,GAAG,EAAE;EACtC,GAAI,IAAI,OAAO,EAAE,MAAM,MAAM,GAAG,EAAE;EACnC,CACF,GACD,EAAE,CACP;AACD,KAAI,MAAM,SAAS,EAAG,QAAO;CAW7B,MAAM,SAAS,QAAQ,MAAM,QAAQ;CACrC,MAAM,YAAY,UAAU,QAAQ,KAAK,CAAC;CAC1C,MAAM,YAAY,UAAU,CAAC,UAAU,CAAC;AAExC,QACE,oBAAC,WAAD;EACE,WAAW,GACT,YACA,UAAU,QACV,YAAY;GAAE;GAAc;GAAc,CAAC,CAC5C;YAED,oBAAC,OAAD;GACE,WAAW,GACT,oBACA,YAAY,eAAe,kBAC3B,YAAY,iBAAiB,iBAC9B;aAED,oBAAC,gBAAD;IACS;IACP,SAAS,YAAY,MAAM,QAAQ;IACnC,MAAM,MAAM,QAAQ;IACpB,OAAO,MAAM,SAAS;IACtB,SAAU,CAAC,UAAU,MAAM,WAAY,KAAA;IACvC,OAAQ,CAAC,UAAU,MAAM,SAAU,KAAA;IACnC,aAAc,CAAC,UAAU,MAAM,eAAgB,KAAA;IAC/C,oBAAmB;IACnB,YAAY,eAAe,cAAc;IACzC,CAAA;GACE,CAAA;EACE,CAAA;;;;;;;;;;;AC7Ed,MAAM,YAAY;;AAGlB,SAAgB,SACd,OACuB;CACvB,MAAM,QAAQ,UAAU,KAAK,SAAS,GAAG;AACzC,QAAO,QAAS,GAAG,OAAO,MAAM,GAAG,CAAC,OAA0B;;;;ACEhE,SAAS,aAAa,KAA4C;AAChE,KAAI,CAAC,IAAI,SAAS,CAAC,IAAI,KAAM,QAAO;AACpC,QAAO;EACL,OAAO,IAAI;EACX,MAAM,IAAI;EACV,GAAI,IAAI,cAAc,EAAE,aAAa,IAAI,aAAa,GAAG,EAAE;EAC3D,GAAI,IAAI,UAAU,EAAE,SAAS,IAAI,SAAS,GAAG,EAAE;EAC/C,GAAI,IAAI,OAAO,EAAE,MAAM,IAAI,MAAM,GAAG,EAAE;EACtC,GAAI,IAAI,SAAS,EAAE,QAAQ,MAAM,GAAG,EAAE;EACvC;;;;;;;AAQH,SAAgB,qBACd,KAC0B;CAC1B,MAAM,QAAQ,IAAI;AAClB,KAAI,CAAC,OAAO,SAAS,OAAQ,QAAO;CACpC,MAAM,SAAS,QAAQ,MAAM,aAAa;CAE1C,MAAM,UAA4B,EAAE;AACpC,MAAK,MAAM,UAAU,MAAM,SAAS;EAClC,MAAM,WAA8B,EAAE;AACtC,OAAK,MAAM,WAAW,OAAO,YAAY,EAAE,EAAE;GAC3C,MAAM,SAAS,QAAQ,SAAS,EAAE,EAC/B,IAAI,aAAa,CACjB,QAAQ,SAA+B,SAAS,KAAK;AACxD,OAAI,CAAC,MAAM,OAAQ;AACnB,YAAS,KAAK;IACZ,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,SAAS,GAAG,EAAE;IACvD,SAAS,QAAQ,WAAW;IAC5B,0BAA0B,QAAQ,4BAA4B;IAC9D;IACD,CAAC;;AAEJ,UAAQ,KAAK;GACX,GAAI,UAAU,OAAO,cACjB,EAAE,aAAa,OAAO,aAAa,GACnC,EAAE,OAAO,OAAO,OAAO,SAAS,MAAM,EAAmB;GAC7D,GAAI,OAAO,UAAU,EAAE,SAAS,MAAM,GAAG,EAAE;GAC3C;GACD,CAAC;;AAEJ,KAAI,CAAC,QAAQ,MAAM,WAAW,OAAO,SAAS,OAAO,CAAE,QAAO;CAE9D,MAAM,WAAW,MAAM,QAAQ;CAC/B,MAAM,MAAM,MAAM,QAAQ;AAK1B,QAAO;EACL,UAHgB,SAAS,MAAM,eAAe,IAGvB,MAAM,YAAY;EACzC;EACA,QAAQ;GACN,GAAI,UAAU,SAAS,SAAS,OAC5B,EACE,UAAU;IACR,OAAO,SAAS;IAChB,MAAM,SAAS;IACf,GAAI,SAAS,SAAS,EAAE,QAAQ,MAAM,GAAG,EAAE;IAC5C,EACF,GACD,EAAE;GACN,GAAI,KAAK,SAAS,IAAI,OAClB,EACE,KAAK;IACH,OAAO,IAAI;IACX,MAAM,IAAI;IACV,GAAI,IAAI,SAAS,EAAE,QAAQ,MAAM,GAAG,EAAE;IACvC,EACF,GACD,EAAE;GACP;EACF;;;;;;AAeH,SAAgB,sBAAsB,EACpC,OACA,eACA,UACA,SAC6B;CAC7B,MAAM,QAAQ,qBAAqB,MAAM;AACzC,KAAI,CAAC,MAAO,QAAO;AACnB,QACE,oBAAC,eAAD;EACE,GAAI;EACM;EACH;EACP,YAAY,eAAe,cAAc;EACzC,CAAA;;;;;;;;;;;;;AC1FN,SAAgB,sBACd,QACA,EAAE,UAAU,OAAO,UAAU,kBAAsC,EAAE,EAClD;CACnB,MAAM,QAAwB,EAAE;AAChC,MAAK,MAAM,OAAO,QAAQ;AACxB,MAAI,CAAC,IAAI,MAAO;AAChB,MAAI,IAAI,cAAc,WAAW;AAC/B,OAAI,CAAC,IAAI,KAAM;AACf,SAAM,KAAK;IAAE,OAAO,IAAI;IAAO,MAAM,IAAI;IAAM,CAAC;aACvC,IAAI,cAAc,YAAY;GACvC,MAAM,QAAQ,qBAAqB,IAAI;AACvC,OAAI,CAAC,MAAO;AACZ,SAAM,KAAK;IACT,OAAO,IAAI;IACX,GAAI,IAAI,OAAO,EAAE,MAAM,IAAI,MAAM,GAAG,EAAE;IACtC;IACD,CAAC;;;AAGN,QAAO,2BACL,EAAE,OAAO,EACT;EACE;EACA;EACA;EACA,GAAI,gBAAgB,EAAE,YAAY,eAAe,cAAc,EAAE,GAAG,EAAE;EACvE,CACF"}
1
+ {"version":3,"file":"react.mjs","names":["defaultResolveLink","resolveLink"],"sources":["../src/image.tsx","../src/render-blocks.tsx","../src/media.ts","../src/blocks/hero/component.tsx","../src/blocks/showcase-panels/component.tsx","../src/blocks/process-steps/component.tsx","../src/blocks/faq-columns/component.tsx","../src/blocks/testimonial-masonry/component.tsx","../src/blocks/nap/component.tsx","../src/blocks/stats-band/component.tsx","../src/blocks/mega-menu/rem-width.ts","../src/blocks/mega-menu/component.tsx","../src/blocks/mega-menu/header.tsx"],"sourcesContent":["import type { ComponentType } from \"react\";\n\n/**\n * The image injection point.\n *\n * The package deliberately does not depend on `next` — a Payload site is not\n * necessarily a Next.js site, and `next/image` needs per-site configuration\n * (`remotePatterns` for the media route) that a package cannot supply. So\n * renderers never construct an image element themselves; they render the\n * component they were handed. A Next site writes one adapter around\n * `next/image` and every block gets optimisation from it.\n *\n * This is a *component type*, not a render function, on purpose: a Server\n * Component may pass a client component reference across the boundary, but not\n * a closure. `imageComponent` therefore survives the trip from a server page\n * into these client renderers, where a `renderImage` callback would not.\n */\n\nexport interface BlockImageProps {\n src: string;\n /** Always a string — see `resolveMedia`. Empty means decorative. */\n alt: string;\n width?: number;\n height?: number;\n className?: string;\n /** Passed through to `next/image`'s `sizes`; ignored by the default. */\n sizes?: string;\n /** Set on the one image above the fold, typically the hero's. */\n priority?: boolean;\n}\n\nexport type BlockImageComponent = ComponentType<BlockImageProps>;\n\n/**\n * The fallback when no `imageComponent` is supplied: a plain `<img>`. Correct\n * everywhere and optimised nowhere, which is the right default for a package\n * that cannot know what framework it landed in.\n */\nexport function DefaultBlockImage({\n src,\n alt,\n width,\n height,\n className,\n sizes,\n priority,\n}: BlockImageProps) {\n return (\n <img\n src={src}\n alt={alt}\n width={width}\n height={height}\n className={className}\n sizes={sizes}\n loading={priority ? \"eager\" : \"lazy\"}\n decoding=\"async\"\n />\n );\n}\n","import type { ComponentType, ReactElement } from \"react\";\n\nimport { resolveLink as defaultResolveLink, type ResolveLink } from \"./fields/link\";\nimport { type BlockImageComponent, DefaultBlockImage } from \"./image\";\nimport { type BlockLinkComponent, DefaultBlockLink } from \"./link\";\nimport { overlapsSeam } from \"./seam\";\n\n/** The minimum a row must be for the dispatch to place it. */\nexport interface BlockLike {\n blockType: string;\n id?: string | null;\n}\n\n/**\n * What every renderer is handed.\n *\n * The neighbour props let a block adapt to what sits around it: `prevType` to\n * close a seam against the block above, `runIndex` to alternate surfaces across\n * back-to-back blocks of one type, `isLast` because the final block sits\n * against the site footer, and `overlapAbove` / `overlapBelow` when the\n * neighbour on that side floats across the seam (see `seam.ts`).\n *\n * `containerClassName`, `imageComponent`, `linkComponent` and `resolveLink`\n * are the four things a package block cannot decide for itself — the site's\n * page measure, how an image gets optimised, which router a link goes\n * through, and what path a CMS page lives at. All four arrive already\n * defaulted, so a renderer never checks them.\n */\nexport interface BlockRendererProps<B extends BlockLike = BlockLike> {\n block: B;\n /** Position on the page, hero included. */\n index: number;\n /** The type of the block before this one; absent on the first block. */\n prevType?: string;\n /** Position within a run of consecutive same-type blocks, from 0. */\n runIndex: number;\n isLast: boolean;\n /**\n * The block before this one floats down over the seam, so this block's band\n * wrapper needs clearance at the top. Decided by `RenderBlocks`' `floats`.\n */\n overlapAbove?: boolean;\n /** The block after this one floats up over the seam; clearance at the bottom. */\n overlapBelow?: boolean;\n /** The site's page measure, for the band wrapper. */\n containerClassName: string;\n /** How this block turns a resolved image into an element. */\n imageComponent: BlockImageComponent;\n /** How this block turns an `href` into an element. */\n linkComponent: BlockLinkComponent;\n /** How this block turns a link's destination into an `href`; `null` renders the label as text. */\n resolveLink: ResolveLink;\n}\n\n/**\n * One renderer per block type, each typed to its own block.\n *\n * Written as a mapped type over the union so `hero` gets `HeroBlockData`, not\n * the whole union. A site writes `satisfies BlockRegistryFor<AnyBlock>` where\n * `AnyBlock` comes off its *generated* `Page` type — that is the compile-time\n * layout lock, and it has to stay in the site, because only the site has\n * generated types. The registry is a parameter here for exactly that reason.\n */\nexport type BlockRegistryFor<B extends BlockLike> = {\n [K in B[\"blockType\"]]: ComponentType<\n BlockRendererProps<Extract<B, { blockType: K }>>\n >;\n};\n\n/** Fallback page measure for a site that does not pass its own. */\nexport const DEFAULT_CONTAINER = \"mx-auto w-full max-w-7xl px-6\";\n\n/**\n * The attribute a block editor in the Payload admin (payload-better-editor)\n * walks up to from a click in its preview iframe, to find which row was\n * clicked. `RenderBlocks` puts it on a wrapper around every block, so a site\n * adopting the editor has nothing to add per block; a site's tests can assert\n * against the name here rather than retype it.\n */\nexport const BLOCK_ID_ATTRIBUTE = \"data-better-editor-id\";\n\ntype LooseRenderer = ComponentType<BlockRendererProps<BlockLike>>;\n\n/**\n * A row saved against a block that has since left the config has no renderer.\n * It is dropped before anything is counted, so its neighbours see each other,\n * not a gap: the block after it gets the real `prevType`, a run of one type is\n * not broken by it, and the block before it is still `isLast` when it ends the\n * page.\n */\nfunction renderable(\n blocks: BlockLike[],\n registry: Record<string, LooseRenderer | undefined>,\n): { block: BlockLike; Renderer: LooseRenderer }[] {\n return blocks.flatMap((block) => {\n const Renderer = registry[block.blockType];\n return Renderer ? [{ block, Renderer }] : [];\n });\n}\n\n/** Position of each block within its run of consecutive same-type blocks. */\nfunction runIndexes(blocks: BlockLike[]): number[] {\n const out: number[] = [];\n for (let i = 0; i < blocks.length; i += 1) {\n out.push(\n i > 0 && blocks[i - 1].blockType === blocks[i].blockType\n ? out[i - 1] + 1\n : 0,\n );\n }\n return out;\n}\n\nexport interface RenderBlocksProps<B extends BlockLike> {\n /** Pass `[...page.hero, ...page.layout]`, so the hero counts as index 0. */\n blocks: B[];\n registry: BlockRegistryFor<B>;\n /** The site's page measure. Defaults to `DEFAULT_CONTAINER`. */\n containerClassName?: string;\n /** Defaults to a plain `<img>`; a Next site passes a `next/image` adapter. */\n imageComponent?: BlockImageComponent;\n /** Defaults to a plain `<a>`; a Next site passes a `next/link` adapter. */\n linkComponent?: BlockLinkComponent;\n /**\n * Which rows float across their seams, so their neighbours make room.\n * Defaults to `overlapsSeam`: the package's stats band with `overlap` on. A\n * site with its own floating block extends it:\n * `floats={(row) => overlapsSeam(row) || row.blockType === \"bookingCard\"}`.\n */\n floats?: (block: B) => boolean;\n /**\n * Defaults to `resolveLink`, which makes a page link `/<slug>`. A site\n * whose routes differ passes its own; like `linkComponent`, it must be\n * exported from a client module, since a Server Component cannot hand a\n * closure across the boundary.\n */\n resolveLink?: ResolveLink;\n}\n\n/**\n * Renders a page's blocks in order through the registry. A Server Component:\n * it only maps and looks up, and each renderer decides its own nature.\n *\n * Each block sits inside an unstyled block-level `<div>` carrying\n * `BLOCK_ID_ATTRIBUTE`, which is what a click-to-edit overlay resolves and\n * what its hover outline paints on. A row with no `id` (a fixture, a row the\n * form has not saved) gets no attribute: there is no form-state path for an\n * editor to open. The band's own `<section>` and its full-bleed background\n * are unchanged by the wrapper.\n */\nexport function RenderBlocks<B extends BlockLike>({\n blocks,\n registry,\n containerClassName = DEFAULT_CONTAINER,\n imageComponent = DefaultBlockImage,\n linkComponent = DefaultBlockLink,\n floats = overlapsSeam,\n resolveLink = defaultResolveLink,\n}: RenderBlocksProps<B>): ReactElement {\n const rows = renderable(\n blocks,\n // The mapped registry type is per-block, and the dispatch is not; the\n // lookup below is what guarantees each renderer only ever sees its own\n // block type, and no signature can express that to the compiler.\n registry as unknown as Record<string, LooseRenderer | undefined>,\n );\n const runs = runIndexes(rows.map((row) => row.block));\n const floating = rows.map((row) => floats(row.block as B));\n return (\n <>\n {rows.map(({ block, Renderer }, index) => (\n <div\n key={block.id ?? index}\n {...(block.id ? { [BLOCK_ID_ATTRIBUTE]: block.id } : {})}\n >\n <Renderer\n block={block}\n index={index}\n prevType={index > 0 ? rows[index - 1].block.blockType : undefined}\n runIndex={runs[index]}\n isLast={index === rows.length - 1}\n overlapAbove={index > 0 && floating[index - 1]}\n overlapBelow={index < rows.length - 1 && floating[index + 1]}\n containerClassName={containerClassName}\n imageComponent={imageComponent}\n linkComponent={linkComponent}\n resolveLink={resolveLink}\n />\n </div>\n ))}\n </>\n );\n}\n","/**\n * The upload side of the boundary.\n *\n * A Payload upload field holds either the row's id or, once depth resolves it,\n * the whole document. Neither shape can be imported from here: `Media` is\n * generated per site, and this package must never reach for a site's\n * `payload-types`. So the doc is described structurally, and every renderer\n * goes through `resolveMedia` rather than reading `.url` itself.\n */\n\n/**\n * The subset of a Payload upload document a block renderer actually reads.\n * Every site's generated `Media` interface is assignable to this, whatever\n * else it carries.\n *\n * Deliberately no index signature: TypeScript refuses to assign an interface\n * to a type with one, and a site's generated `Media` is always an interface.\n * An index signature here would break exactly the structural match this type\n * exists to provide.\n */\nexport interface MediaDoc {\n url?: string | null;\n alt?: string | null;\n width?: number | null;\n height?: number | null;\n}\n\n/** What an upload field holds before and after `depth` populates it. */\nexport type MediaValue = number | string | MediaDoc | null | undefined;\n\n/** A resolved image, in the shape every `@bison-lab/ui` block asks for. */\nexport interface ResolvedMedia {\n src: string;\n alt: string;\n width?: number;\n height?: number;\n}\n\nfunction isMediaDoc(value: MediaValue): value is MediaDoc {\n return typeof value === \"object\" && value !== null;\n}\n\n/**\n * Narrows an upload value to something renderable, or `undefined`.\n *\n * `undefined` covers three cases a renderer must treat identically: the field\n * is empty, `depth: 0` left it as a bare id, or the document exists but has no\n * `url` yet (an upload mid-flight). A renderer that gets `undefined` omits the\n * image rather than emitting a broken `<img>`.\n *\n * `alt` is always a string: the media collection requires it, but a draft saved\n * before that validation ran can still reach a renderer, and an `alt` of\n * `undefined` would silently drop the attribute entirely.\n */\nexport function resolveMedia(value: MediaValue): ResolvedMedia | undefined {\n if (!isMediaDoc(value)) return undefined;\n const { url, alt, width, height } = value;\n if (typeof url !== \"string\" || url.length === 0) return undefined;\n return {\n src: url,\n alt: typeof alt === \"string\" ? alt : \"\",\n ...(typeof width === \"number\" ? { width } : {}),\n ...(typeof height === \"number\" ? { height } : {}),\n };\n}\n","import { Button } from \"@bison-lab/ui\";\n\nimport { cx } from \"../../cx\";\nimport { newTabAttributes } from \"../../link\";\nimport { resolveMedia } from \"../../media\";\nimport type { BlockRendererProps } from \"../../render-blocks\";\nimport { seamClasses } from \"../../seam\";\nimport type { HeroBlockData } from \"../../types\";\n\n/**\n * How each tone paints. Semantic tokens only — a site's brand reaches these\n * through its theme, never through a class named here.\n */\nconst TONES = {\n sweep: \"bg-primary text-primary-foreground\",\n dark: \"bg-foreground text-background\",\n light: \"bg-background text-foreground\",\n} as const;\n\n/**\n * The package's hero: deliberately plain.\n *\n * A hero is where a site's brand is loudest, so this exists to make a page\n * render at all, not to be the final answer. A site overrides this one registry\n * entry with its own and keeps every other block.\n */\nexport function HeroBlockRenderer({\n block,\n index,\n overlapAbove,\n overlapBelow,\n containerClassName,\n imageComponent: Image,\n linkComponent: Link,\n resolveLink: resolve,\n}: BlockRendererProps<HeroBlockData>) {\n const image = resolveMedia(block.image);\n // A call to action with no destination (its page missing or unpublished,\n // or nothing picked yet on a draft) keeps its label as text.\n const links = (block.links ?? []).flatMap((link) =>\n link.label\n ? [{ label: link.label, href: resolve(link), newTab: link.newTab ?? false }]\n : [],\n );\n const tone = TONES[block.tone ?? \"sweep\"];\n\n return (\n <section\n className={cx(\n \"relative isolate overflow-hidden\",\n tone,\n // Symmetric padding by default; the extra below appears only when an\n // editor drops an overlapping band under the hero.\n seamClasses({ overlapAbove, overlapBelow }),\n )}\n >\n {image ? (\n <div className=\"absolute inset-0 -z-10 opacity-25\">\n <Image\n src={image.src}\n alt=\"\"\n width={image.width}\n height={image.height}\n className=\"h-full w-full object-cover\"\n sizes=\"100vw\"\n // The hero is the page's LCP element whenever it opens the page.\n priority={index === 0}\n />\n </div>\n ) : null}\n <div className={cx(containerClassName, \"py-20 lg:py-28\")}>\n <div className=\"max-w-3xl\">\n {block.eyebrow ? (\n <p className=\"text-sm font-semibold tracking-widest uppercase opacity-80\">\n {block.eyebrow}\n </p>\n ) : null}\n <h1 className=\"mt-3 text-4xl font-bold tracking-tight text-balance lg:text-5xl\">\n {block.heading}\n </h1>\n {block.body ? (\n <p className=\"mt-6 max-w-2xl text-lg leading-relaxed opacity-90\">\n {block.body}\n </p>\n ) : null}\n {links.length > 0 ? (\n <div className=\"mt-8 flex flex-wrap gap-3\">\n {links.map((link, i) => (\n <Button\n key={`${link.href ?? link.label}-${i}`}\n asChild\n variant={i === 0 ? \"default\" : \"outline\"}\n >\n {link.href ? (\n <Link\n href={link.href}\n newTab={link.newTab}\n {...newTabAttributes(link.newTab)}\n >\n {link.label}\n </Link>\n ) : (\n <span>{link.label}</span>\n )}\n </Button>\n ))}\n </div>\n ) : null}\n </div>\n </div>\n </section>\n );\n}\n","import { ShowcasePanelsBlock, type ShowcasePanelItem } from \"@bison-lab/ui\";\n\nimport { cx } from \"../../cx\";\nimport { renderLinkWith } from \"../../link\";\nimport { resolveMedia, type ResolvedMedia } from \"../../media\";\nimport type { BlockRendererProps } from \"../../render-blocks\";\nimport { seamClasses } from \"../../seam\";\nimport type { ShowcasePanelsBlockData } from \"../../types\";\n\nexport function ShowcasePanelsBlockRenderer({\n block,\n overlapAbove,\n overlapBelow,\n containerClassName,\n imageComponent: Image,\n linkComponent,\n}: BlockRendererProps<ShowcasePanelsBlockData>) {\n // A panel without a picture has nothing to expand into, so a row whose\n // upload has not resolved is dropped rather than rendered empty. `dims`\n // stays index-aligned with `items` so the image callback can reach the\n // width and height the library's own prop shape has no room for.\n const dims: ResolvedMedia[] = [];\n const items: ShowcasePanelItem[] = [];\n for (const [i, row] of (block.items ?? []).entries()) {\n const image = resolveMedia(row.image);\n if (!image) continue;\n dims.push(image);\n items.push({\n id: row.id ?? String(i),\n title: row.title ?? \"\",\n summary: row.summary ?? \"\",\n image: { src: image.src, alt: image.alt },\n ...(row.href ? { href: row.href } : {}),\n ...(row.numeral ? { numeral: row.numeral } : {}),\n });\n }\n if (items.length === 0) return null;\n\n return (\n <section className={seamClasses({ overlapAbove, overlapBelow })}>\n <div className={cx(containerClassName, \"py-16 lg:py-24\")}>\n <ShowcasePanelsBlock\n items={items}\n spineVariant={block.spineVariant ?? \"numbered\"}\n defaultActiveIndex={Math.min(\n Math.max(block.defaultActiveIndex ?? 0, 0),\n items.length - 1,\n )}\n {...(block.watermark ? { watermark: block.watermark } : {})}\n renderLink={renderLinkWith(linkComponent)}\n renderImage={(item, i) => (\n <Image\n src={item.image.src}\n alt={item.image.alt ?? \"\"}\n width={dims[i]?.width}\n height={dims[i]?.height}\n className=\"absolute inset-0 h-full w-full object-cover\"\n sizes=\"(min-width: 1024px) 50vw, 100vw\"\n />\n )}\n />\n </div>\n </section>\n );\n}\n","import { ProcessStepsBlock, type ProcessStepItem } from \"@bison-lab/ui\";\n\nimport { cx } from \"../../cx\";\nimport { resolveMedia, type ResolvedMedia } from \"../../media\";\nimport type { BlockRendererProps } from \"../../render-blocks\";\nimport { seamClasses } from \"../../seam\";\nimport type { ProcessStepsBlockData } from \"../../types\";\n\nexport function ProcessStepsBlockRenderer({\n block,\n overlapAbove,\n overlapBelow,\n containerClassName,\n imageComponent: Image,\n}: BlockRendererProps<ProcessStepsBlockData>) {\n // Same rule as the showcase panels: a step is its picture, so an unresolved\n // upload drops the row instead of rendering a hole in the rail.\n const dims: ResolvedMedia[] = [];\n const items: ProcessStepItem[] = [];\n for (const [i, row] of (block.items ?? []).entries()) {\n const image = resolveMedia(row.image);\n if (!image) continue;\n dims.push(image);\n items.push({\n id: row.id ?? String(i),\n title: row.title ?? \"\",\n description: row.description ?? \"\",\n image: { src: image.src, alt: image.alt },\n });\n }\n if (items.length === 0) return null;\n\n return (\n <section className={seamClasses({ overlapAbove, overlapBelow })}>\n <div className={cx(containerClassName, \"py-16 lg:py-24\")}>\n <ProcessStepsBlock\n items={items}\n autoAdvance={block.autoAdvance ?? true}\n autoAdvanceDuration={block.autoAdvanceDuration ?? 6000}\n pauseOnHover={block.pauseOnHover ?? true}\n defaultActiveIndex={Math.min(\n Math.max(block.defaultActiveIndex ?? 0, 0),\n items.length - 1,\n )}\n renderImage={(item, i) => (\n <Image\n src={item.image.src}\n alt={item.image.alt ?? \"\"}\n width={dims[i]?.width}\n height={dims[i]?.height}\n className=\"size-full object-cover\"\n sizes=\"(min-width: 768px) 66vw, 100vw\"\n />\n )}\n />\n </div>\n </section>\n );\n}\n","import { FAQColumnsBlock, type FAQItem } from \"@bison-lab/ui\";\n\nimport { cx } from \"../../cx\";\nimport { renderLinkWith } from \"../../link\";\nimport type { BlockRendererProps } from \"../../render-blocks\";\nimport { seamClasses } from \"../../seam\";\nimport type { FaqColumnsBlockData } from \"../../types\";\n\nexport function FaqColumnsBlockRenderer({\n block,\n overlapAbove,\n overlapBelow,\n containerClassName,\n linkComponent,\n resolveLink: resolve,\n}: BlockRendererProps<FaqColumnsBlockData>) {\n const items: FAQItem[] = (block.items ?? [])\n .filter((row) => row.question && row.answer)\n .map((row, i) => ({\n id: row.id ?? String(i),\n question: row.question as string,\n // The field is a textarea, so blank lines are the editor's paragraph\n // breaks and have to survive into the DOM.\n answer: <span className=\"whitespace-pre-line\">{row.answer}</span>,\n }));\n if (items.length === 0) return null;\n\n const cta = block.cta;\n // The link text is what makes a call to action; with no destination it\n // renders as text (see `renderLinkWith`).\n const hasCta = Boolean(cta?.linkText);\n const ctaHref = cta ? resolve(cta) : null;\n\n return (\n <section className={seamClasses({ overlapAbove, overlapBelow })}>\n <div className={cx(containerClassName, \"py-16 lg:py-24\")}>\n <FAQColumnsBlock\n // The site measure is on the wrapper above; the block's own\n // container and vertical rhythm are reset so they cannot fight it.\n className=\"py-0 md:py-0\"\n containerClassName=\"max-w-none px-0 sm:px-0\"\n items={items}\n title={block.title ?? \"\"}\n {...(block.eyebrow ? { eyebrow: block.eyebrow } : {})}\n {...(block.description ? { description: block.description } : {})}\n sticky={block.sticky ?? true}\n type={block.type ?? \"single\"}\n collapsible={block.collapsible ?? true}\n renderLink={renderLinkWith(linkComponent)}\n {...(hasCta && cta\n ? {\n cta: {\n title: cta.title ?? \"\",\n linkText: cta.linkText as string,\n href: ctaHref ?? \"\",\n newTab: cta.newTab ?? false,\n },\n }\n : {})}\n />\n </div>\n </section>\n );\n}\n","import { TestimonialMasonry, type TestimonialItem } from \"@bison-lab/ui\";\n\nimport { cx } from \"../../cx\";\nimport { renderLinkWith } from \"../../link\";\nimport { resolveMedia } from \"../../media\";\nimport type { BlockRendererProps } from \"../../render-blocks\";\nimport { seamClasses } from \"../../seam\";\nimport type { TestimonialMasonryBlockData } from \"../../types\";\n\nexport function TestimonialMasonryBlockRenderer({\n block,\n overlapAbove,\n overlapBelow,\n containerClassName,\n linkComponent,\n resolveLink: resolve,\n}: BlockRendererProps<TestimonialMasonryBlockData>) {\n const items: TestimonialItem[] = (block.items ?? [])\n .filter((row) => row.content && row.author?.name)\n .map((row, i) => {\n const avatar = resolveMedia(row.author?.avatar);\n return {\n id: row.id ?? String(i),\n content: row.content as string,\n author: {\n name: row.author?.name as string,\n ...(row.author?.title ? { title: row.author.title } : {}),\n // The library falls back to initials when this is absent, which is\n // why an unresolved upload must not become an empty string.\n ...(avatar ? { avatarUrl: avatar.src } : {}),\n },\n };\n });\n if (items.length === 0) return null;\n\n const link = block.link;\n // The label is what makes the link; with no destination it renders as\n // text (see `renderLinkWith`).\n const hasLink = Boolean(link?.label);\n const linkHref = link ? resolve(link) : null;\n\n return (\n // The band's surface is full-bleed; only its contents sit at the measure.\n <section className={cx(\"bg-accent\", seamClasses({ overlapAbove, overlapBelow }))}>\n <div className={cx(containerClassName, \"py-16 lg:py-24\")}>\n <TestimonialMasonry\n className=\"bg-transparent px-0 lg:px-0\"\n containerClassName=\"max-w-none px-0 py-0 sm:py-0 md:px-0 md:py-0\"\n items={items}\n title={block.title ?? \"\"}\n renderLink={renderLinkWith(linkComponent)}\n {...(block.eyebrow ? { eyebrow: block.eyebrow } : {})}\n {...(block.description ? { description: block.description } : {})}\n {...(typeof block.minItemsForFade === \"number\"\n ? { minItemsForFade: block.minItemsForFade }\n : {})}\n {...(typeof block.maxVisibleRows === \"number\"\n ? { maxVisibleRows: block.maxVisibleRows }\n : {})}\n {...(hasLink && link\n ? {\n link: {\n label: link.label as string,\n href: linkHref ?? \"\",\n newTab: link.newTab ?? false,\n },\n }\n : {})}\n />\n </div>\n </section>\n );\n}\n","import {\n JsonLd,\n NapBlock,\n type NapDepartment,\n type NapRecord,\n} from \"@bison-lab/ui\";\n\nimport { cx } from \"../../cx\";\nimport { renderLinkWith } from \"../../link\";\nimport type { BlockRendererProps } from \"../../render-blocks\";\nimport { seamClasses } from \"../../seam\";\nimport type { NapBlockData } from \"../../types\";\n\nexport function NapBlockRenderer({\n block,\n overlapAbove,\n overlapBelow,\n containerClassName,\n linkComponent,\n}: BlockRendererProps<NapBlockData>) {\n const address = block.address;\n const departments: NapDepartment[] = (block.departments ?? [])\n // `phoneE164` is what becomes the `tel:` href and the structured data's\n // `telephone`, so a department without one is not publishable as a NAP\n // entry at all — dropping it beats emitting an unreachable number.\n .filter((row) => row.name && row.phoneE164)\n .map((row, i) => ({\n id: row.id ?? String(i),\n name: row.name as string,\n ...(row.departmentType ? { type: row.departmentType } : {}),\n phone: {\n e164: row.phoneE164 as string,\n ...(row.phoneDisplay ? { display: row.phoneDisplay } : {}),\n },\n }));\n\n if (!block.businessName || !address?.streetAddress || departments.length === 0)\n return null;\n\n const record: NapRecord = {\n name: block.businessName,\n type: block.businessType ?? \"LocalBusiness\",\n address: {\n streetAddress: address.streetAddress,\n addressLocality: address.addressLocality ?? \"\",\n addressRegion: address.addressRegion ?? \"\",\n postalCode: address.postalCode ?? \"\",\n ...(address.addressCountry\n ? { addressCountry: address.addressCountry }\n : {}),\n },\n departments,\n ...(block.url ? { url: block.url } : {}),\n };\n\n return (\n <section className={seamClasses({ overlapAbove, overlapBelow })}>\n <div className={cx(containerClassName, \"py-16 lg:py-24\")}>\n <NapBlock\n record={record}\n showName={block.showName ?? true}\n headingLevel={block.headingLevel ?? \"h2\"}\n renderLink={renderLinkWith(linkComponent)}\n />\n {block.emitJsonLd === false ? null : <JsonLd data={record} />}\n </div>\n </section>\n );\n}\n","import {\n StatsBandBlock,\n type StatItem,\n type StatsBandColumns,\n} from \"@bison-lab/ui\";\n\nimport { cx } from \"../../cx\";\nimport { renderLinkWith } from \"../../link\";\nimport type { BlockRendererProps } from \"../../render-blocks\";\nimport { SEAM_PULL_DOWN, SEAM_PULL_UP, seamClasses } from \"../../seam\";\nimport type { StatsBandBlockData } from \"../../types\";\n\n/** The select only offers 2–6, but a site can edit the config; fall back to the design's four. */\nfunction columnsFrom(value: StatsBandBlockData[\"columns\"]): StatsBandColumns {\n const n = Number(value ?? 4);\n return (Number.isInteger(n) && n >= 2 && n <= 6 ? n : 4) as StatsBandColumns;\n}\n\nexport function StatsBandBlockRenderer({\n block,\n index,\n isLast,\n overlapAbove,\n overlapBelow,\n containerClassName,\n linkComponent,\n}: BlockRendererProps<StatsBandBlockData>) {\n // A stat is its figure: a row without one is dropped rather than rendered\n // as an empty cell, and the band needs two to be a band at all.\n const items: StatItem[] = (block.items ?? []).flatMap((row, i) =>\n row.value\n ? [\n {\n id: row.id ?? String(i),\n value: row.value,\n label: row.label ?? \"\",\n ...(row.href ? { href: row.href } : {}),\n ...(row.wide ? { wide: true } : {}),\n },\n ]\n : [],\n );\n if (items.length < 2) return null;\n\n // Floating only makes sense across a seam: first on the page, the band\n // keeps its top padding, and last on the page it keeps its bottom padding\n // rather than pulling the footer up. The neighbours clear the same\n // distance through `seamClasses` (see `seam.ts`), and so does this band\n // when the neighbour is the one floating: two floating bands in a row\n // would otherwise pull into each other, so on that side neither pulls and\n // both clear. A floating band is headless (the config hides the heading\n // fields), so any heading left over from before `overlap` was ticked is\n // not pulled up into the hero.\n const floats = Boolean(block.overlap);\n const overAbove = floats && index > 0 && !overlapAbove;\n const overBelow = floats && !isLast && !overlapBelow;\n\n return (\n <section\n className={cx(\n \"relative\",\n floats && \"z-10\",\n seamClasses({ overlapAbove, overlapBelow }),\n )}\n >\n <div\n className={cx(\n containerClassName,\n overAbove ? SEAM_PULL_UP : \"pt-16 lg:pt-24\",\n overBelow ? SEAM_PULL_DOWN : \"pb-16 lg:pb-24\",\n )}\n >\n <StatsBandBlock\n items={items}\n columns={columnsFrom(block.columns)}\n tone={block.tone ?? \"card\"}\n align={block.align ?? \"start\"}\n eyebrow={(!floats && block.eyebrow) || undefined}\n title={(!floats && block.title) || undefined}\n description={(!floats && block.description) || undefined}\n containerClassName=\"max-w-none px-0 sm:px-0\"\n renderLink={renderLinkWith(linkComponent)}\n />\n </div>\n </section>\n );\n}\n","/**\n * The one rule for a typed panel width, shared by the config's validator and\n * the renderer's reader so the two cannot drift: if the validator accepted a\n * value the renderer dropped, the editor would be back to a width that\n * silently does nothing. React-free, because `config.ts` ships from the Node\n * entry.\n */\nconst REM_WIDTH = /^\\s*(\\d+(?:\\.\\d+)?)\\s*(rem)?\\s*$/;\n\n/** \"30\" and \"30rem\" both mean 30rem; anything else is not a width. */\nexport function remWidth(\n value: string | null | undefined,\n): `${number}rem` | null {\n const match = REM_WIDTH.exec(value ?? \"\");\n return match ? (`${Number(match[1])}rem` as `${number}rem`) : null;\n}\n","import {\n MegaMenuPanel,\n type MegaMenuColumn,\n type MegaMenuIcons,\n type MegaMenuLink,\n type MegaMenuPanelData,\n type MegaMenuSection,\n type MegaMenuVariants,\n type MegaMenuWidth,\n} from \"@bison-lab/ui\";\n\nimport { resolveLink, type LinkValue, type ResolveLink } from \"../../fields/link\";\nimport { renderLinkWith } from \"../../link\";\nimport type { BlockRendererProps } from \"../../render-blocks\";\nimport type { MegaMenuBlockData, MegaMenuLinkData } from \"../../types\";\nimport { remWidth } from \"./rem-width\";\n\n/**\n * A menu link needs both halves. Unlike a call to action, a link whose page\n * is missing or unpublished is dropped rather than shown as text: a menu\n * item that goes nowhere is worse than one fewer item.\n */\nfunction completeLink(\n row: MegaMenuLinkData,\n resolve: ResolveLink,\n): MegaMenuLink | null {\n const href = resolve(row);\n if (!row.label || !href) return null;\n return {\n label: row.label,\n href,\n ...(row.description ? { description: row.description } : {}),\n ...(row.variant ? { variant: row.variant } : {}),\n ...(row.icon ? { icon: row.icon } : {}),\n ...(row.newTab ? { newTab: true } : {}),\n };\n}\n\n/**\n * The row as `MegaMenuPanel` data, or `null` when there is nothing to open:\n * no columns, or columns whose links are not yet complete. Draft saves skip\n * validation, so a live preview genuinely hands this half-built rows.\n * `resolve` turns each link's page into a path; the package's own is the\n * default.\n */\nexport function megaMenuPanelFromRow(\n row: MegaMenuBlockData,\n resolve: ResolveLink = resolveLink,\n): MegaMenuPanelData | null {\n const panel = row.panel;\n if (!panel?.columns?.length) return null;\n const custom = Boolean(panel.customWidths);\n\n const columns: MegaMenuColumn[] = [];\n for (const column of panel.columns) {\n const sections: MegaMenuSection[] = [];\n for (const section of column.sections ?? []) {\n const links = (section.links ?? [])\n .map((link) => completeLink(link, resolve))\n .filter((link): link is MegaMenuLink => link !== null);\n if (!links.length) continue;\n sections.push({\n ...(section.eyebrow ? { eyebrow: section.eyebrow } : {}),\n display: section.display ?? \"list\",\n hideDescriptionsOnMobile: section.hideDescriptionsOnMobile ?? true,\n links,\n });\n }\n columns.push({\n ...(custom && column.customWidth\n ? { customWidth: column.customWidth }\n : { width: Number(column.width ?? \"100\") as MegaMenuWidth }),\n ...(column.divider ? { divider: true } : {}),\n sections,\n });\n }\n if (!columns.some((column) => column.sections.length)) return null;\n\n const overview = footerLink(panel.footer?.overview, resolve);\n const cta = footerLink(panel.footer?.cta, resolve);\n // Independent of the column checkbox: the config gates only `customWidth`\n // on it, and the help text promises a typed panel width is used.\n const customMax = remWidth(panel.customMaxWidth);\n\n return {\n maxWidth: customMax ?? panel.maxWidth ?? \"standard\",\n columns,\n footer: {\n ...(overview ? { overview } : {}),\n ...(cta ? { cta } : {}),\n },\n };\n}\n\n/** A footer link, complete, or nothing: same rule as `completeLink`. */\nfunction footerLink(\n row: LinkValue | undefined,\n resolve: ResolveLink,\n): { label: string; href: string; newTab?: true } | null {\n const href = row ? resolve(row) : null;\n if (!row?.label || !href) return null;\n return {\n label: row.label,\n href,\n ...(row.newTab ? { newTab: true } : {}),\n };\n}\n\nexport interface MegaMenuBlockRendererProps\n extends BlockRendererProps<MegaMenuBlockData> {\n /** The look behind each `variants` value the site passed to `megaMenuBlock`. */\n variants?: MegaMenuVariants;\n /** The glyph behind each `icons` value the site passed to `megaMenuBlock`. */\n icons?: MegaMenuIcons;\n}\n\n/**\n * One panel, standing alone: what a live preview shows while the row is being\n * edited. A whole header goes through `headerItemsFromBlocks` instead.\n */\nexport function MegaMenuBlockRenderer({\n block,\n linkComponent,\n resolveLink: resolve,\n variants,\n icons,\n}: MegaMenuBlockRendererProps) {\n const panel = megaMenuPanelFromRow(block, resolve);\n if (!panel) return null;\n return (\n <MegaMenuPanel\n {...panel}\n variants={variants}\n icons={icons}\n renderLink={renderLinkWith(linkComponent)}\n />\n );\n}\n","import {\n megaMenuToFloatingNavItems,\n type FloatingNavItem,\n type MegaMenuIcons,\n type MegaMenuItem,\n type MegaMenuVariants,\n} from \"@bison-lab/ui\";\n\nimport { resolveLink, type ResolveLink } from \"../../fields/link\";\nimport { renderLinkWith, type BlockLinkComponent } from \"../../link\";\nimport type { MegaMenuBlockData, NavLinkBlockData } from \"../../types\";\nimport { megaMenuPanelFromRow } from \"./component\";\n\n/** A row of a header global's `items`: either block, or whatever else a site added. */\nexport type HeaderBlockRow = MegaMenuBlockData | NavLinkBlockData;\n\nexport interface HeaderItemsOptions {\n variants?: MegaMenuVariants;\n icons?: MegaMenuIcons;\n /** Marks the current section; receives each item's `href`. */\n isActive?: (href: string) => boolean;\n /** The site's link adapter, applied to every link inside the panels. */\n linkComponent?: BlockLinkComponent;\n /** The site's resolver for page links; defaults to the package's `resolveLink`. */\n resolveLink?: ResolveLink;\n}\n\n/**\n * A global's `items` as `FloatingNavBlock` items, so a site's header is one\n * call. Rows the bar cannot show are dropped rather than rendered broken: a\n * row with no label, a mega menu with nothing to open, and any block type a\n * site added that this package does not know.\n *\n * A `navLink` row's `newTab` is not carried: `FloatingNavItem` has no such\n * field, because the bar renders its own links. A bar item whose page is\n * missing or unpublished is dropped, like a panel link. A mega menu's own\n * `href` (the trigger's landing page) is plain text, not a picked page.\n */\nexport function headerItemsFromBlocks(\n blocks: HeaderBlockRow[],\n {\n variants,\n icons,\n isActive,\n linkComponent,\n resolveLink: resolve = resolveLink,\n }: HeaderItemsOptions = {},\n): FloatingNavItem[] {\n const items: MegaMenuItem[] = [];\n for (const row of blocks) {\n if (!row.label) continue;\n if (row.blockType === \"navLink\") {\n const href = resolve(row);\n if (!href) continue;\n items.push({ label: row.label, href });\n } else if (row.blockType === \"megaMenu\") {\n const panel = megaMenuPanelFromRow(row, resolve);\n if (!panel) continue;\n items.push({\n label: row.label,\n ...(row.href ? { href: row.href } : {}),\n panel,\n });\n }\n }\n return megaMenuToFloatingNavItems(\n { items },\n {\n isActive,\n variants,\n icons,\n ...(linkComponent ? { renderLink: renderLinkWith(linkComponent) } : {}),\n },\n );\n}\n"],"mappings":"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"}
@@ -1,26 +1,50 @@
1
1
 
2
- import { p as BlockRendererProps, s as RichTextBlockData } from "./types-D9rg0BoW.mjs";
2
+ import { m as BlockRendererProps, s as RichTextBlockData } from "./types-ODMRyOpH.mjs";
3
3
  import * as react_jsx_runtime0 from "react/jsx-runtime";
4
+ import { SerializedLinkNode } from "@payloadcms/richtext-lexical";
4
5
 
5
6
  //#region src/blocks/rich-text/component.d.ts
6
7
  /**
7
- * A Lexical document as prose.
8
- *
9
- * This is the one renderer that needs `@payloadcms/richtext-lexical`, which is
10
- * why it ships from its own entry (`@bison-lab/payload-blocks/rich-text`)
11
- * rather than the `./react` barrel every consumer loads.
8
+ * How an editor's link to another document becomes a URL. The same argument
9
+ * Payload's own `LinkJSXConverter` takes: `linkNode.fields.doc` carries the
10
+ * `relationTo` and the related document (populated, or just its id, by the
11
+ * depth the page was fetched at). Only the site knows its routes.
12
+ */
13
+ type InternalDocToHref = (args: {
14
+ linkNode: SerializedLinkNode;
15
+ }) => string;
16
+ interface RichTextBlockRendererOptions {
17
+ internalDocToHref?: InternalDocToHref;
18
+ }
19
+ /**
20
+ * Builds the `richText` renderer for a site.
12
21
  *
13
- * The cast at the boundary is the price of the package's no-generated-types
14
- * rule: `RichTextContent` describes the document structurally so a site's
15
- * generated block type matches it, and Lexical's own type is the same shape
16
- * with tighter unions.
22
+ * `RichTextBlockRenderer` below is the plain registry value and is what a
23
+ * site with no internal links needs. A site whose editors link to other
24
+ * documents builds its own with `internalDocToHref`, and it must do so in a
25
+ * `"use client"` module: this entry carries the client banner, so the
26
+ * factory is a client reference that a Server Component can pass along but
27
+ * cannot call, and the resolver is a closure that cannot cross the boundary
28
+ * as a prop either. The site's `next/link` adapter lives in the same kind of
29
+ * module for the same reason.
17
30
  */
18
- declare function RichTextBlockRenderer({
31
+ declare function richTextBlockRenderer({
32
+ internalDocToHref
33
+ }?: RichTextBlockRendererOptions): ({
34
+ block,
35
+ overlapAbove,
36
+ overlapBelow,
37
+ containerClassName,
38
+ linkComponent
39
+ }: BlockRendererProps<RichTextBlockData>) => react_jsx_runtime0.JSX.Element | null;
40
+ /** The `richText` renderer with no internal-link resolver; see the factory. */
41
+ declare const RichTextBlockRenderer: ({
19
42
  block,
20
43
  overlapAbove,
21
44
  overlapBelow,
22
- containerClassName
23
- }: BlockRendererProps<RichTextBlockData>): react_jsx_runtime0.JSX.Element | null;
45
+ containerClassName,
46
+ linkComponent
47
+ }: BlockRendererProps<RichTextBlockData>) => react_jsx_runtime0.JSX.Element | null;
24
48
  //#endregion
25
- export { RichTextBlockRenderer };
49
+ export { type InternalDocToHref, RichTextBlockRenderer, type RichTextBlockRendererOptions, richTextBlockRenderer };
26
50
  //# sourceMappingURL=rich-text.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rich-text.d.mts","names":[],"sources":["../src/blocks/rich-text/component.tsx"],"mappings":";;;;;;;AAoBA;;;;;;;;;;iBAAgB,qBAAA,CAAA;EACd,KAAA;EACA,YAAA;EACA,YAAA;EACA;AAAA,GACC,kBAAA,CAAmB,iBAAA,IAAkB,kBAAA,CAAA,GAAA,CAAA,OAAA"}
1
+ {"version":3,"file":"rich-text.d.mts","names":[],"sources":["../src/blocks/rich-text/component.tsx"],"mappings":";;;;;;;;AAoBA;;;;KAAY,iBAAA,IAAqB,IAAA;EAAQ,QAAA,EAAU,kBAAA;AAAA;AAAA,UAElC,4BAAA;EACf,iBAAA,GAAoB,iBAAA;AAAA;;;;;AAmEtB;;;;;;;;iBAAgB,qBAAA,CAAA;EAAwB;AAAA,IAAqB,4BAAA;EAAiC,KAAA;EAAA,YAAA;EAAA,YAAA;EAAA,kBAAA;EAAA;AAAA,GAmBzF,kBAAA,CAAmB,iBAAA,MAAkB,kBAAA,CAAA,GAAA,CAAA,OAAA;;cAkB7B,qBAAA;EAAqB,KAAA;EAAA,YAAA;EAAA,YAAA;EAAA,kBAAA;EAAA;AAAA,GAlB7B,kBAAA,CAAmB,iBAAA,MAAkB,kBAAA,CAAA,GAAA,CAAA,OAAA"}
@@ -1,37 +1,94 @@
1
1
  "use client";
2
- import { o as seamClasses, s as cx } from "./seam-BDFZlV3F.mjs";
2
+ import { l as newTabAttributes, o as seamClasses, s as cx } from "./seam-CmLb3BSo.mjs";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  import { RichText } from "@payloadcms/richtext-lexical/react";
5
5
  //#region src/blocks/rich-text/component.tsx
6
6
  /**
7
- * A Lexical document as prose.
7
+ * The `link` and `autolink` converters, through the seam. Payload's default
8
+ * converters render both as a bare `<a>`, a full document load on a Next
9
+ * site; these take their place and leave every other node to
10
+ * `defaultJSXConverters`. `newTab` is expanded to `target`/`rel` the way
11
+ * every other renderer does it, so an adapter that only spreads the anchor
12
+ * attributes is still safe.
8
13
  *
9
- * This is the one renderer that needs `@payloadcms/richtext-lexical`, which is
10
- * why it ships from its own entry (`@bison-lab/payload-blocks/rich-text`)
11
- * rather than the `./react` barrel every consumer loads.
14
+ * One default converter still writes markup the seams exist to own: `upload`
15
+ * renders a file an editor drops into the prose as a bare `<a>` and an image
16
+ * as a raw `<img>`, outside `linkComponent` and `imageComponent` both. That
17
+ * is BIS-76, pinned in renderers.test.tsx; until it ships, a rich-text
18
+ * section is client-side for its links and not for its uploads.
12
19
  *
13
- * The cast at the boundary is the price of the package's no-generated-types
14
- * rule: `RichTextContent` describes the document structurally so a site's
15
- * generated block type matches it, and Lexical's own type is the same shape
16
- * with tighter unions.
20
+ * An internal link with no `internalDocToHref` cannot be resolved here, so it
21
+ * renders through the seam at `#`, the href Payload's own converter falls back
22
+ * to, and says so on the console rather than shipping a dead link quietly.
17
23
  */
18
- function RichTextBlockRenderer({ block, overlapAbove, overlapBelow, containerClassName }) {
19
- if (!block.content) return null;
20
- return /* @__PURE__ */ jsx("section", {
21
- className: seamClasses({
22
- overlapAbove,
23
- overlapBelow
24
- }),
25
- children: /* @__PURE__ */ jsx("div", {
26
- className: cx(containerClassName, "py-16 lg:py-20"),
27
- children: /* @__PURE__ */ jsx(RichText, {
28
- data: block.content,
29
- className: "max-w-prose"
30
- })
31
- })
24
+ function linkConverters(Link, internalDocToHref) {
25
+ const through = (href, node, children) => /* @__PURE__ */ jsx(Link, {
26
+ href,
27
+ newTab: node.fields.newTab,
28
+ ...newTabAttributes(node.fields.newTab),
29
+ children
30
+ });
31
+ return ({ defaultConverters }) => ({
32
+ ...defaultConverters,
33
+ autolink: ({ node, nodesToJSX }) => through(node.fields.url ?? "", node, nodesToJSX({ nodes: node.children })),
34
+ link: ({ node, nodesToJSX }) => {
35
+ let href = node.fields.url ?? "";
36
+ if (node.fields.linkType === "internal") if (internalDocToHref) href = internalDocToHref({ linkNode: node });
37
+ else {
38
+ console.error("@bison-lab/payload-blocks: a rich-text link points at another document, but the richText renderer has no internalDocToHref to turn it into a URL. Register richTextBlockRenderer({ internalDocToHref }) in a client module instead of RichTextBlockRenderer.");
39
+ href = "#";
40
+ }
41
+ return through(href, node, nodesToJSX({ nodes: node.children }));
42
+ }
32
43
  });
33
44
  }
45
+ /**
46
+ * Builds the `richText` renderer for a site.
47
+ *
48
+ * `RichTextBlockRenderer` below is the plain registry value and is what a
49
+ * site with no internal links needs. A site whose editors link to other
50
+ * documents builds its own with `internalDocToHref`, and it must do so in a
51
+ * `"use client"` module: this entry carries the client banner, so the
52
+ * factory is a client reference that a Server Component can pass along but
53
+ * cannot call, and the resolver is a closure that cannot cross the boundary
54
+ * as a prop either. The site's `next/link` adapter lives in the same kind of
55
+ * module for the same reason.
56
+ */
57
+ function richTextBlockRenderer({ internalDocToHref } = {}) {
58
+ /**
59
+ * A Lexical document as prose.
60
+ *
61
+ * This is the one renderer that needs `@payloadcms/richtext-lexical`, which
62
+ * is why it ships from its own entry (`@bison-lab/payload-blocks/rich-text`)
63
+ * rather than the `./react` barrel every consumer loads.
64
+ *
65
+ * The cast at the boundary is the price of the package's no-generated-types
66
+ * rule: `RichTextContent` describes the document structurally so a site's
67
+ * generated block type matches it, and Lexical's own type is the same shape
68
+ * with tighter unions.
69
+ */
70
+ function RichTextBlockRenderer({ block, overlapAbove, overlapBelow, containerClassName, linkComponent }) {
71
+ if (!block.content) return null;
72
+ return /* @__PURE__ */ jsx("section", {
73
+ className: seamClasses({
74
+ overlapAbove,
75
+ overlapBelow
76
+ }),
77
+ children: /* @__PURE__ */ jsx("div", {
78
+ className: cx(containerClassName, "py-16 lg:py-20"),
79
+ children: /* @__PURE__ */ jsx(RichText, {
80
+ data: block.content,
81
+ converters: linkConverters(linkComponent, internalDocToHref),
82
+ className: "max-w-prose"
83
+ })
84
+ })
85
+ });
86
+ }
87
+ return RichTextBlockRenderer;
88
+ }
89
+ /** The `richText` renderer with no internal-link resolver; see the factory. */
90
+ const RichTextBlockRenderer = richTextBlockRenderer();
34
91
  //#endregion
35
- export { RichTextBlockRenderer };
92
+ export { RichTextBlockRenderer, richTextBlockRenderer };
36
93
 
37
94
  //# sourceMappingURL=rich-text.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"rich-text.mjs","names":[],"sources":["../src/blocks/rich-text/component.tsx"],"sourcesContent":["import { RichText } from \"@payloadcms/richtext-lexical/react\";\nimport type { SerializedEditorState } from \"@payloadcms/richtext-lexical/lexical\";\n\nimport { cx } from \"../../cx\";\nimport type { BlockRendererProps } from \"../../render-blocks\";\nimport { seamClasses } from \"../../seam\";\nimport type { RichTextBlockData } from \"../../types\";\n\n/**\n * A Lexical document as prose.\n *\n * This is the one renderer that needs `@payloadcms/richtext-lexical`, which is\n * why it ships from its own entry (`@bison-lab/payload-blocks/rich-text`)\n * rather than the `./react` barrel every consumer loads.\n *\n * The cast at the boundary is the price of the package's no-generated-types\n * rule: `RichTextContent` describes the document structurally so a site's\n * generated block type matches it, and Lexical's own type is the same shape\n * with tighter unions.\n */\nexport function RichTextBlockRenderer({\n block,\n overlapAbove,\n overlapBelow,\n containerClassName,\n}: BlockRendererProps<RichTextBlockData>) {\n if (!block.content) return null;\n return (\n <section className={seamClasses({ overlapAbove, overlapBelow })}>\n <div className={cx(containerClassName, \"py-16 lg:py-20\")}>\n <RichText\n data={block.content as unknown as SerializedEditorState}\n className=\"max-w-prose\"\n />\n </div>\n </section>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAoBA,SAAgB,sBAAsB,EACpC,OACA,cACA,cACA,sBACwC;AACxC,KAAI,CAAC,MAAM,QAAS,QAAO;AAC3B,QACE,oBAAC,WAAD;EAAS,WAAW,YAAY;GAAE;GAAc;GAAc,CAAC;YAC7D,oBAAC,OAAD;GAAK,WAAW,GAAG,oBAAoB,iBAAiB;aACtD,oBAAC,UAAD;IACE,MAAM,MAAM;IACZ,WAAU;IACV,CAAA;GACE,CAAA;EACE,CAAA"}
1
+ {"version":3,"file":"rich-text.mjs","names":[],"sources":["../src/blocks/rich-text/component.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { RichText, type JSXConvertersFunction } from \"@payloadcms/richtext-lexical/react\";\nimport type { SerializedEditorState } from \"@payloadcms/richtext-lexical/lexical\";\nimport type {\n SerializedAutoLinkNode,\n SerializedLinkNode,\n} from \"@payloadcms/richtext-lexical\";\n\nimport { cx } from \"../../cx\";\nimport { type BlockLinkComponent, newTabAttributes } from \"../../link\";\nimport type { BlockRendererProps } from \"../../render-blocks\";\nimport { seamClasses } from \"../../seam\";\nimport type { RichTextBlockData } from \"../../types\";\n\n/**\n * How an editor's link to another document becomes a URL. The same argument\n * Payload's own `LinkJSXConverter` takes: `linkNode.fields.doc` carries the\n * `relationTo` and the related document (populated, or just its id, by the\n * depth the page was fetched at). Only the site knows its routes.\n */\nexport type InternalDocToHref = (args: { linkNode: SerializedLinkNode }) => string;\n\nexport interface RichTextBlockRendererOptions {\n internalDocToHref?: InternalDocToHref;\n}\n\n/**\n * The `link` and `autolink` converters, through the seam. Payload's default\n * converters render both as a bare `<a>`, a full document load on a Next\n * site; these take their place and leave every other node to\n * `defaultJSXConverters`. `newTab` is expanded to `target`/`rel` the way\n * every other renderer does it, so an adapter that only spreads the anchor\n * attributes is still safe.\n *\n * One default converter still writes markup the seams exist to own: `upload`\n * renders a file an editor drops into the prose as a bare `<a>` and an image\n * as a raw `<img>`, outside `linkComponent` and `imageComponent` both. That\n * is BIS-76, pinned in renderers.test.tsx; until it ships, a rich-text\n * section is client-side for its links and not for its uploads.\n *\n * An internal link with no `internalDocToHref` cannot be resolved here, so it\n * renders through the seam at `#`, the href Payload's own converter falls back\n * to, and says so on the console rather than shipping a dead link quietly.\n */\nfunction linkConverters(\n Link: BlockLinkComponent,\n internalDocToHref: InternalDocToHref | undefined,\n): JSXConvertersFunction {\n const through = (\n href: string,\n node: SerializedAutoLinkNode | SerializedLinkNode,\n children: ReactNode,\n ) => (\n <Link href={href} newTab={node.fields.newTab} {...newTabAttributes(node.fields.newTab)}>\n {children}\n </Link>\n );\n return ({ defaultConverters }) => ({\n ...defaultConverters,\n autolink: ({ node, nodesToJSX }) =>\n through(node.fields.url ?? \"\", node, nodesToJSX({ nodes: node.children })),\n link: ({ node, nodesToJSX }) => {\n let href = node.fields.url ?? \"\";\n if (node.fields.linkType === \"internal\") {\n if (internalDocToHref) {\n href = internalDocToHref({ linkNode: node });\n } else {\n console.error(\n \"@bison-lab/payload-blocks: a rich-text link points at another document, but the richText renderer has no internalDocToHref to turn it into a URL. Register richTextBlockRenderer({ internalDocToHref }) in a client module instead of RichTextBlockRenderer.\",\n );\n href = \"#\";\n }\n }\n return through(href, node, nodesToJSX({ nodes: node.children }));\n },\n });\n}\n\n/**\n * Builds the `richText` renderer for a site.\n *\n * `RichTextBlockRenderer` below is the plain registry value and is what a\n * site with no internal links needs. A site whose editors link to other\n * documents builds its own with `internalDocToHref`, and it must do so in a\n * `\"use client\"` module: this entry carries the client banner, so the\n * factory is a client reference that a Server Component can pass along but\n * cannot call, and the resolver is a closure that cannot cross the boundary\n * as a prop either. The site's `next/link` adapter lives in the same kind of\n * module for the same reason.\n */\nexport function richTextBlockRenderer({ internalDocToHref }: RichTextBlockRendererOptions = {}) {\n /**\n * A Lexical document as prose.\n *\n * This is the one renderer that needs `@payloadcms/richtext-lexical`, which\n * is why it ships from its own entry (`@bison-lab/payload-blocks/rich-text`)\n * rather than the `./react` barrel every consumer loads.\n *\n * The cast at the boundary is the price of the package's no-generated-types\n * rule: `RichTextContent` describes the document structurally so a site's\n * generated block type matches it, and Lexical's own type is the same shape\n * with tighter unions.\n */\n function RichTextBlockRenderer({\n block,\n overlapAbove,\n overlapBelow,\n containerClassName,\n linkComponent,\n }: BlockRendererProps<RichTextBlockData>) {\n if (!block.content) return null;\n return (\n <section className={seamClasses({ overlapAbove, overlapBelow })}>\n <div className={cx(containerClassName, \"py-16 lg:py-20\")}>\n <RichText\n data={block.content as unknown as SerializedEditorState}\n converters={linkConverters(linkComponent, internalDocToHref)}\n className=\"max-w-prose\"\n />\n </div>\n </section>\n );\n }\n return RichTextBlockRenderer;\n}\n\n/** The `richText` renderer with no internal-link resolver; see the factory. */\nexport const RichTextBlockRenderer = richTextBlockRenderer();\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA4CA,SAAS,eACP,MACA,mBACuB;CACvB,MAAM,WACJ,MACA,MACA,aAEA,oBAAC,MAAD;EAAY;EAAM,QAAQ,KAAK,OAAO;EAAQ,GAAI,iBAAiB,KAAK,OAAO,OAAO;EACnF;EACI,CAAA;AAET,SAAQ,EAAE,yBAAyB;EACjC,GAAG;EACH,WAAW,EAAE,MAAM,iBACjB,QAAQ,KAAK,OAAO,OAAO,IAAI,MAAM,WAAW,EAAE,OAAO,KAAK,UAAU,CAAC,CAAC;EAC5E,OAAO,EAAE,MAAM,iBAAiB;GAC9B,IAAI,OAAO,KAAK,OAAO,OAAO;AAC9B,OAAI,KAAK,OAAO,aAAa,WAC3B,KAAI,kBACF,QAAO,kBAAkB,EAAE,UAAU,MAAM,CAAC;QACvC;AACL,YAAQ,MACN,+PACD;AACD,WAAO;;AAGX,UAAO,QAAQ,MAAM,MAAM,WAAW,EAAE,OAAO,KAAK,UAAU,CAAC,CAAC;;EAEnE;;;;;;;;;;;;;;AAeH,SAAgB,sBAAsB,EAAE,sBAAoD,EAAE,EAAE;;;;;;;;;;;;;CAa9F,SAAS,sBAAsB,EAC7B,OACA,cACA,cACA,oBACA,iBACwC;AACxC,MAAI,CAAC,MAAM,QAAS,QAAO;AAC3B,SACE,oBAAC,WAAD;GAAS,WAAW,YAAY;IAAE;IAAc;IAAc,CAAC;aAC7D,oBAAC,OAAD;IAAK,WAAW,GAAG,oBAAoB,iBAAiB;cACtD,oBAAC,UAAD;KACE,MAAM,MAAM;KACZ,YAAY,eAAe,eAAe,kBAAkB;KAC5D,WAAU;KACV,CAAA;IACE,CAAA;GACE,CAAA;;AAGd,QAAO;;;AAIT,MAAa,wBAAwB,uBAAuB"}
@@ -1,4 +1,49 @@
1
1
  "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ //#region src/link.tsx
4
+ /** `target` and `rel` together, or neither: a bare `target` is a tabnabbing hole. */
5
+ function newTabAttributes(newTab) {
6
+ return newTab ? {
7
+ target: "_blank",
8
+ rel: "noopener noreferrer"
9
+ } : {};
10
+ }
11
+ /**
12
+ * The fallback when no `linkComponent` is supplied: a plain `<a>`. Correct
13
+ * everywhere and client-side nowhere, which is the right default for a package
14
+ * that cannot know what router it landed in.
15
+ */
16
+ function DefaultBlockLink({ href, newTab, children, ...rest }) {
17
+ return /* @__PURE__ */ jsx("a", {
18
+ href,
19
+ ...rest,
20
+ ...newTabAttributes(newTab),
21
+ children
22
+ });
23
+ }
24
+ DefaultBlockLink.displayName = "DefaultBlockLink";
25
+ /**
26
+ * Adapts the seam to the `renderLink` prop every `@bison-lab/ui` block takes.
27
+ * The ui blocks hand over `target`/`rel` already expanded, so `newTab` is
28
+ * read back off `target` to keep the flag and the attributes in step.
29
+ *
30
+ * An empty `href` is a link with no destination: a page link whose page is
31
+ * missing or unpublished (`resolveLink` gave `null`). It renders as text in
32
+ * the same place, with the same class, so a call to action never points
33
+ * nowhere and never throws.
34
+ */
35
+ function renderLinkWith(Link) {
36
+ return ({ href, children, ...rest }) => href ? /* @__PURE__ */ jsx(Link, {
37
+ href,
38
+ newTab: rest.target === "_blank",
39
+ ...rest,
40
+ children
41
+ }) : /* @__PURE__ */ jsx("span", {
42
+ className: rest.className,
43
+ children
44
+ });
45
+ }
46
+ //#endregion
2
47
  //#region src/cx.ts
3
48
  /**
4
49
  * Class joining, locally.
@@ -49,6 +94,6 @@ function seamClasses({ overlapAbove, overlapBelow }) {
49
94
  return cx(overlapAbove && "pt-16 lg:pt-20", overlapBelow && "pb-16 lg:pb-20");
50
95
  }
51
96
  //#endregion
52
- export { overlapsSeam as a, SEAM_PULL_UP as i, OVERLAP_BELOW_CLEARANCE as n, seamClasses as o, SEAM_PULL_DOWN as r, cx as s, OVERLAP_ABOVE_CLEARANCE as t };
97
+ export { overlapsSeam as a, DefaultBlockLink as c, SEAM_PULL_UP as i, newTabAttributes as l, OVERLAP_BELOW_CLEARANCE as n, seamClasses as o, SEAM_PULL_DOWN as r, cx as s, OVERLAP_ABOVE_CLEARANCE as t, renderLinkWith as u };
53
98
 
54
- //# sourceMappingURL=seam-BDFZlV3F.mjs.map
99
+ //# sourceMappingURL=seam-CmLb3BSo.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seam-CmLb3BSo.mjs","names":[],"sources":["../src/link.tsx","../src/cx.ts","../src/seam.ts"],"sourcesContent":["import type { AnchorHTMLAttributes, ComponentType, ReactNode } from \"react\";\nimport type { RenderLink } from \"@bison-lab/ui\";\n\n/**\n * The link injection point, the same shape as `BlockImageComponent`.\n *\n * A renderer never writes `<a>` directly. A site hands in one link component\n * and every CMS link routes through it — Next's `Link`, so navigation stays\n * client-side. It is a *component type*, not a render function, for the same\n * reason the image seam is: a Server Component can pass a client component\n * reference across the boundary, but not a closure.\n *\n * `newTab` is the flag from `linkFields()`; `target` and `rel` are always\n * already expanded from it by the time the component is called, so an adapter\n * that only spreads the anchor attributes onto its router link is still safe.\n * The one thing an adapter must do is drop `newTab` before spreading, or React\n * warns about an unknown DOM attribute — see the README's `next/link` adapter.\n */\nexport interface BlockLinkProps\n extends AnchorHTMLAttributes<HTMLAnchorElement> {\n href: string;\n children: ReactNode;\n newTab?: boolean;\n className?: string;\n}\n\nexport type BlockLinkComponent = ComponentType<BlockLinkProps>;\n\n/** `target` and `rel` together, or neither: a bare `target` is a tabnabbing hole. */\nexport function newTabAttributes(\n newTab: boolean | null | undefined,\n): Pick<AnchorHTMLAttributes<HTMLAnchorElement>, \"target\" | \"rel\"> {\n return newTab ? { target: \"_blank\", rel: \"noopener noreferrer\" } : {};\n}\n\n/**\n * The fallback when no `linkComponent` is supplied: a plain `<a>`. Correct\n * everywhere and client-side nowhere, which is the right default for a package\n * that cannot know what router it landed in.\n */\nexport function DefaultBlockLink({\n href,\n newTab,\n children,\n ...rest\n}: BlockLinkProps) {\n return (\n <a href={href} {...rest} {...newTabAttributes(newTab)}>\n {children}\n </a>\n );\n}\nDefaultBlockLink.displayName = \"DefaultBlockLink\";\n\n/**\n * Adapts the seam to the `renderLink` prop every `@bison-lab/ui` block takes.\n * The ui blocks hand over `target`/`rel` already expanded, so `newTab` is\n * read back off `target` to keep the flag and the attributes in step.\n *\n * An empty `href` is a link with no destination: a page link whose page is\n * missing or unpublished (`resolveLink` gave `null`). It renders as text in\n * the same place, with the same class, so a call to action never points\n * nowhere and never throws.\n */\nexport function renderLinkWith(Link: BlockLinkComponent): RenderLink {\n return ({ href, children, ...rest }) =>\n href ? (\n <Link href={href} newTab={rest.target === \"_blank\"} {...rest}>\n {children}\n </Link>\n ) : (\n <span className={rest.className}>{children}</span>\n );\n}\n","/**\n * Class joining, locally.\n *\n * `cn()` from `@bison-lab/ui` cannot be used here: that barrel is stamped\n * `\"use client\"`, so calling it from a Server Component throws. The renderers\n * only ever concatenate their own literals with a caller-supplied string —\n * there are no conflicting Tailwind utilities to merge — so a join is enough.\n */\nexport function cx(...parts: (string | false | null | undefined)[]): string {\n return parts.filter(Boolean).join(\" \");\n}\n","import { cx } from \"./cx\";\nimport type { BlockLike, BlockRendererProps } from \"./render-blocks\";\nimport type { StatsBandBlockData } from \"./types\";\n\n/**\n * The seam contract: how a block that floats across the join between two\n * bands gets its neighbours to make room.\n *\n * A floating block pulls itself up over the block above and down over the\n * block below with `SEAM_PULL_UP` / `SEAM_PULL_DOWN`, and each neighbour adds\n * the same distance on its own side, so the block sits across the seam\n * without covering copy. `RenderBlocks` decides which rows float (its\n * `floats` prop, defaulting to `overlapsSeam`) and hands every renderer two\n * booleans, `overlapAbove` and `overlapBelow`; a renderer's band wrapper adds\n * `seamClasses(props)` and never looks at a neighbour's row.\n *\n * The four distances are one number in two directions, which is why they all\n * live here: change 16/20 in one place.\n */\n\n/** How far a floating block pulls itself up over the block above. */\nexport const SEAM_PULL_UP = \"-mt-16 lg:-mt-20\";\n\n/** How far a floating block pulls itself down over the block below. */\nexport const SEAM_PULL_DOWN = \"-mb-16 lg:-mb-20\";\n\n/** Extra top padding for the block *below* a floating block. */\nexport const OVERLAP_ABOVE_CLEARANCE = \"pt-16 lg:pt-20\";\n\n/** Extra bottom padding for the block *above* a floating block. */\nexport const OVERLAP_BELOW_CLEARANCE = \"pb-16 lg:pb-20\";\n\n/**\n * The default `floats`: the package's stats band with `overlap` on. A site\n * with its own floating block composes it —\n * `floats={(row) => overlapsSeam(row) || row.blockType === \"bookingCard\"}`.\n */\nexport function overlapsSeam(block: BlockLike | undefined): boolean {\n return (\n block?.blockType === \"statsBand\" &&\n Boolean((block as StatsBandBlockData).overlap)\n );\n}\n\n/** The clearance a band wrapper needs for its neighbours, as one class string. */\nexport function seamClasses({\n overlapAbove,\n overlapBelow,\n}: Pick<BlockRendererProps, \"overlapAbove\" | \"overlapBelow\">): string {\n return cx(\n overlapAbove && OVERLAP_ABOVE_CLEARANCE,\n overlapBelow && OVERLAP_BELOW_CLEARANCE,\n );\n}\n"],"mappings":";;;;AA6BA,SAAgB,iBACd,QACiE;AACjE,QAAO,SAAS;EAAE,QAAQ;EAAU,KAAK;EAAuB,GAAG,EAAE;;;;;;;AAQvE,SAAgB,iBAAiB,EAC/B,MACA,QACA,UACA,GAAG,QACc;AACjB,QACE,oBAAC,KAAD;EAAS;EAAM,GAAI;EAAM,GAAI,iBAAiB,OAAO;EAClD;EACC,CAAA;;AAGR,iBAAiB,cAAc;;;;;;;;;;;AAY/B,SAAgB,eAAe,MAAsC;AACnE,SAAQ,EAAE,MAAM,UAAU,GAAG,WAC3B,OACE,oBAAC,MAAD;EAAY;EAAM,QAAQ,KAAK,WAAW;EAAU,GAAI;EACrD;EACI,CAAA,GAEP,oBAAC,QAAD;EAAM,WAAW,KAAK;EAAY;EAAgB,CAAA;;;;;;;;;;;;AC/DxD,SAAgB,GAAG,GAAG,OAAsD;AAC1E,QAAO,MAAM,OAAO,QAAQ,CAAC,KAAK,IAAI;;;;;;;;;;;;;;;;;;;;ACYxC,MAAa,eAAe;;AAG5B,MAAa,iBAAiB;;AAG9B,MAAa,0BAA0B;;AAGvC,MAAa,0BAA0B;;;;;;AAOvC,SAAgB,aAAa,OAAuC;AAClE,QACE,OAAO,cAAc,eACrB,QAAS,MAA6B,QAAQ;;;AAKlD,SAAgB,YAAY,EAC1B,cACA,gBACoE;AACpE,QAAO,GACL,gBAAA,kBACA,gBAAA,iBACD"}