@avocadostudio-ai/blocks 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Render a rich-text value stored as a ProseMirror *document*.
3
+ *
4
+ * The markdown-string form has a renderer next door in `_shared.tsx`. This one
5
+ * exists because an integration whose CMS has real rich text — Sanity Portable
6
+ * Text, Contentful Rich Text — maps the field to a document instead, so nothing
7
+ * is flattened on the way in. Both produce the same HTML for the constructs
8
+ * they share; the document form additionally carries what markdown has no
9
+ * syntax for, which today means `underline` and whatever marks the CMS's own
10
+ * schema declares.
11
+ *
12
+ * A node type this renderer does not know contributes its children and nothing
13
+ * else, and `avocadoUnknownBlock` contributes nothing at all. That is
14
+ * deliberate: an embedded entry is the integration's to render, and a
15
+ * placeholder shipped to a reader is worse than an omission. Integrations that
16
+ * want to draw one pass `renderUnknown`.
17
+ */
18
+ import type { JSX, ReactNode } from "react";
19
+ import { type RichTextDoc, type RichTextMark } from "@avocadostudio-ai/shared";
20
+ export type RichTextDocumentProps = {
21
+ doc: RichTextDoc;
22
+ /**
23
+ * Draw a block this renderer cannot: an embedded entry, a table, a component
24
+ * from the CMS. Receives the source object the converter preserved.
25
+ */
26
+ renderUnknown?: (data: unknown, key: string) => ReactNode;
27
+ /**
28
+ * Draw a character style this renderer does not know — a site's own
29
+ * decorator, such as a brand highlight.
30
+ *
31
+ * Avocado ships five decorators; every CMS lets a project declare more, and
32
+ * a mark with no renderer would otherwise reach the page as unstyled text.
33
+ * Return `undefined` to decline and leave the content as it was.
34
+ */
35
+ renderMark?: (mark: RichTextMark, children: ReactNode, key: string) => ReactNode | undefined;
36
+ };
37
+ type RenderHooks = Pick<RichTextDocumentProps, "renderUnknown" | "renderMark">;
38
+ export declare function renderRichTextDocument(doc: RichTextDoc, hooks?: RenderHooks): ReactNode[];
39
+ /** The document form of `renderRichTextContent`, as a component. */
40
+ export declare function RichTextDocument({ doc, renderUnknown, renderMark }: RichTextDocumentProps): JSX.Element;
41
+ export {};
@@ -0,0 +1,109 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { MARK, NODE } from "@avocadostudio-ai/shared";
3
+ function markNames(node) {
4
+ return new Set((node.marks ?? []).map((mark) => mark.type));
5
+ }
6
+ const BUILT_IN_MARKS = new Set([
7
+ MARK.bold,
8
+ MARK.italic,
9
+ MARK.strike,
10
+ MARK.code,
11
+ MARK.underline,
12
+ MARK.link
13
+ ]);
14
+ function renderText(node, key, hooks) {
15
+ const marks = markNames(node);
16
+ let out = node.text ?? "";
17
+ // Innermost first, so the nesting matches what the markdown renderer emits
18
+ // for the same content and the two surfaces produce the same DOM.
19
+ if (marks.has(MARK.code))
20
+ out = _jsx("code", { children: out });
21
+ /*
22
+ * A site's own decorators sit just inside the standard emphasis, in the order
23
+ * the document lists them — so a highlighted phrase inside bold text nests as
24
+ * `<strong><span class="highlight">`, which is what a stylesheet expects.
25
+ */
26
+ if (hooks.renderMark) {
27
+ for (const mark of node.marks ?? []) {
28
+ if (BUILT_IN_MARKS.has(mark.type))
29
+ continue;
30
+ const custom = hooks.renderMark(mark, out, `${key}-${mark.type}`);
31
+ if (custom !== undefined)
32
+ out = custom;
33
+ }
34
+ }
35
+ if (marks.has(MARK.strike))
36
+ out = _jsx("s", { children: out });
37
+ if (marks.has(MARK.underline))
38
+ out = _jsx("u", { children: out });
39
+ if (marks.has(MARK.italic))
40
+ out = _jsx("em", { children: out });
41
+ if (marks.has(MARK.bold))
42
+ out = _jsx("strong", { children: out });
43
+ const link = (node.marks ?? []).find((mark) => mark.type === MARK.link);
44
+ const href = link?.attrs?.href;
45
+ if (typeof href === "string" && href.length > 0)
46
+ out = _jsx("a", { href: href, children: out });
47
+ return _jsx("span", { children: out }, key);
48
+ }
49
+ function renderInline(nodes, prefix, hooks) {
50
+ return (nodes ?? []).map((node, index) => {
51
+ const key = `${prefix}-${index}`;
52
+ if (node.type === NODE.hardBreak)
53
+ return _jsx("br", {}, key);
54
+ if (typeof node.text === "string")
55
+ return renderText(node, key, hooks);
56
+ return _jsx("span", { children: renderInline(node.content, key, hooks) }, key);
57
+ });
58
+ }
59
+ function textOf(nodes) {
60
+ return (nodes ?? []).map((node) => node.text ?? "").join("");
61
+ }
62
+ function isList(node) {
63
+ return node.type === NODE.bulletList || node.type === NODE.orderedList;
64
+ }
65
+ function renderNode(node, key, hooks) {
66
+ switch (node.type) {
67
+ case NODE.paragraph:
68
+ return _jsx("p", { children: renderInline(node.content, key, hooks) }, key);
69
+ case NODE.heading: {
70
+ const level = typeof node.attrs?.level === "number" ? Math.min(6, Math.max(1, node.attrs.level)) : 2;
71
+ const Tag = `h${level}`;
72
+ return _jsx(Tag, { children: renderInline(node.content, key, hooks) }, key);
73
+ }
74
+ case NODE.bulletList:
75
+ case NODE.orderedList: {
76
+ const Tag = node.type === NODE.orderedList ? "ol" : "ul";
77
+ return (_jsx(Tag, { children: (node.content ?? []).map((item, index) => {
78
+ const itemKey = `${key}-${index}`;
79
+ const children = item.content ?? [];
80
+ return (_jsx("li", { children: children.map((child, childIndex) => isList(child)
81
+ ? renderNode(child, `${itemKey}-${childIndex}`, hooks)
82
+ : child.type === NODE.paragraph
83
+ ? renderInline(child.content, `${itemKey}-${childIndex}`, hooks)
84
+ : renderNode(child, `${itemKey}-${childIndex}`, hooks)) }, itemKey));
85
+ }) }, key));
86
+ }
87
+ case NODE.blockquote:
88
+ return (_jsx("blockquote", { children: (node.content ?? []).map((child, index) => renderNode(child, `${key}-${index}`, hooks)) }, key));
89
+ case NODE.codeBlock: {
90
+ const language = typeof node.attrs?.language === "string" ? node.attrs.language : undefined;
91
+ return (_jsx("pre", { children: _jsx("code", { className: language ? `language-${language}` : undefined, children: textOf(node.content) }) }, key));
92
+ }
93
+ case NODE.horizontalRule:
94
+ return _jsx("hr", {}, key);
95
+ case NODE.unknown:
96
+ return hooks.renderUnknown ? hooks.renderUnknown(node.attrs?.data, key) : null;
97
+ default:
98
+ // Something this renderer has no element for but which still wraps
99
+ // content — keep the prose rather than dropping the subtree.
100
+ return (_jsx("div", { children: (node.content ?? []).map((child, index) => renderNode(child, `${key}-${index}`, hooks)) }, key));
101
+ }
102
+ }
103
+ export function renderRichTextDocument(doc, hooks = {}) {
104
+ return (doc.content ?? []).map((node, index) => renderNode(node, `n${index}`, hooks));
105
+ }
106
+ /** The document form of `renderRichTextContent`, as a component. */
107
+ export function RichTextDocument({ doc, renderUnknown, renderMark }) {
108
+ return _jsx(_Fragment, { children: renderRichTextDocument(doc, { renderUnknown, renderMark }) });
109
+ }
@@ -1,5 +1,8 @@
1
1
  import type { AnchorHTMLAttributes, JSX, ReactNode } from "react";
