@kyro-cms/kyro-rich-text-react 0.12.15

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 ADDED
@@ -0,0 +1,172 @@
1
+ # kyro-rich-text-react
2
+
3
+ A lightweight, headless React renderer for [Kyro CMS](https://kyro.dev) rich text content.
4
+
5
+ When you use the `richText` field in Kyro CMS, it stores the content as structured Tiptap/ProseMirror JSON (not raw HTML). This package allows you to easily render that JSON structure into React components.
6
+
7
+ By default, it is **completely headless and unstyled**, outputting clean semantic HTML (`<p>`, `<h1>`, `<strong>`). You have full control over the rendering by passing in custom components.
8
+
9
+ ---
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install kyro-rich-text-react
15
+ # or
16
+ pnpm add kyro-rich-text-react
17
+ # or
18
+ yarn add kyro-rich-text-react
19
+ ```
20
+
21
+ ---
22
+
23
+ ## Quick Start
24
+
25
+ Import the `KyroRichTextRenderer` component and pass the JSON array from your Kyro API to the `content` prop.
26
+
27
+ ```tsx
28
+ import React from 'react';
29
+ import { KyroRichTextRenderer } from 'kyro-rich-text-react';
30
+
31
+ export default function BlogPost({ post }) {
32
+ // post.content is the JSON array from Kyro
33
+ return (
34
+ <article className="blog-content">
35
+ <KyroRichTextRenderer content={post.content} />
36
+ </article>
37
+ );
38
+ }
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Customizing Components (Advanced)
44
+
45
+ Because the renderer is headless, it doesn't come with CSS classes. If you use a CSS framework like Tailwind, or if you simply want to use your own custom React components for specific nodes (like links or headings), use the `components` prop.
46
+
47
+ The `components` prop accepts an object with two keys:
48
+ - `types`: For block-level nodes (paragraphs, headings, blockquotes, images)
49
+ - `marks`: For inline text formatting (bold, italic, links)
50
+
51
+ ### Example: Tailwind CSS & Custom Components
52
+
53
+ ```tsx
54
+ import React from 'react';
55
+ import { KyroRichTextRenderer, KyroRichTextComponents } from 'kyro-rich-text-react';
56
+ import Link from 'next/link';
57
+
58
+ const customComponents: KyroRichTextComponents = {
59
+ types: {
60
+ // Override headings to include Tailwind classes
61
+ heading: ({ node, children }) => {
62
+ const level = node.attrs?.level || 1;
63
+ const classes = {
64
+ 1: 'text-4xl font-extrabold mb-6 mt-8',
65
+ 2: 'text-2xl font-bold mb-4 mt-6',
66
+ 3: 'text-xl font-semibold mb-3 mt-4',
67
+ };
68
+ const Tag = `h${level}` as keyof JSX.IntrinsicElements;
69
+
70
+ return <Tag className={classes[level] || ''}>{children}</Tag>;
71
+ },
72
+
73
+ // Custom Paragraphs
74
+ paragraph: ({ children }) => (
75
+ <p className="text-gray-700 leading-relaxed mb-4">{children}</p>
76
+ ),
77
+
78
+ // Render Kyro uploads as responsive images
79
+ image: ({ node }) => (
80
+ <img
81
+ src={node.attrs?.src}
82
+ alt={node.attrs?.alt || 'Image'}
83
+ className="w-full rounded-xl shadow-lg my-6"
84
+ />
85
+ ),
86
+ },
87
+
88
+ marks: {
89
+ // Map links to Next.js Link component for internal routing
90
+ link: ({ mark, children }) => {
91
+ const href = mark.attrs?.href || '#';
92
+ const isInternal = href.startsWith('/') || href.startsWith('#');
93
+
94
+ if (isInternal) {
95
+ return <Link href={href} className="text-blue-600 hover:underline">{children}</Link>;
96
+ }
97
+
98
+ // External link
99
+ return (
100
+ <a href={href} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
101
+ {children}
102
+ </a>
103
+ );
104
+ },
105
+
106
+ // Style bold text
107
+ bold: ({ children }) => <strong className="font-bold text-gray-900">{children}</strong>
108
+ }
109
+ };
110
+
111
+ export default function BlogPost({ post }) {
112
+ return (
113
+ <KyroRichTextRenderer
114
+ content={post.content}
115
+ components={customComponents}
116
+ />
117
+ );
118
+ }
119
+ ```
120
+
121
+ ---
122
+
123
+ ## Using with Tailwind Typography
124
+
125
+ If you use the Tailwind `@tailwindcss/typography` plugin, you don't even need to write custom overrides. You can simply wrap the default headless output in a `.prose` class, and Tailwind will style all the semantic HTML automatically:
126
+
127
+ ```tsx
128
+ import { KyroRichTextRenderer } from 'kyro-rich-text-react';
129
+
130
+ export default function BlogPost({ post }) {
131
+ return (
132
+ <div className="prose prose-blue lg:prose-xl mx-auto">
133
+ <KyroRichTextRenderer content={post.content} />
134
+ </div>
135
+ );
136
+ }
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Supported Nodes and Marks
142
+
143
+ By default, the renderer supports mapping these Tiptap formats to standard HTML tags:
144
+
145
+ **Types (Blocks):**
146
+ - `doc` -> `<React.Fragment>`
147
+ - `paragraph` -> `<p>`
148
+ - `heading` -> `<h1>` - `<h6>`
149
+ - `blockquote` -> `<blockquote>`
150
+ - `bulletList` -> `<ul>`
151
+ - `orderedList` -> `<ol>`
152
+ - `listItem` -> `<li>`
153
+ - `codeBlock` -> `<pre><code>`
154
+ - `horizontalRule` -> `<hr />`
155
+ - `hardBreak` -> `<br />`
156
+ - `image` -> `<img>`
157
+
158
+ **Marks (Inline):**
159
+ - `bold` -> `<strong>`
160
+ - `italic` -> `<em>`
161
+ - `strike` -> `<s>`
162
+ - `code` -> `<code>`
163
+ - `underline` -> `<u>`
164
+ - `link` -> `<a>`
165
+
166
+ If your Kyro CMS uses custom blocks in the rich text editor, you can handle them by adding a new key to the `types` object in your `components` prop matching your custom block's `type` string.
167
+
168
+ ---
169
+
170
+ ## License
171
+
172
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ KyroRichTextRenderer: () => KyroRichTextRenderer,
34
+ defaultMarks: () => defaultMarks,
35
+ defaultTypes: () => defaultTypes
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/KyroRichTextRenderer.tsx
40
+ var import_react = __toESM(require("react"), 1);
41
+
42
+ // src/defaultComponents.tsx
43
+ var import_jsx_runtime = require("react/jsx-runtime");
44
+ var defaultTypes = {
45
+ doc: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children }),
46
+ paragraph: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children }),
47
+ heading: ({ node, children }) => {
48
+ const level = node.attrs?.level || 1;
49
+ const Tag = `h${level}`;
50
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tag, { children });
51
+ },
52
+ blockquote: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("blockquote", { children }),
53
+ bulletList: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children }),
54
+ orderedList: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ol", { children }),
55
+ listItem: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children }),
56
+ codeBlock: ({ node, children }) => {
57
+ const language = node.attrs?.language;
58
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { className: language ? `language-${language}` : "", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children }) });
59
+ },
60
+ horizontalRule: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("hr", {}),
61
+ hardBreak: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("br", {}),
62
+ image: ({ node }) => {
63
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
64
+ "img",
65
+ {
66
+ src: node.attrs?.src,
67
+ alt: node.attrs?.alt || "",
68
+ title: node.attrs?.title || ""
69
+ }
70
+ );
71
+ }
72
+ };
73
+ var defaultMarks = {
74
+ bold: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children }),
75
+ italic: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("em", { children }),
76
+ strike: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("s", { children }),
77
+ code: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children }),
78
+ underline: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("u", { children }),
79
+ link: ({ mark, children }) => {
80
+ const { href, target, rel } = mark.attrs || {};
81
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href, target, rel: rel || (target === "_blank" ? "noopener noreferrer" : void 0), children });
82
+ }
83
+ };
84
+
85
+ // src/KyroRichTextRenderer.tsx
86
+ var import_jsx_runtime2 = require("react/jsx-runtime");
87
+ var KyroRichTextRenderer = ({ content, components = {} }) => {
88
+ const mergedTypes = (0, import_react.useMemo)(
89
+ () => ({ ...defaultTypes, ...components.types }),
90
+ [components.types]
91
+ );
92
+ const mergedMarks = (0, import_react.useMemo)(
93
+ () => ({ ...defaultMarks, ...components.marks }),
94
+ [components.marks]
95
+ );
96
+ const renderNode = (node, index) => {
97
+ if (node.type === "text") {
98
+ let element = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react.default.Fragment, { children: node.text }, index);
99
+ if (node.marks && node.marks.length > 0) {
100
+ const marks = [...node.marks].reverse();
101
+ marks.forEach((mark, markIndex) => {
102
+ const MarkComponent = mergedMarks[mark.type];
103
+ if (MarkComponent) {
104
+ element = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MarkComponent, { mark, children: element }, `${index}-${markIndex}`);
105
+ }
106
+ });
107
+ }
108
+ return element;
109
+ }
110
+ if (node.type) {
111
+ const NodeComponent = mergedTypes[node.type];
112
+ let children = null;
113
+ if (node.content && node.content.length > 0) {
114
+ children = node.content.map((child, childIndex) => renderNode(child, childIndex));
115
+ }
116
+ if (NodeComponent) {
117
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(NodeComponent, { node, children }, index);
118
+ }
119
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react.default.Fragment, { children }, index);
120
+ }
121
+ return null;
122
+ };
123
+ if (!content) return null;
124
+ if (Array.isArray(content)) {
125
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: content.map((node, index) => renderNode(node, index)) });
126
+ }
127
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: renderNode(content, 0) });
128
+ };
129
+ // Annotate the CommonJS export names for ESM import in node:
130
+ 0 && (module.exports = {
131
+ KyroRichTextRenderer,
132
+ defaultMarks,
133
+ defaultTypes
134
+ });
@@ -0,0 +1,40 @@
1
+ import React$1 from 'react';
2
+
3
+ interface JSONContent {
4
+ type?: string;
5
+ attrs?: Record<string, any>;
6
+ content?: JSONContent[];
7
+ marks?: {
8
+ type: string;
9
+ attrs?: Record<string, any>;
10
+ [key: string]: any;
11
+ }[];
12
+ text?: string;
13
+ [key: string]: any;
14
+ }
15
+ type CustomComponentProps = {
16
+ node: JSONContent;
17
+ children?: React.ReactNode;
18
+ };
19
+ type MarkComponentProps = {
20
+ mark: {
21
+ type: string;
22
+ attrs?: Record<string, any>;
23
+ };
24
+ children?: React.ReactNode;
25
+ };
26
+ interface KyroRichTextComponents {
27
+ types?: Record<string, React.ComponentType<CustomComponentProps>>;
28
+ marks?: Record<string, React.ComponentType<MarkComponentProps>>;
29
+ }
30
+
31
+ interface KyroRichTextRendererProps {
32
+ content: JSONContent | JSONContent[];
33
+ components?: KyroRichTextComponents;
34
+ }
35
+ declare const KyroRichTextRenderer: React$1.FC<KyroRichTextRendererProps>;
36
+
37
+ declare const defaultTypes: Record<string, React$1.ComponentType<CustomComponentProps>>;
38
+ declare const defaultMarks: Record<string, React$1.ComponentType<MarkComponentProps>>;
39
+
40
+ export { type CustomComponentProps, type JSONContent, type KyroRichTextComponents, KyroRichTextRenderer, type MarkComponentProps, defaultMarks, defaultTypes };
@@ -0,0 +1,40 @@
1
+ import React$1 from 'react';
2
+
3
+ interface JSONContent {
4
+ type?: string;
5
+ attrs?: Record<string, any>;
6
+ content?: JSONContent[];
7
+ marks?: {
8
+ type: string;
9
+ attrs?: Record<string, any>;
10
+ [key: string]: any;
11
+ }[];
12
+ text?: string;
13
+ [key: string]: any;
14
+ }
15
+ type CustomComponentProps = {
16
+ node: JSONContent;
17
+ children?: React.ReactNode;
18
+ };
19
+ type MarkComponentProps = {
20
+ mark: {
21
+ type: string;
22
+ attrs?: Record<string, any>;
23
+ };
24
+ children?: React.ReactNode;
25
+ };
26
+ interface KyroRichTextComponents {
27
+ types?: Record<string, React.ComponentType<CustomComponentProps>>;
28
+ marks?: Record<string, React.ComponentType<MarkComponentProps>>;
29
+ }
30
+
31
+ interface KyroRichTextRendererProps {
32
+ content: JSONContent | JSONContent[];
33
+ components?: KyroRichTextComponents;
34
+ }
35
+ declare const KyroRichTextRenderer: React$1.FC<KyroRichTextRendererProps>;
36
+
37
+ declare const defaultTypes: Record<string, React$1.ComponentType<CustomComponentProps>>;
38
+ declare const defaultMarks: Record<string, React$1.ComponentType<MarkComponentProps>>;
39
+
40
+ export { type CustomComponentProps, type JSONContent, type KyroRichTextComponents, KyroRichTextRenderer, type MarkComponentProps, defaultMarks, defaultTypes };
package/dist/index.js ADDED
@@ -0,0 +1,95 @@
1
+ // src/KyroRichTextRenderer.tsx
2
+ import React, { useMemo } from "react";
3
+
4
+ // src/defaultComponents.tsx
5
+ import { Fragment, jsx } from "react/jsx-runtime";
6
+ var defaultTypes = {
7
+ doc: ({ children }) => /* @__PURE__ */ jsx(Fragment, { children }),
8
+ paragraph: ({ children }) => /* @__PURE__ */ jsx("p", { children }),
9
+ heading: ({ node, children }) => {
10
+ const level = node.attrs?.level || 1;
11
+ const Tag = `h${level}`;
12
+ return /* @__PURE__ */ jsx(Tag, { children });
13
+ },
14
+ blockquote: ({ children }) => /* @__PURE__ */ jsx("blockquote", { children }),
15
+ bulletList: ({ children }) => /* @__PURE__ */ jsx("ul", { children }),
16
+ orderedList: ({ children }) => /* @__PURE__ */ jsx("ol", { children }),
17
+ listItem: ({ children }) => /* @__PURE__ */ jsx("li", { children }),
18
+ codeBlock: ({ node, children }) => {
19
+ const language = node.attrs?.language;
20
+ return /* @__PURE__ */ jsx("pre", { className: language ? `language-${language}` : "", children: /* @__PURE__ */ jsx("code", { children }) });
21
+ },
22
+ horizontalRule: () => /* @__PURE__ */ jsx("hr", {}),
23
+ hardBreak: () => /* @__PURE__ */ jsx("br", {}),
24
+ image: ({ node }) => {
25
+ return /* @__PURE__ */ jsx(
26
+ "img",
27
+ {
28
+ src: node.attrs?.src,
29
+ alt: node.attrs?.alt || "",
30
+ title: node.attrs?.title || ""
31
+ }
32
+ );
33
+ }
34
+ };
35
+ var defaultMarks = {
36
+ bold: ({ children }) => /* @__PURE__ */ jsx("strong", { children }),
37
+ italic: ({ children }) => /* @__PURE__ */ jsx("em", { children }),
38
+ strike: ({ children }) => /* @__PURE__ */ jsx("s", { children }),
39
+ code: ({ children }) => /* @__PURE__ */ jsx("code", { children }),
40
+ underline: ({ children }) => /* @__PURE__ */ jsx("u", { children }),
41
+ link: ({ mark, children }) => {
42
+ const { href, target, rel } = mark.attrs || {};
43
+ return /* @__PURE__ */ jsx("a", { href, target, rel: rel || (target === "_blank" ? "noopener noreferrer" : void 0), children });
44
+ }
45
+ };
46
+
47
+ // src/KyroRichTextRenderer.tsx
48
+ import { Fragment as Fragment2, jsx as jsx2 } from "react/jsx-runtime";
49
+ var KyroRichTextRenderer = ({ content, components = {} }) => {
50
+ const mergedTypes = useMemo(
51
+ () => ({ ...defaultTypes, ...components.types }),
52
+ [components.types]
53
+ );
54
+ const mergedMarks = useMemo(
55
+ () => ({ ...defaultMarks, ...components.marks }),
56
+ [components.marks]
57
+ );
58
+ const renderNode = (node, index) => {
59
+ if (node.type === "text") {
60
+ let element = /* @__PURE__ */ jsx2(React.Fragment, { children: node.text }, index);
61
+ if (node.marks && node.marks.length > 0) {
62
+ const marks = [...node.marks].reverse();
63
+ marks.forEach((mark, markIndex) => {
64
+ const MarkComponent = mergedMarks[mark.type];
65
+ if (MarkComponent) {
66
+ element = /* @__PURE__ */ jsx2(MarkComponent, { mark, children: element }, `${index}-${markIndex}`);
67
+ }
68
+ });
69
+ }
70
+ return element;
71
+ }
72
+ if (node.type) {
73
+ const NodeComponent = mergedTypes[node.type];
74
+ let children = null;
75
+ if (node.content && node.content.length > 0) {
76
+ children = node.content.map((child, childIndex) => renderNode(child, childIndex));
77
+ }
78
+ if (NodeComponent) {
79
+ return /* @__PURE__ */ jsx2(NodeComponent, { node, children }, index);
80
+ }
81
+ return /* @__PURE__ */ jsx2(React.Fragment, { children }, index);
82
+ }
83
+ return null;
84
+ };
85
+ if (!content) return null;
86
+ if (Array.isArray(content)) {
87
+ return /* @__PURE__ */ jsx2(Fragment2, { children: content.map((node, index) => renderNode(node, index)) });
88
+ }
89
+ return /* @__PURE__ */ jsx2(Fragment2, { children: renderNode(content, 0) });
90
+ };
91
+ export {
92
+ KyroRichTextRenderer,
93
+ defaultMarks,
94
+ defaultTypes
95
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@kyro-cms/kyro-rich-text-react",
3
+ "version": "0.12.15",
4
+ "description": "React renderer for Kyro CMS Tiptap rich text content",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "dev": "tsup --watch",
21
+ "prepublishOnly": "pnpm build"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/danielDozie/kyro-cms"
26
+ },
27
+ "license": "MIT",
28
+ "peerDependencies": {
29
+ "react": "^18.0.0 || ^19.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "@testing-library/dom": "^10.4.1",
33
+ "@testing-library/react": "^16.3.2",
34
+ "@types/react": "^18.0.0",
35
+ "jsdom": "^29.1.1",
36
+ "react": "^18.0.0",
37
+ "react-dom": "18.3.1",
38
+ "tsup": "^8.0.0",
39
+ "typescript": "^5.0.0",
40
+ "vitest": "^4.1.9"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ }
45
+ }