@bison-lab/payload-blocks 3.4.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,7 +23,7 @@ Payload site already has).
23
23
  | --- | --- | --- |
24
24
  | `@bison-lab/payload-blocks` | Block configs, field builders, row types, `resolveMedia` | Node. This is what `payload.config.ts` imports, and it touches no React. |
25
25
  | `@bison-lab/payload-blocks/react` | Renderers, `RenderBlocks`, the image and link seams, the rendering types | Client (`"use client"`). |
26
- | `@bison-lab/payload-blocks/rich-text` | The `richText` renderer, and `richTextBlockRenderer()` to build one that resolves internal links | Client. Split out because it is the only thing that needs `@payloadcms/richtext-lexical`. |
26
+ | `@bison-lab/payload-blocks/rich-text` | The `richText` renderer, `internalDocToHrefFrom`, and `richTextBlockRenderer()` to build one that resolves internal links | Client. Split out because it is the only thing that needs `@payloadcms/richtext-lexical`. |
27
27
  | `@bison-lab/payload-blocks/admin` | `MinRowsArrayField` and `LinkField`, the admin fields the configs reference by path | Client, inside the Payload admin. Resolved through the site's import map, never imported by hand. |
28
28
 
29
29
  The renderers are client components because the blocks they render are: every
@@ -241,33 +241,32 @@ Links in a `richText` section go through `linkComponent` too, the ones Lexical
241
241
  auto-detects included. One kind needs more: a link an editor makes to another
242
242
  *document* has no URL, only the document, and the route is the site's to know.
243
243
  That is the same knowledge `resolveLink` carries, but Lexical hands the
244
- renderer a link *node* rather than one of the link rows above, so it is a
245
- separate resolver taking a separate shape. Build that site's renderer with
246
- `richTextBlockRenderer({ internalDocToHref })` and register it in place of
247
- `RichTextBlockRenderer`. Do it in a `'use client'` module, like the adapters
248
- above: the `/rich-text` entry carries the client banner, so the factory is a
249
- client reference a Server Component can pass along but cannot call, and the
250
- resolver is a closure that could not cross the boundary as a prop.
244
+ renderer a link *node* rather than one of the link rows above. The helper
245
+ `internalDocToHrefFrom` unpacks that node into a page destination and calls
246
+ the site's `resolveLink`, so the page path is defined once. Pass that same
247
+ function to the factory (`richTextBlockRenderer({ resolveLink })`) and
248
+ register the result in place of `RichTextBlockRenderer`. Do it in a
249
+ `'use client'` module, like the adapters above: the `/rich-text` entry
250
+ carries the client banner, so the factory is a client reference a Server
251
+ Component can pass along but cannot call, and the resolver is a closure that
252
+ could not cross the boundary as a prop. A hand-written `internalDocToHref`
253
+ still works; if both options are passed, `internalDocToHref` wins.
251
254
 
252
255
  ```tsx
253
256
  'use client'
254
257
  import { richTextBlockRenderer } from '@bison-lab/payload-blocks/rich-text'
258
+ import { resolveSiteLink } from './resolve-site-link'
255
259
 
256
260
  export const SiteRichText = richTextBlockRenderer({
257
- internalDocToHref: ({ linkNode }) => {
258
- const doc = linkNode.fields.doc
259
- if (!doc) return '#'
260
- // `value` is the related document when the page was fetched deep enough
261
- // to populate it, otherwise its id. Fetch pages at depth 1 or more.
262
- const slug = typeof doc.value === 'object' ? doc.value.slug : doc.value
263
- return doc.relationTo === 'pages' ? `/${slug}` : `/${doc.relationTo}/${slug}`
264
- },
261
+ resolveLink: resolveSiteLink,
265
262
  })
266
263
  ```
267
264
 
268
265
  Without a resolver an internal link still renders through the adapter, at `#`,
269
266
  with a console error naming the option. A site whose editors link between
270
- documents should not ship that way.
267
+ documents should not ship that way. An unpopulated or unpublished `doc`
268
+ follows `resolveLink`'s `null` rule and the helper maps that to `#` — it does
269
+ not invent a second unpublished policy.
271
270
 
272
271
  One rich-text node still bypasses both seams: an *upload* an editor drops into
273
272
  the prose renders through Payload's own converter, a file as a bare `<a>` and
@@ -396,7 +395,7 @@ renders nothing, and `headerItemsFromBlocks` drops it.
396
395
  | Slug | Renders | Notes |