2
+ import { normalizeRichTextBody } from "@avocadostudio-ai/shared";
2
3
  export { BlockImage, type BlockImageProps } from "./block-image.tsx";
4
+ export { RichTextDocument, renderRichTextDocument, type RichTextDocumentProps } from "./_richtext-document.tsx";
5
+ export { normalizeRichTextBody };
3
6
  /**
4
7
  * Render a block "icon" prop safely. Icons may be a single emoji/symbol or an
5
8
  * image URL. Anything else (a bare icon-library name, descriptive text, an
@@ -20,5 +23,4 @@ export declare function PrimaryButton({ href, children, className, ...rest }: Bu
20
23
  export declare function SecondaryButton({ href, children, className, ...rest }: ButtonProps): import("react/jsx-runtime").JSX.Element;
21
24
  export declare function TertiaryButton({ href, children, className, ...rest }: ButtonProps): import("react/jsx-runtime").JSX.Element;
22
25
  export declare function renderInline(text: string): (string | JSX.Element)[];
23
- export declare function normalizeRichTextBody(input: string): string;
24
- export declare function renderRichTextContent(input: string): (import("react/jsx-runtime").JSX.Element | import("react/jsx-runtime").JSX.Element[] | null)[];
26
+ export declare function renderRichTextContent(input: string): (import("react/jsx-runtime").JSX.Element | import("react/jsx-runtime").JSX.Element[])[];
@@ -1,5 +1,11 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { parseInline, parseRichText, normalizeRichTextBody, resolveRichTextHeadingLevel } from "@avocadostudio-ai/shared";
2
3
  export { BlockImage } from "./block-image.js";
4
+ export { RichTextDocument, renderRichTextDocument } from "./_richtext-document.js";
5
+ // The grammar itself lives in @avocadostudio-ai/shared so that this renderer,
6
+ // the editor overlay and the Puck adapter cannot drift apart again. What stays
7
+ // here is presentation: which React elements a token becomes.
8
+ export { normalizeRichTextBody };
3
9
  function isIconUrl(value) {
4
10
  return /^https?:\/\//i.test(value);
5
11
  }
@@ -79,75 +85,60 @@ export function SecondaryButton({ href, children, className, ...rest }) {
79
85
  export function TertiaryButton({ href, children, className, ...rest }) {
80
86
  return (_jsx("a", { href: href, className: ["btn-tertiary", className].filter(Boolean).join(" "), ...rest, children: children }));
81
87
  }
82
- export function renderInline(text) {
83
- const tokens = [];
84
- const regex = /\*\*(.+?)\*\*|\*(.+?)\*|\[(.+?)\]\((.+?)\)/g;
85
- let last = 0;
86
- let match;
87
- while ((match = regex.exec(text)) !== null) {
88
- if (match.index > last)
89
- tokens.push(text.slice(last, match.index));
90
- if (match[1] !== undefined) {
91
- tokens.push(_jsx("strong", { children: match[1] }, match.index));
92
- }
93
- else if (match[2] !== undefined) {
94
- tokens.push(_jsx("em", { children: match[2] }, match.index));
95
- }
96
- else if (match[3] !== undefined && match[4] !== undefined) {
97
- tokens.push(_jsx("a", { href: match[4], children: match[3] }, match.index));
98
- }
99
- last = match.index + match[0].length;
88
+ function renderInlineToken(token) {
89
+ switch (token.type) {
90
+ case "strong":
91
+ return _jsx("strong", { children: token.text }, token.at);
92
+ case "em":
93
+ return _jsx("em", { children: token.text }, token.at);
94
+ case "strike":
95
+ return _jsx("s", { children: token.text }, token.at);
96
+ case "code":
97
+ return _jsx("code", { children: token.text }, token.at);
98
+ case "link":
99
+ return (_jsx("a", { href: token.href, children: token.text }, token.at));
100
+ case "break":
101
+ return _jsx("br", {}, token.at);
102
+ default:
103
+ return token.text;
100
104
  }
101
- if (last < text.length)
102
- tokens.push(text.slice(last));
103
- return tokens;
104
105
  }
105
- export function normalizeRichTextBody(input) {
106
- return input
107
- .replace(/\r\n?/g, "\n")
108
- // Fix run-on sentence joins like "requested.Here's" -> "requested. Here's".
109
- .replace(/([.!?])([A-Z])/g, "$1 $2")
110
- // Avoid excessive blank lines while preserving paragraph breaks.
111
- .replace(/\n{3,}/g, "\n\n")
112
- .trim();
106
+ export function renderInline(text) {
107
+ return parseInline(text).map(renderInlineToken);
113
108
  }
114
- function renderRichTextBlock(block, idx) {
115
- const lines = block
116
- .split(/\n+/)
117
- .map((line) => line.trim())
118
- .filter(Boolean);
119
- if (lines.length === 0)
120
- return null;
121
- const headingMatch = /^(#{1,6})\s+(.+)$/.exec(lines[0]);
122
- if (headingMatch) {
123
- const heading = _jsx("h3", { children: renderInline(headingMatch[2].trim()) }, `${idx}-heading`);
124
- const remainder = lines.slice(1).join(" ").trim();
125
- if (!remainder)
126
- return heading;
127
- return [
128
- heading,
129
- _jsx("p", { children: renderInline(remainder) }, `${idx}-paragraph`)
130
- ];
131
- }
132
- const unorderedItems = lines
133
- .map((line) => /^\s*[-*+•]\s+(.+)$/.exec(line)?.[1]?.trim() ?? null)
134
- .filter((line) => !!line && line.length > 0);
135
- if (unorderedItems.length === lines.length) {
136
- return (_jsx("ul", { children: unorderedItems.map((item, itemIdx) => (_jsx("li", { children: renderInline(item) }, itemIdx))) }, idx));
137
- }
138
- const orderedItems = lines
139
- .map((line) => /^\s*\d+[.)]\s+(.+)$/.exec(line)?.[1]?.trim() ?? null)
140
- .filter((line) => !!line && line.length > 0);
141
- if (orderedItems.length === lines.length) {
142
- return (_jsx("ol", { children: orderedItems.map((item, itemIdx) => (_jsx("li", { children: renderInline(item) }, itemIdx))) }, idx));
109
+ function renderItems(items) {
110
+ return items.map((item, itemIdx) => (_jsxs("li", { children: [item.inline.map(renderInlineToken), item.children
111
+ ? item.children.type === "ordered-list"
112
+ ? _jsx("ol", { children: renderItems(item.children.items) })
113
+ : _jsx("ul", { children: renderItems(item.children.items) })
114
+ : null] }, itemIdx)));
115
+ }
116
+ function renderRichTextBlock(block) {
117
+ switch (block.type) {
118
+ case "heading": {
119
+ const Tag = `h${resolveRichTextHeadingLevel(block.level)}`;
120
+ const heading = _jsx(Tag, { children: block.inline.map(renderInlineToken) }, `${block.index}-heading`);
121
+ if (!block.trailing)
122
+ return heading;
123
+ return [
124
+ heading,
125
+ _jsx("p", { children: block.trailing.map(renderInlineToken) }, `${block.index}-paragraph`)
126
+ ];
127
+ }
128
+ case "unordered-list":
129
+ return _jsx("ul", { children: renderItems(block.items) }, block.index);
130
+ case "ordered-list":
131
+ return _jsx("ol", { children: renderItems(block.items) }, block.index);
132
+ case "blockquote":
133
+ return _jsx("blockquote", { children: block.children.map(renderRichTextBlock) }, block.index);
134
+ case "code":
135
+ return (_jsx("pre", { children: _jsx("code", { className: block.language ? `language-${block.language}` : undefined, children: block.code }) }, block.index));
136
+ case "rule":
137
+ return _jsx("hr", {}, block.index);
138
+ default:
139
+ return _jsx("p", { children: block.inline.map(renderInlineToken) }, block.index);
143
140
  }
144
- return _jsx("p", { children: renderInline(block) }, idx);
145
141
  }
146
142
  export function renderRichTextContent(input) {
147
- const body = normalizeRichTextBody(input);
148
- const blocks = body
149
- .split(/\n\s*\n+/)
150
- .map((block) => block.trim())
151
- .filter(Boolean);
152
- return blocks.map((block, idx) => renderRichTextBlock(block, idx));
143
+ return parseRichText(input).map(renderRichTextBlock);
153
144
  }
@@ -35,16 +35,74 @@
35
35
  margin-top: 0.3em;
36
36
  }
37
37
 
38
- .rich-text__body h3 {
38
+ /*
39
+ * Body headings. A `##` renders <h2> here, so every level the parser can emit
40
+ * needs a size — without these the browser default applies and a body heading
41
+ * comes out larger than the section title above it. The scale stays below
42
+ * `.section__inner > h2`, which is the section's own heading.
43
+ */
44
+ .rich-text__body :is(h2, h3, h4, h5, h6) {
39
45
  margin-top: 1.25em;
40
46
  margin-bottom: 0.45em;
41
- font-size: 1.25rem;
42
47
  line-height: 1.3;
43
48
  letter-spacing: -0.01em;
44
49
  font-weight: 700;
45
50
  color: var(--heading);
46
51
  }
