@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.
- package/dist/blocks/_richtext-document.d.ts +41 -0
- package/dist/blocks/_richtext-document.js +109 -0
- package/dist/blocks/_shared.d.ts +4 -2
- package/dist/blocks/_shared.js +57 -66
- package/dist/blocks/rich-text/styles.css +60 -2
- package/dist/blocks/tabs/styles.css +56 -2
- package/dist/blocks/two-column/styles.css +57 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +7 -6
|
@@ -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
|
+
}
|
package/dist/blocks/_shared.d.ts
CHANGED
|
@@ -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
|
|
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[])[];
|
package/dist/blocks/_shared.js
CHANGED
|
@@ -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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
106
|
-
return
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
|
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/
|
|
29
|
+
"@avocadostudio-ai/richtext": "0.2.1",
|
|
30
|
+
"@avocadostudio-ai/shared": "0.2.1"
|
|
29
31
|
},
|
|
30
32
|
"peerDependencies": {
|
|
31
|
-
"next": ">=
|
|
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
|
|
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
|
}
|