397
396
  | --- | --- | --- |
398
397
  | `hero` | plain markup | No `@bison-lab/ui` counterpart. Override this entry with your own. |
399
- | `richText` | `RichText` (Lexical) | From `/rich-text`. Links go through `linkComponent`; a link to another document needs `richTextBlockRenderer({ internalDocToHref })`. Uploads dropped into the prose do not use the seams yet (BIS-76). |
398
+ | `richText` | `RichText` (Lexical) | From `/rich-text`. Links go through `linkComponent`; a link to another document needs `richTextBlockRenderer({ resolveLink })` (or a hand-written `internalDocToHref`). Uploads dropped into the prose do not use the seams yet (BIS-76). |
400
399
  | `showcasePanels` | `ShowcasePanelsBlock` | 3–6 panels, each with a required image. Opens with three. |
401
400
  | `processSteps` | `ProcessStepsBlock` | 3–6 ordered steps, numbered by position; advances on a timer. Opens with three. |
402
401
  | `faqColumns` | `FAQColumnsBlock` | Answers are plain text, not Lexical — see the config for why. |
@@ -1,5 +1,5 @@
1
1
 
2
- import { m as BlockRendererProps, s as RichTextBlockData } from "./types-ODMRyOpH.mjs";
2
+ import { m as BlockRendererProps, s as RichTextBlockData, x as ResolveLink } from "./types-ODMRyOpH.mjs";
3
3
  import * as react_jsx_runtime0 from "react/jsx-runtime";
4
4
  import { SerializedLinkNode } from "@payloadcms/richtext-lexical";
5
5
 
@@ -14,22 +14,41 @@ type InternalDocToHref = (args: {
14
14
  linkNode: SerializedLinkNode;
15
15
  }) => string;
16
16
  interface RichTextBlockRendererOptions {
17
+ /**
18
+ * Wins when both this and `resolveLink` are passed, so a site that already
19
+ * wrote a custom Lexical resolver is unchanged.
20
+ */
17
21
  internalDocToHref?: InternalDocToHref;
22
+ /**
23
+ * The same function `RenderBlocks` takes. When `internalDocToHref` is
24
+ * omitted, the factory builds one with `internalDocToHrefFrom`.
25
+ */
26
+ resolveLink?: ResolveLink;
18
27
  }
28
+ /**
29
+ * Turns the site's `resolveLink` into the Lexical resolver the rich-text
30
+ * factory needs. Unpacks `linkNode.fields.doc` into a page destination and
31
+ * calls `resolveLink`; `null` (unpopulated or unpublished) becomes `#`, the
32
+ * same fallback BIS-69 uses when no resolver is registered.
33
+ */
34
+ declare function internalDocToHrefFrom(resolve: ResolveLink): InternalDocToHref;
19
35
  /**
20
36
  * Builds the `richText` renderer for a site.
21
37
  *
22
38
  * `RichTextBlockRenderer` below is the plain registry value and is what a
23
39
  * 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.
40
+ * documents builds its own with `resolveLink` (the same function
41
+ * `RenderBlocks` takes; the factory wraps it with `internalDocToHrefFrom`)
42
+ * or a hand-written `internalDocToHref`. When both are passed,
43
+ * `internalDocToHref` wins. It must do so in a `"use client"` module: this
44
+ * entry carries the client banner, so the factory is a client reference
45
+ * that a Server Component can pass along but cannot call, and the resolver
46
+ * is a closure that cannot cross the boundary as a prop either. The site's
47
+ * `next/link` adapter lives in the same kind of module for the same reason.
30
48
  */