47
52
 
53
+ .rich-text__body h2 { font-size: 1.45rem; }
54
+ .rich-text__body h3 { font-size: 1.25rem; }
55
+ .rich-text__body h4 { font-size: 1.1rem; }
56
+ .rich-text__body h5 { font-size: 1rem; }
57
+ .rich-text__body h6 { font-size: 0.9375rem; }
58
+
59
+
60
+ /* Constructs the editor can produce and the parser now renders. Without sizes
61
+ here they fall back to browser defaults, which do not match the block. */
62
+ .rich-text__body blockquote {
63
+ margin: 1.25em 0;
64
+ padding: 0.15em 0 0.15em 1em;
65
+ border-left: 3px solid var(--rule, rgba(127, 127, 127, 0.3));
66
+ color: var(--body);
67
+ font-style: italic;
68
+ }
69
+
70
+ .rich-text__body blockquote > :last-child {
71
+ margin-bottom: 0;
72
+ }
73
+
74
+ .rich-text__body pre {
75
+ margin: 1.25em 0;
76
+ padding: 12px 14px;
77
+ border-radius: 6px;
78
+ background: rgba(127, 127, 127, 0.12);
79
+ overflow-x: auto;
80
+ }
81
+
82
+ .rich-text__body pre code {
83
+ background: none;
84
+ padding: 0;
85
+ font-size: 0.875em;
86
+ line-height: 1.55;
87
+ }
88
+
89
+ .rich-text__body code {
90
+ padding: 0.12em 0.36em;
91
+ border-radius: 3px;
92
+ background: rgba(127, 127, 127, 0.15);
93
+ font-size: 0.9em;
94
+ }
95
+
96
+ .rich-text__body hr {
97
+ margin: 1.75em 0;
98
+ border: 0;
99
+ border-top: 1px solid var(--rule, rgba(127, 127, 127, 0.3));
100
+ }
101
+
102
+ .rich-text__body :is(ul, ol) :is(ul, ol) {
103
+ margin: 0.3em 0 0;
104
+ }
105
+
48
106
  /* Dark */
49
107
  .dark .rich-text__body,
50
108
  .dark .rich-text__body li,
@@ -85,13 +85,20 @@
85
85
  margin-bottom: 0;
86
86
  }
87
87
 
88
- .tabs-block__content h3 {
88
+ /* Body headings — see the note in rich-text/styles.css. Tighter scale than the
89
+ other rich bodies because tab panels are short. */
90
+ .tabs-block__content :is(h2, h3, h4, h5, h6) {
89
91
  margin: 0 0 8px;
90
- font-size: 1.15rem;
91
92
  font-weight: 600;
92
93
  color: var(--heading);
93
94
  }
94
95
 
96
+ .tabs-block__content h2 { font-size: 1.3rem; }
97
+ .tabs-block__content h3 { font-size: 1.15rem; }
98
+ .tabs-block__content h4 { font-size: 1.05rem; }
99
+ .tabs-block__content h5 { font-size: 1rem; }
100
+ .tabs-block__content h6 { font-size: 0.9375rem; }
101
+
95
102
  .tabs-block__content ul {
96
103
  margin: 0 0 12px;
97
104
  padding-left: 24px;
@@ -145,3 +152,50 @@
145
152
  margin-bottom: 16px;
146
153
  }
147
154
  }
155
+
156
+ /* Constructs the editor can produce and the parser now renders. Without sizes
157
+ here they fall back to browser defaults, which do not match the block. */
158
+ .tabs-block__content blockquote {
159
+ margin: 1.25em 0;
160
+ padding: 0.15em 0 0.15em 1em;
161
+ border-left: 3px solid var(--rule, rgba(127, 127, 127, 0.3));
162
+ color: var(--body);
163
+ font-style: italic;
164
+ }
165
+
166
+ .tabs-block__content blockquote > :last-child {
167
+ margin-bottom: 0;
168
+ }
169
+
170
+ .tabs-block__content pre {
171
+ margin: 1.25em 0;
172
+ padding: 12px 14px;
173
+ border-radius: 6px;
174
+ background: rgba(127, 127, 127, 0.12);
175
+ overflow-x: auto;
176
+ }
177
+
178
+ .tabs-block__content pre code {
179
+ background: none;
180
+ padding: 0;
181
+ font-size: 0.875em;
182
+ line-height: 1.55;
183
+ }
184
+
185
+ .tabs-block__content code {
186
+ padding: 0.12em 0.36em;
187
+ border-radius: 3px;
188
+ background: rgba(127, 127, 127, 0.15);
189
+ font-size: 0.9em;
190
+ }
191
+
192
+ .tabs-block__content hr {
193
+ margin: 1.75em 0;
194
+ border: 0;
195
+ border-top: 1px solid var(--rule, rgba(127, 127, 127, 0.3));
196
+ }
197
+
198
+ .tabs-block__content :is(ul, ol) :is(ul, ol) {
199
+ margin: 0.3em 0 0;
200
+ }
201
+
@@ -57,16 +57,24 @@
57
57
  margin-top: 0.3em;