31
49
  declare function richTextBlockRenderer({
32
- internalDocToHref
50
+ internalDocToHref,
51
+ resolveLink: resolve
33
52
  }?: RichTextBlockRendererOptions): ({
34
53
  block,
35
54
  overlapAbove,
@@ -46,5 +65,5 @@ declare const RichTextBlockRenderer: ({
46
65
  linkComponent
47
66
  }: BlockRendererProps<RichTextBlockData>) => react_jsx_runtime0.JSX.Element | null;
48
67
  //#endregion
49
- export { type InternalDocToHref, RichTextBlockRenderer, type RichTextBlockRendererOptions, richTextBlockRenderer };
68
+ export { type InternalDocToHref, RichTextBlockRenderer, type RichTextBlockRendererOptions, internalDocToHrefFrom, richTextBlockRenderer };
50
69
  //# 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;;;;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
+ {"version":3,"file":"rich-text.d.mts","names":[],"sources":["../src/blocks/rich-text/component.tsx"],"mappings":";;;;;;;AAqBA;;;;;KAAY,iBAAA,IAAqB,IAAA;EAAQ,QAAA,EAAU,kBAAA;AAAA;AAAA,UAElC,4BAAA;EAAA;;;;EAKf,iBAAA,GAAoB,iBAAA;EAAA;;;;EAKpB,WAAA,GAAc,WAAA;AAAA;;;;;;;iBASA,qBAAA,CAAsB,OAAA,EAAS,WAAA,GAAc,iBAAA;;AA2E7D;;;;;;;;;;;;;iBAAgB,qBAAA,CAAA;EACd,iBAAA;EACA,WAAA,EAAa;AAAA,IACZ,4BAAA;EAAiC,KAAA;EAAA,YAAA;EAAA,YAAA;EAAA,kBAAA;EAAA;AAAA,GAoB/B,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"}
@@ -4,6 +4,21 @@ 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
+ * Turns the site's `resolveLink` into the Lexical resolver the rich-text
8
+ * factory needs. Unpacks `linkNode.fields.doc` into a page destination and
9
+ * calls `resolveLink`; `null` (unpopulated or unpublished) becomes `#`, the
10
+ * same fallback BIS-69 uses when no resolver is registered.
11
+ */
12
+ function internalDocToHrefFrom(resolve) {
13
+ return ({ linkNode }) => {
14
+ const doc = linkNode.fields.doc;
15
+ return resolve({
16
+ type: "page",
17
+ page: (doc && typeof doc === "object" ? doc.value : void 0) ?? null
18
+ }) ?? "#";
19
+ };
20
+ }
21
+ /**
7
22
  * The `link` and `autolink` converters, through the seam. Payload's default
8
23
  * converters render both as a bare `<a>`, a full document load on a Next
9
24
  * site; these take their place and leave every other node to
@@ -17,7 +32,7 @@ import { RichText } from "@payloadcms/richtext-lexical/react";
17
32
  * is BIS-76, pinned in renderers.test.tsx; until it ships, a rich-text
18
33
  * section is client-side for its links and not for its uploads.
19
34
  *
20
- * An internal link with no `internalDocToHref` cannot be resolved here, so it
35
+ * An internal link with no resolver cannot be resolved here, so it
21
36
  * renders through the seam at `#`, the href Payload's own converter falls back
22
37
  * to, and says so on the console rather than shipping a dead link quietly.
23
38
  */
@@ -35,7 +50,7 @@ function linkConverters(Link, internalDocToHref) {
35
50
  let href = node.fields.url ?? "";
36
51
  if (node.fields.linkType === "internal") if (internalDocToHref) href = internalDocToHref({ linkNode: node });
37
52
  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.");
53
+ 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({ resolveLink }) or richTextBlockRenderer({ internalDocToHref }) in a client module instead of RichTextBlockRenderer.");
39
54
  href = "#";
40
55
  }
41
56
  return through(href, node, nodesToJSX({ nodes: node.children }));
@@ -47,14 +62,17 @@ function linkConverters(Link, internalDocToHref) {
47
62
  *
48
63
  * `RichTextBlockRenderer` below is the plain registry value and is what a
49
64
  * 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.
65
+ * documents builds its own with `resolveLink` (the same function
66
+ * `RenderBlocks` takes; the factory wraps it with `internalDocToHrefFrom`)
67
+ * or a hand-written `internalDocToHref`. When both are passed,
68
+ * `internalDocToHref` wins. It must do so in a `"use client"` module: this
69
+ * entry carries the client banner, so the factory is a client reference
70
+ * that a Server Component can pass along but cannot call, and the resolver
71
+ * is a closure that cannot cross the boundary as a prop either. The site's
72
+ * `next/link` adapter lives in the same kind of module for the same reason.
56
73
  */
57
- function richTextBlockRenderer({ internalDocToHref } = {}) {
74
+ function richTextBlockRenderer({ internalDocToHref, resolveLink: resolve } = {}) {
75
+ const toHref = internalDocToHref ?? (resolve ? internalDocToHrefFrom(resolve) : void 0);
58
76
  /**
59
77
  * A Lexical document as prose.
60
78
  *
@@ -78,7 +96,7 @@ function richTextBlockRenderer({ internalDocToHref } = {}) {
78
96
  className: cx(containerClassName, "py-16 lg:py-20"),
79
97
  children: /* @__PURE__ */ jsx(RichText, {
80
98
  data: block.content,
81
- converters: linkConverters(linkComponent, internalDocToHref),
99
+ converters: linkConverters(linkComponent, toHref),
82
100
  className: "max-w-prose"
83
101
  })
84
102
  })
@@ -89,6 +107,6 @@ function richTextBlockRenderer({ internalDocToHref } = {}) {
89
107
  /** The `richText` renderer with no internal-link resolver; see the factory. */
90
108
  const RichTextBlockRenderer = richTextBlockRenderer();
91
109
  //#endregion
92
- export { RichTextBlockRenderer, richTextBlockRenderer };
110
+ export { RichTextBlockRenderer, internalDocToHrefFrom, richTextBlockRenderer };
93
111
 
94
112
  //# sourceMappingURL=rich-text.mjs.map
@@ -1 +1 @@
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
+ {"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 { LinkDestination, ResolveLink } from \"../../fields/link\";\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 /**\n * Wins when both this and `resolveLink` are passed, so a site that already\n * wrote a custom Lexical resolver is unchanged.\n */\n internalDocToHref?: InternalDocToHref;\n /**\n * The same function `RenderBlocks` takes. When `internalDocToHref` is\n * omitted, the factory builds one with `internalDocToHrefFrom`.\n */\n resolveLink?: ResolveLink;\n}\n\n/**\n * Turns the site's `resolveLink` into the Lexical resolver the rich-text\n * factory needs. Unpacks `linkNode.fields.doc` into a page destination and\n * calls `resolveLink`; `null` (unpopulated or unpublished) becomes `#`, the\n * same fallback BIS-69 uses when no resolver is registered.\n */\nexport function internalDocToHrefFrom(resolve: ResolveLink): InternalDocToHref {\n return ({ linkNode }) => {\n const doc = linkNode.fields.doc;\n const value = doc && typeof doc === \"object\" ? doc.value : undefined;\n const page = (value ?? null) as LinkDestination[\"page\"];\n return resolve({ type: \"page\", page }) ?? \"#\";\n };\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 resolver 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({ resolveLink }) or 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 `resolveLink` (the same function\n * `RenderBlocks` takes; the factory wraps it with `internalDocToHrefFrom`)\n * or a hand-written `internalDocToHref`. When both are passed,\n * `internalDocToHref` wins. It must do so in a `\"use client\"` module: this\n * entry carries the client banner, so the factory is a client reference\n * that a Server Component can pass along but cannot call, and the resolver\n * is a closure that cannot cross the boundary as a prop either. The site's\n * `next/link` adapter lives in the same kind of module for the same reason.\n */\nexport function richTextBlockRenderer({\n internalDocToHref,\n resolveLink: resolve,\n}: RichTextBlockRendererOptions = {}) {\n const toHref = internalDocToHref ?? (resolve ? internalDocToHrefFrom(resolve) : undefined);\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, toHref)}\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":";;;;;;;;;;;AA0CA,SAAgB,sBAAsB,SAAyC;AAC7E,SAAQ,EAAE,eAAe;EACvB,MAAM,MAAM,SAAS,OAAO;AAG5B,SAAO,QAAQ;GAAE,MAAM;GAAQ,OAFjB,OAAO,OAAO,QAAQ,WAAW,IAAI,QAAQ,KAAA,MACpC;GACc,CAAC,IAAI;;;;;;;;;;;;;;;;;;;;;AAsB9C,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,ySACD;AACD,WAAO;;AAGX,UAAO,QAAQ,MAAM,MAAM,WAAW,EAAE,OAAO,KAAK,UAAU,CAAC,CAAC;;EAEnE;;;;;;;;;;;;;;;;AAiBH,SAAgB,sBAAsB,EACpC,mBACA,aAAa,YACmB,EAAE,EAAE;CACpC,MAAM,SAAS,sBAAsB,UAAU,sBAAsB,QAAQ,GAAG,KAAA;;;;;;;;;;;;;CAahF,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,OAAO;KACjD,WAAU;KACV,CAAA;IACE,CAAA;GACE,CAAA;;AAGd,QAAO;;;AAIT,MAAa,wBAAwB,uBAAuB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bison-lab/payload-blocks",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "Payload CMS block configs and renderers for the Bison Lab marketing blocks",
5
5
  "homepage": "https://components.bisonlab.ai",
6
6
  "repository": {