58
58
  }
59
59
 
60
- .two-column__body h3 {
60
+ /* Body headings — see the note in rich-text/styles.css. These rules come after
61
+ `.two-column__text h2` on purpose: same specificity, so order decides, and a
62
+ heading written inside the body must not take the column title's size. */
63
+ .two-column__body :is(h2, h3, h4, h5, h6) {
61
64
  margin-top: 1.25em;
62
65
  margin-bottom: 0.45em;
63
- font-size: 1.25rem;
64
66
  line-height: 1.3;
65
67
  letter-spacing: -0.01em;
66
68
  font-weight: 700;
67
69
  color: var(--heading);
68
70
  }
69
71
 
72
+ .two-column__body h2 { font-size: 1.45rem; }
73
+ .two-column__body h3 { font-size: 1.25rem; }
74
+ .two-column__body h4 { font-size: 1.1rem; }
75
+ .two-column__body h5 { font-size: 1rem; }
76
+ .two-column__body h6 { font-size: 0.9375rem; }
77
+
70
78
  .two-column__cta {
71
79
  margin-top: 8px;
72
80
  }
@@ -81,3 +89,50 @@
81
89
  order: 0;
82
90
  }
83
91
  }
92
+
93
+ /* Constructs the editor can produce and the parser now renders. Without sizes
94
+ here they fall back to browser defaults, which do not match the block. */
95
+ .two-column__body blockquote {
96
+ margin: 1.25em 0;
97
+ padding: 0.15em 0 0.15em 1em;
98
+ border-left: 3px solid var(--rule, rgba(127, 127, 127, 0.3));
99
+ color: var(--body);
100
+ font-style: italic;
101
+ }
102
+
103
+ .two-column__body blockquote > :last-child {
104
+ margin-bottom: 0;
105
+ }
106
+
107
+ .two-column__body pre {
108
+ margin: 1.25em 0;
109
+ padding: 12px 14px;
110
+ border-radius: 6px;
111
+ background: rgba(127, 127, 127, 0.12);
112
+ overflow-x: auto;
113
+ }
114
+
115
+ .two-column__body pre code {
116
+ background: none;
117
+ padding: 0;
118
+ font-size: 0.875em;
119
+ line-height: 1.55;
120
+ }
121
+
122
+ .two-column__body code {
123
+ padding: 0.12em 0.36em;
124
+ border-radius: 3px;
125
+ background: rgba(127, 127, 127, 0.15);
126
+ font-size: 0.9em;
127
+ }
128
+
129
+ .two-column__body hr {
130
+ margin: 1.75em 0;
131
+ border: 0;
132
+ border-top: 1px solid var(--rule, rgba(127, 127, 127, 0.3));
133
+ }
134
+
135
+ .two-column__body :is(ul, ol) :is(ul, ol) {
136
+ margin: 0.3em 0 0;
137
+ }
138
+
package/dist/index.d.ts CHANGED
@@ -5,3 +5,4 @@ export { BlocksHydrator } from "./blocks-hydrator.tsx";
5
5
  export { BlockErrorBoundary } from "./block-error-boundary.tsx";
6
6
  export { rendererTypes } from "./blocks/index.ts";
7
7
  export type { BlockImageProps } from "./blocks/block-image.tsx";
8
+ export { RichTextDocument, renderRichTextDocument, type RichTextDocumentProps } from "./blocks/_richtext-document.tsx";
package/dist/index.js CHANGED
@@ -3,3 +3,4 @@ export { BlockCatalogue } from "./catalogue.js";
3
3
  export { BlocksHydrator } from "./blocks-hydrator.js";
4
4
  export { BlockErrorBoundary } from "./block-error-boundary.js";
5
5
  export { rendererTypes } from "./blocks/index.js";
6
+ export { RichTextDocument, renderRichTextDocument } from "./blocks/_richtext-document.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avocadostudio-ai/blocks",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,7 +8,8 @@
8
8
  "./package.json": "./package.json",
9
9
  ".": {
10
10
  "types": "./dist/index.d.ts",
11
- "import": "./dist/index.js"
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
12
13
  },
13
14
  "./styles.css": "./dist/blocks/styles.css"
14
15
  },
@@ -25,10 +26,11 @@
25
26
  "dist"
26
27
  ],
27
28
  "dependencies": {
28
- "@avocadostudio-ai/shared": "0.1.0"
29
+ "@avocadostudio-ai/richtext": "0.2.1",
30
+ "@avocadostudio-ai/shared": "0.2.1"
29
31
  },
30
32
  "peerDependencies": {
31
- "next": ">=14.0.0",
33
+ "next": ">=15.0.0",
32
34
  "react": ">=19.0.0",
33
35
  "react-dom": ">=19.0.0"
34
36
  },
@@ -47,8 +49,7 @@
47
49
  },
48
50
  "scripts": {
49
51
  "build": "tsc -p tsconfig.build.json && node ./scripts/copy-css.mjs",
50
- "test": "NODE_ENV=test tsx --test src/blocks/*/renderer.test.ts src/blocks/*.test.ts",
51
- "test:unit": "NODE_ENV=test tsx --test src/blocks/*/renderer.test.ts src/blocks/*.test.ts",
52
+ "test": "NODE_ENV=test node ../../scripts/run-tests.mjs",
52
53
  "typecheck": "tsc --noEmit"
53
54
  }
54
55
  }