@obelism/content-react 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Obelism
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,55 @@
1
- # Temporary Holding Version
1
+ # Obelism Content React SDK
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ```bash
4
+ npm i @obelism/content @obelism/content-react
5
+ ```
6
+
7
+ ## Live preview
8
+
9
+ ```tsx
10
+ // app/layout.tsx: starts the bridge, so blocks are selectable on every page
11
+ ;<ContentBridge />
12
+
13
+ // A client component rendering an entry
14
+ ;('use client')
15
+ import { editable, useLiveEntry } from '@obelism/content-react'
16
+
17
+ export const Page = ({ entry }) => {
18
+ const { fields } = useLiveEntry(entry) // the editor's unsaved version while editing
19
+ return fields.body.map((block) => (
20
+ <section key={block._uid} {...editable(block)}>
21
+ …
22
+ </section>
23
+ ))
24
+ }
25
+ ```
26
+
27
+ - `useLiveEntry(entry, { url? })`: `entry`, or the editor's live version of it.
28
+ - `useContentBridge()`: `{ isEditing, entryId, environment }`, after hydration.
29
+ - `useContentBridgeEvent('saved' | 'published' | 'update', listener)`, e.g.
30
+ `router.refresh()` on publish.
31
+ - `<ContentBridge url? prompt? />`: pass `url` only when not on content.obelism.studio.
32
+
33
+ ## Prompt editing
34
+
35
+ `<ContentBridge />` shows editors a prompt box inside the visual editor (see
36
+ `@obelism/content`). `<ContentBridge prompt={false} />` leaves it out;
37
+ `useContentPrompt()` gives `{ available, prompting, canUndo, selectedUid,
38
+ prompt, undo, select }` for your own UI.
39
+
40
+ ## Rich text
41
+
42
+ ```tsx
43
+ <RichText
44
+ document={entry.fields.body}
45
+ embeddedEntry={({ entry }) => <Card entry={entry} />}
46
+ nodes={{ heading: ({ node, children }) => <h2>{children}</h2> }}
47
+ marks={{
48
+ link: ({ mark, children }) => (
49
+ <Link href={mark.attrs.href}>{children}</Link>
50
+ ),
51
+ }}
52
+ />
53
+ ```
54
+
55
+ Works in server components too (it isn't a client component).
@@ -0,0 +1,131 @@
1
+ 'use client';
2
+ var react = require('react');
3
+ var bridge = require('@obelism/content/bridge');
4
+
5
+ const noop = ()=>{};
6
+ // The page's bridge, as a React external store: subscribing starts it (after
7
+ // mount, never during render), so it is null on the server, while hydrating,
8
+ // and until then.
9
+ const useBridge = (options)=>{
10
+ const startWith = react.useRef(options);
11
+ const { url } = options;
12
+ const subscribe = react.useCallback((onChange)=>{
13
+ bridge.getContentBridge({
14
+ ...startWith.current,
15
+ url
16
+ });
17
+ onChange();
18
+ return noop;
19
+ }, [
20
+ url
21
+ ]);
22
+ return react.useSyncExternalStore(subscribe, bridge.peekContentBridge, ()=>null);
23
+ };
24
+ // Starts the visual editor bridge. Put it in the root layout, so clicking a
25
+ // block selects it in the editor on every page, also those without
26
+ // `useLiveEntry`, and editors get the prompt box (`prompt={false}` leaves it
27
+ // out). Renders nothing, and does nothing outside the editor. Its options
28
+ // apply when it is the first to start the bridge, which in a root layout it
29
+ // is.
30
+ const ContentBridge = ({ url, prompt })=>{
31
+ useBridge({
32
+ url,
33
+ prompt
34
+ });
35
+ return null;
36
+ };
37
+ const NOT_EDITING = {
38
+ isEditing: false,
39
+ mode: null,
40
+ entryId: null,
41
+ environment: null
42
+ };
43
+ // Whether the page is open in the visual editor (or signed in to edit on the
44
+ // site), and on which entry. Always
45
+ // "not editing" during server rendering and hydration, so markup matches;
46
+ // the real state follows right after.
47
+ const useContentBridge = ({ url } = {})=>{
48
+ const bridge = useBridge({
49
+ url
50
+ });
51
+ if (!bridge?.isEditing) return NOT_EDITING;
52
+ const { isEditing, mode, entryId, environment } = bridge;
53
+ return {
54
+ isEditing,
55
+ mode,
56
+ entryId,
57
+ environment
58
+ };
59
+ };
60
+ // Calls `listener` on a bridge event, e.g. to refetch server data once the
61
+ // editor saved or published:
62
+ //
63
+ // const router = useRouter()
64
+ // useContentBridgeEvent('saved', () => router.refresh())
65
+ const useContentBridgeEvent = (event, listener, { url } = {})=>{
66
+ const bridge = useBridge({
67
+ url
68
+ });
69
+ const latest = react.useRef(listener);
70
+ react.useEffect(()=>{
71
+ latest.current = listener;
72
+ });
73
+ react.useEffect(()=>bridge?.on(event, (payload)=>latest.current(payload)), [
74
+ bridge,
75
+ event
76
+ ]);
77
+ };
78
+ function useLiveEntry(entry, { url } = {}) {
79
+ const bridge = useBridge({
80
+ url
81
+ });
82
+ const [live, setLive] = react.useState(null);
83
+ const id = entry?.id;
84
+ react.useEffect(()=>{
85
+ if (!id || !bridge) return;
86
+ return bridge.on('update', ({ entry: next })=>{
87
+ if (next.id === id) setLive(next);
88
+ });
89
+ }, [
90
+ bridge,
91
+ id
92
+ ]);
93
+ // A live version only stands in for the entry it was sent for.
94
+ if (live && live.id === id) return live;
95
+ return entry ?? null;
96
+ }
97
+ const notStarted = ()=>Promise.reject(new Error('Prompts only work inside the visual editor'));
98
+ const notOnSite = ()=>Promise.reject(new Error('Only while signed in to edit on the site'));
99
+ // For your own prompt UI instead of the built-in box (render
100
+ // `<ContentBridge prompt={false} />`):
101
+ //
102
+ // const { available, prompting, prompt } = useContentPrompt()
103
+ // if (!available) return null
104
+ // … <button onClick={() => prompt('Make it shorter')}>
105
+ const useContentPrompt = ({ url } = {})=>{
106
+ const bridge$1 = useBridge({
107
+ url
108
+ });
109
+ const subscribe = react.useCallback((onChange)=>bridge$1 ? bridge$1.on('change', onChange) : noop, [
110
+ bridge$1
111
+ ]);
112
+ const state = react.useSyncExternalStore(subscribe, ()=>bridge$1?.getState() ?? bridge.INITIAL_STATE, ()=>bridge.INITIAL_STATE);
113
+ return {
114
+ ...state,
115
+ available: state.features.includes('prompt'),
116
+ mode: bridge$1?.mode ?? null,
117
+ prompt: bridge$1 ? bridge$1.prompt : notStarted,
118
+ undo: bridge$1 ? bridge$1.undo : noop,
119
+ select: bridge$1 ? bridge$1.select : noop,
120
+ save: bridge$1 ? bridge$1.save : notOnSite,
121
+ publish: bridge$1 ? bridge$1.publish : notOnSite,
122
+ signOut: bridge$1 ? bridge$1.signOut : noop,
123
+ setSelecting: bridge$1 ? bridge$1.setSelecting : noop
124
+ };
125
+ };
126
+
127
+ exports.ContentBridge = ContentBridge;
128
+ exports.useContentBridge = useContentBridge;
129
+ exports.useContentBridgeEvent = useContentBridgeEvent;
130
+ exports.useContentPrompt = useContentPrompt;
131
+ exports.useLiveEntry = useLiveEntry;
@@ -0,0 +1,127 @@
1
+ 'use client';
2
+ import { useRef, useEffect, useState, useCallback, useSyncExternalStore } from 'react';
3
+ import { INITIAL_STATE, getContentBridge, peekContentBridge } from '@obelism/content/bridge';
4
+
5
+ const noop = ()=>{};
6
+ // The page's bridge, as a React external store: subscribing starts it (after
7
+ // mount, never during render), so it is null on the server, while hydrating,
8
+ // and until then.
9
+ const useBridge = (options)=>{
10
+ const startWith = useRef(options);
11
+ const { url } = options;
12
+ const subscribe = useCallback((onChange)=>{
13
+ getContentBridge({
14
+ ...startWith.current,
15
+ url
16
+ });
17
+ onChange();
18
+ return noop;
19
+ }, [
20
+ url
21
+ ]);
22
+ return useSyncExternalStore(subscribe, peekContentBridge, ()=>null);
23
+ };
24
+ // Starts the visual editor bridge. Put it in the root layout, so clicking a
25
+ // block selects it in the editor on every page, also those without
26
+ // `useLiveEntry`, and editors get the prompt box (`prompt={false}` leaves it
27
+ // out). Renders nothing, and does nothing outside the editor. Its options
28
+ // apply when it is the first to start the bridge, which in a root layout it
29
+ // is.
30
+ const ContentBridge = ({ url, prompt })=>{
31
+ useBridge({
32
+ url,
33
+ prompt
34
+ });
35
+ return null;
36
+ };
37
+ const NOT_EDITING = {
38
+ isEditing: false,
39
+ mode: null,
40
+ entryId: null,
41
+ environment: null
42
+ };
43
+ // Whether the page is open in the visual editor (or signed in to edit on the
44
+ // site), and on which entry. Always
45
+ // "not editing" during server rendering and hydration, so markup matches;
46
+ // the real state follows right after.
47
+ const useContentBridge = ({ url } = {})=>{
48
+ const bridge = useBridge({
49
+ url
50
+ });
51
+ if (!bridge?.isEditing) return NOT_EDITING;
52
+ const { isEditing, mode, entryId, environment } = bridge;
53
+ return {
54
+ isEditing,
55
+ mode,
56
+ entryId,
57
+ environment
58
+ };
59
+ };
60
+ // Calls `listener` on a bridge event, e.g. to refetch server data once the
61
+ // editor saved or published:
62
+ //
63
+ // const router = useRouter()
64
+ // useContentBridgeEvent('saved', () => router.refresh())
65
+ const useContentBridgeEvent = (event, listener, { url } = {})=>{
66
+ const bridge = useBridge({
67
+ url
68
+ });
69
+ const latest = useRef(listener);
70
+ useEffect(()=>{
71
+ latest.current = listener;
72
+ });
73
+ useEffect(()=>bridge?.on(event, (payload)=>latest.current(payload)), [
74
+ bridge,
75
+ event
76
+ ]);
77
+ };
78
+ function useLiveEntry(entry, { url } = {}) {
79
+ const bridge = useBridge({
80
+ url
81
+ });
82
+ const [live, setLive] = useState(null);
83
+ const id = entry?.id;
84
+ useEffect(()=>{
85
+ if (!id || !bridge) return;
86
+ return bridge.on('update', ({ entry: next })=>{
87
+ if (next.id === id) setLive(next);
88
+ });
89
+ }, [
90
+ bridge,
91
+ id
92
+ ]);
93
+ // A live version only stands in for the entry it was sent for.
94
+ if (live && live.id === id) return live;
95
+ return entry ?? null;
96
+ }
97
+ const notStarted = ()=>Promise.reject(new Error('Prompts only work inside the visual editor'));
98
+ const notOnSite = ()=>Promise.reject(new Error('Only while signed in to edit on the site'));
99
+ // For your own prompt UI instead of the built-in box (render
100
+ // `<ContentBridge prompt={false} />`):
101
+ //
102
+ // const { available, prompting, prompt } = useContentPrompt()
103
+ // if (!available) return null
104
+ // … <button onClick={() => prompt('Make it shorter')}>
105
+ const useContentPrompt = ({ url } = {})=>{
106
+ const bridge = useBridge({
107
+ url
108
+ });
109
+ const subscribe = useCallback((onChange)=>bridge ? bridge.on('change', onChange) : noop, [
110
+ bridge
111
+ ]);
112
+ const state = useSyncExternalStore(subscribe, ()=>bridge?.getState() ?? INITIAL_STATE, ()=>INITIAL_STATE);
113
+ return {
114
+ ...state,
115
+ available: state.features.includes('prompt'),
116
+ mode: bridge?.mode ?? null,
117
+ prompt: bridge ? bridge.prompt : notStarted,
118
+ undo: bridge ? bridge.undo : noop,
119
+ select: bridge ? bridge.select : noop,
120
+ save: bridge ? bridge.save : notOnSite,
121
+ publish: bridge ? bridge.publish : notOnSite,
122
+ signOut: bridge ? bridge.signOut : noop,
123
+ setSelecting: bridge ? bridge.setSelecting : noop
124
+ };
125
+ };
126
+
127
+ export { ContentBridge as C, useContentBridgeEvent as a, useContentPrompt as b, useLiveEntry as c, useContentBridge as u };
@@ -0,0 +1,57 @@
1
+ import { BridgeOptions, ContentBridge as ContentBridge$1, BridgeState, PromptResult, BridgeEvent, BridgeListener } from '@obelism/content/bridge';
2
+ export { BridgeEvent, BridgeEvents, BridgeOptions, BridgeState, PromptBoxOptions, PromptLabels, PromptResult } from '@obelism/content/bridge';
3
+ import { Entry, RichTextNode, RichTextMark, EntryStub, Asset } from '@obelism/content';
4
+ export { Asset, Block, Entry, EntryStub, RichTextDocument, RichTextNode, editable } from '@obelism/content';
5
+ import * as react from 'react';
6
+ import { ReactNode } from 'react';
7
+
8
+ type ContentBridgeProps = BridgeOptions;
9
+ declare const ContentBridge: ({ url, prompt }: ContentBridgeProps) => null;
10
+ type ContentBridgeState = Pick<ContentBridge$1, 'isEditing' | 'mode' | 'entryId' | 'environment'>;
11
+ declare const useContentBridge: ({ url, }?: BridgeOptions) => ContentBridgeState;
12
+ declare const useContentBridgeEvent: <E extends BridgeEvent>(event: E, listener: BridgeListener<E>, { url }?: BridgeOptions) => void;
13
+ declare function useLiveEntry<TEntry extends Entry>(entry: TEntry, options?: BridgeOptions): TEntry;
14
+ declare function useLiveEntry<TEntry extends Entry>(entry: TEntry | null | undefined, options?: BridgeOptions): TEntry | null;
15
+ type ContentPromptState = BridgeState & {
16
+ available: boolean;
17
+ mode: ContentBridge$1['mode'];
18
+ prompt: (prompt: string, options?: {
19
+ uid?: string | null;
20
+ }) => Promise<PromptResult>;
21
+ undo: () => void;
22
+ select: (uid: string | null) => void;
23
+ save: () => Promise<void>;
24
+ publish: () => Promise<void>;
25
+ signOut: () => void;
26
+ setSelecting: (selecting: boolean) => void;
27
+ };
28
+ declare const useContentPrompt: ({ url, }?: Pick<BridgeOptions, "url">) => ContentPromptState;
29
+
30
+ type RichTextNodeProps = {
31
+ node: RichTextNode;
32
+ children?: ReactNode;
33
+ };
34
+ type RichTextMarkProps = {
35
+ mark: RichTextMark;
36
+ children?: ReactNode;
37
+ };
38
+ type RichTextComponents = {
39
+ nodes?: Partial<Record<string, (props: RichTextNodeProps) => ReactNode>>;
40
+ marks?: Partial<Record<string, (props: RichTextMarkProps) => ReactNode>>;
41
+ embeddedEntry?: (props: {
42
+ entry: Entry | EntryStub;
43
+ node: RichTextNode;
44
+ }) => ReactNode;
45
+ embeddedAsset?: (props: {
46
+ asset: Asset;
47
+ node: RichTextNode;
48
+ }) => ReactNode;
49
+ };
50
+ type RichTextProps = RichTextComponents & {
51
+ document: RichTextNode | null | undefined;
52
+ };
53
+ declare const RichText: ({ document, nodes, marks, embeddedEntry, embeddedAsset: EmbeddedAsset, }: RichTextProps) => react.JSX.Element | null;
54
+
55
+ export { ContentBridge, RichText, useContentBridge, useContentBridgeEvent, useContentPrompt, useLiveEntry };
56
+ export type { ContentBridgeProps, ContentBridgeState, ContentPromptState, RichTextComponents, RichTextMarkProps, RichTextNodeProps, RichTextProps };
57
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","sources":["../src/bridge.ts","../src/RichText.tsx"],"mappings":";;;;;;;AAEA,KAAK,kBAAkB,GAAG,aAAa;AACvC,cAAc,aAAa,oBAAoB,kBAAkB;AACjE,KAAK,kBAAkB,GAAG,IAAI,CAACA,eAAM;AACrC,cAAc,gBAAgB,cAAc,aAAa,KAAK,kBAAkB;AAChF,cAAc,qBAAqB,aAAa,WAAW,sBAAsB,cAAc,eAAe,aAAa;AAC3H,iBAAiB,YAAY,gBAAgB,KAAK,2BAA2B,aAAa;AAC1F,iBAAiB,YAAY,gBAAgB,KAAK,8CAA8C,aAAa;AAC7G,KAAK,kBAAkB,GAAG,WAAW;AACrC;AACA,UAAUA,eAAM;AAChB;AACA;AACA,UAAU,OAAO,CAAC,YAAY;AAC9B;AACA;AACA,gBAAgB,OAAO;AACvB,mBAAmB,OAAO;AAC1B;AACA;AACA;AACA,cAAc,gBAAgB,cAAc,IAAI,CAAC,aAAa,aAAa,kBAAkB;;ACnB7F,KAAK,iBAAiB;AACtB,UAAU,YAAY;AACtB,eAAe,SAAS;AACxB;AACA,KAAK,iBAAiB;AACtB,UAAU,YAAY;AACtB,eAAe,SAAS;AACxB;AACA,KAAK,kBAAkB;AACvB,YAAY,OAAO,CAAC,MAAM,iBAAiB,iBAAiB,KAAK,SAAS;AAC1E,YAAY,OAAO,CAAC,MAAM,iBAAiB,iBAAiB,KAAK,SAAS;AAC1E;AACA,eAAe,KAAK,GAAG,SAAS;AAChC,cAAc,YAAY;AAC1B,UAAU,SAAS;AACnB;AACA,eAAe,KAAK;AACpB,cAAc,YAAY;AAC1B,UAAU,SAAS;AACnB;AACA,KAAK,aAAa,GAAG,kBAAkB;AACvC,cAAc,YAAY;AAC1B;AACA,cAAc,QAAQ,6EAA6E,aAAa,KAAK,KAAK,CAAC,GAAG,CAAC,OAAO;;","names":["Bridge"]}
@@ -0,0 +1,57 @@
1
+ import { BridgeOptions, ContentBridge as ContentBridge$1, BridgeState, PromptResult, BridgeEvent, BridgeListener } from '@obelism/content/bridge';
2
+ export { BridgeEvent, BridgeEvents, BridgeOptions, BridgeState, PromptBoxOptions, PromptLabels, PromptResult } from '@obelism/content/bridge';
3
+ import { Entry, RichTextNode, RichTextMark, EntryStub, Asset } from '@obelism/content';
4
+ export { Asset, Block, Entry, EntryStub, RichTextDocument, RichTextNode, editable } from '@obelism/content';
5
+ import * as react from 'react';
6
+ import { ReactNode } from 'react';
7
+
8
+ type ContentBridgeProps = BridgeOptions;
9
+ declare const ContentBridge: ({ url, prompt }: ContentBridgeProps) => null;
10
+ type ContentBridgeState = Pick<ContentBridge$1, 'isEditing' | 'mode' | 'entryId' | 'environment'>;
11
+ declare const useContentBridge: ({ url, }?: BridgeOptions) => ContentBridgeState;
12
+ declare const useContentBridgeEvent: <E extends BridgeEvent>(event: E, listener: BridgeListener<E>, { url }?: BridgeOptions) => void;
13
+ declare function useLiveEntry<TEntry extends Entry>(entry: TEntry, options?: BridgeOptions): TEntry;
14
+ declare function useLiveEntry<TEntry extends Entry>(entry: TEntry | null | undefined, options?: BridgeOptions): TEntry | null;
15
+ type ContentPromptState = BridgeState & {
16
+ available: boolean;
17
+ mode: ContentBridge$1['mode'];
18
+ prompt: (prompt: string, options?: {
19
+ uid?: string | null;
20
+ }) => Promise<PromptResult>;
21
+ undo: () => void;
22
+ select: (uid: string | null) => void;
23
+ save: () => Promise<void>;
24
+ publish: () => Promise<void>;
25
+ signOut: () => void;
26
+ setSelecting: (selecting: boolean) => void;
27
+ };
28
+ declare const useContentPrompt: ({ url, }?: Pick<BridgeOptions, "url">) => ContentPromptState;
29
+
30
+ type RichTextNodeProps = {
31
+ node: RichTextNode;
32
+ children?: ReactNode;
33
+ };
34
+ type RichTextMarkProps = {
35
+ mark: RichTextMark;
36
+ children?: ReactNode;
37
+ };
38
+ type RichTextComponents = {
39
+ nodes?: Partial<Record<string, (props: RichTextNodeProps) => ReactNode>>;
40
+ marks?: Partial<Record<string, (props: RichTextMarkProps) => ReactNode>>;
41
+ embeddedEntry?: (props: {
42
+ entry: Entry | EntryStub;
43
+ node: RichTextNode;
44
+ }) => ReactNode;
45
+ embeddedAsset?: (props: {
46
+ asset: Asset;
47
+ node: RichTextNode;
48
+ }) => ReactNode;
49
+ };
50
+ type RichTextProps = RichTextComponents & {
51
+ document: RichTextNode | null | undefined;
52
+ };
53
+ declare const RichText: ({ document, nodes, marks, embeddedEntry, embeddedAsset: EmbeddedAsset, }: RichTextProps) => react.JSX.Element | null;
54
+
55
+ export { ContentBridge, RichText, useContentBridge, useContentBridgeEvent, useContentPrompt, useLiveEntry };
56
+ export type { ContentBridgeProps, ContentBridgeState, ContentPromptState, RichTextComponents, RichTextMarkProps, RichTextNodeProps, RichTextProps };
57
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sources":["../src/bridge.ts","../src/RichText.tsx"],"mappings":";;;;;;;AAEA,KAAK,kBAAkB,GAAG,aAAa;AACvC,cAAc,aAAa,oBAAoB,kBAAkB;AACjE,KAAK,kBAAkB,GAAG,IAAI,CAACA,eAAM;AACrC,cAAc,gBAAgB,cAAc,aAAa,KAAK,kBAAkB;AAChF,cAAc,qBAAqB,aAAa,WAAW,sBAAsB,cAAc,eAAe,aAAa;AAC3H,iBAAiB,YAAY,gBAAgB,KAAK,2BAA2B,aAAa;AAC1F,iBAAiB,YAAY,gBAAgB,KAAK,8CAA8C,aAAa;AAC7G,KAAK,kBAAkB,GAAG,WAAW;AACrC;AACA,UAAUA,eAAM;AAChB;AACA;AACA,UAAU,OAAO,CAAC,YAAY;AAC9B;AACA;AACA,gBAAgB,OAAO;AACvB,mBAAmB,OAAO;AAC1B;AACA;AACA;AACA,cAAc,gBAAgB,cAAc,IAAI,CAAC,aAAa,aAAa,kBAAkB;;ACnB7F,KAAK,iBAAiB;AACtB,UAAU,YAAY;AACtB,eAAe,SAAS;AACxB;AACA,KAAK,iBAAiB;AACtB,UAAU,YAAY;AACtB,eAAe,SAAS;AACxB;AACA,KAAK,kBAAkB;AACvB,YAAY,OAAO,CAAC,MAAM,iBAAiB,iBAAiB,KAAK,SAAS;AAC1E,YAAY,OAAO,CAAC,MAAM,iBAAiB,iBAAiB,KAAK,SAAS;AAC1E;AACA,eAAe,KAAK,GAAG,SAAS;AAChC,cAAc,YAAY;AAC1B,UAAU,SAAS;AACnB;AACA,eAAe,KAAK;AACpB,cAAc,YAAY;AAC1B,UAAU,SAAS;AACnB;AACA,KAAK,aAAa,GAAG,kBAAkB;AACvC,cAAc,YAAY;AAC1B;AACA,cAAc,QAAQ,6EAA6E,aAAa,KAAK,KAAK,CAAC,GAAG,CAAC,OAAO;;","names":["Bridge"]}
package/dist/index.js ADDED
@@ -0,0 +1,181 @@
1
+ Object.defineProperty(exports, '__esModule', { value: true });
2
+
3
+ var bunchee_group_0 = require('./bridge-ca5685t4.js');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+ var react = require('react');
6
+ var content = require('@obelism/content');
7
+
8
+ const DefaultAsset = ({ asset })=>{
9
+ if (asset.mimeType.startsWith('image/')) {
10
+ return /*#__PURE__*/ jsxRuntime.jsx("figure", {
11
+ children: /*#__PURE__*/ jsxRuntime.jsx("img", {
12
+ src: asset.url,
13
+ alt: typeof asset.alt === 'string' ? asset.alt : '',
14
+ width: asset.width ?? undefined,
15
+ height: asset.height ?? undefined
16
+ })
17
+ });
18
+ }
19
+ return /*#__PURE__*/ jsxRuntime.jsx("p", {
20
+ children: /*#__PURE__*/ jsxRuntime.jsx("a", {
21
+ href: asset.url,
22
+ children: asset.filename
23
+ })
24
+ });
25
+ };
26
+ const renderMark = (mark, children, marks)=>{
27
+ const Custom = marks?.[mark.type];
28
+ if (Custom) return /*#__PURE__*/ jsxRuntime.jsx(Custom, {
29
+ mark: mark,
30
+ children: children
31
+ });
32
+ switch(mark.type){
33
+ case 'bold':
34
+ return /*#__PURE__*/ jsxRuntime.jsx("strong", {
35
+ children: children
36
+ });
37
+ case 'italic':
38
+ return /*#__PURE__*/ jsxRuntime.jsx("em", {
39
+ children: children
40
+ });
41
+ case 'underline':
42
+ return /*#__PURE__*/ jsxRuntime.jsx("u", {
43
+ children: children
44
+ });
45
+ case 'strike':
46
+ return /*#__PURE__*/ jsxRuntime.jsx("s", {
47
+ children: children
48
+ });
49
+ case 'code':
50
+ return /*#__PURE__*/ jsxRuntime.jsx("code", {
51
+ children: children
52
+ });
53
+ case 'link':
54
+ {
55
+ const href = String(mark.attrs?.href ?? '');
56
+ if (!content.SAFE_HREF.test(href)) return children;
57
+ const blank = mark.attrs?.target === '_blank';
58
+ return /*#__PURE__*/ jsxRuntime.jsx("a", {
59
+ href: href,
60
+ title: mark.attrs?.title ? String(mark.attrs.title) : undefined,
61
+ target: blank ? '_blank' : undefined,
62
+ rel: blank ? 'noopener noreferrer' : undefined,
63
+ children: children
64
+ });
65
+ }
66
+ default:
67
+ return children;
68
+ }
69
+ };
70
+ // Renders a rich text document as React elements: no HTML strings, links
71
+ // limited to safe schemes, and every node, mark and embed overridable.
72
+ //
73
+ // <RichText
74
+ // document={entry.fields.body}
75
+ // embeddedEntry={({ entry }) => <Card entry={entry} />}
76
+ // />
77
+ const RichText = ({ document, nodes, marks, embeddedEntry, embeddedAsset: EmbeddedAsset = DefaultAsset })=>{
78
+ if (!document) return null;
79
+ const render = (node, key)=>{
80
+ if (node.type === 'text') {
81
+ const text = node.text ?? '';
82
+ return /*#__PURE__*/ jsxRuntime.jsx(react.Fragment, {
83
+ children: (node.marks ?? []).reduceRight((inner, mark)=>renderMark(mark, inner, marks), text)
84
+ }, key);
85
+ }
86
+ const children = node.content && node.content.length > 0 ? node.content.map((child, index)=>render(child, index)) : undefined;
87
+ const Custom = nodes?.[node.type];
88
+ if (Custom) {
89
+ return /*#__PURE__*/ jsxRuntime.jsx(Custom, {
90
+ node: node,
91
+ children: children
92
+ }, key);
93
+ }
94
+ switch(node.type){
95
+ case 'doc':
96
+ return /*#__PURE__*/ jsxRuntime.jsx(react.Fragment, {
97
+ children: children
98
+ }, key);
99
+ case 'paragraph':
100
+ return /*#__PURE__*/ jsxRuntime.jsx("p", {
101
+ children: children
102
+ }, key);
103
+ case 'heading':
104
+ {
105
+ const Heading = `h${content.headingLevel(node)}`;
106
+ return /*#__PURE__*/ jsxRuntime.jsx(Heading, {
107
+ children: children
108
+ }, key);
109
+ }
110
+ case 'blockquote':
111
+ return /*#__PURE__*/ jsxRuntime.jsx("blockquote", {
112
+ children: children
113
+ }, key);
114
+ case 'bulletList':
115
+ return /*#__PURE__*/ jsxRuntime.jsx("ul", {
116
+ children: children
117
+ }, key);
118
+ case 'orderedList':
119
+ {
120
+ const start = Number(node.attrs?.start ?? 1);
121
+ return /*#__PURE__*/ jsxRuntime.jsx("ol", {
122
+ start: start !== 1 ? start : undefined,
123
+ children: children
124
+ }, key);
125
+ }
126
+ case 'listItem':
127
+ return /*#__PURE__*/ jsxRuntime.jsx("li", {
128
+ children: children
129
+ }, key);
130
+ case 'codeBlock':
131
+ {
132
+ const language = node.attrs?.language;
133
+ return /*#__PURE__*/ jsxRuntime.jsx("pre", {
134
+ children: /*#__PURE__*/ jsxRuntime.jsx("code", {
135
+ className: language ? `language-${String(language)}` : undefined,
136
+ children: children
137
+ })
138
+ }, key);
139
+ }
140
+ case 'hardBreak':
141
+ return /*#__PURE__*/ jsxRuntime.jsx("br", {}, key);
142
+ case 'horizontalRule':
143
+ return /*#__PURE__*/ jsxRuntime.jsx("hr", {}, key);
144
+ case 'embeddedEntry':
145
+ {
146
+ const entry = node.attrs?.entry;
147
+ if (!entry || !embeddedEntry) return null;
148
+ const EmbeddedEntry = embeddedEntry;
149
+ return /*#__PURE__*/ jsxRuntime.jsx(EmbeddedEntry, {
150
+ entry: entry,
151
+ node: node
152
+ }, key);
153
+ }
154
+ case 'embeddedAsset':
155
+ {
156
+ const asset = node.attrs?.asset;
157
+ if (!asset) return null;
158
+ return /*#__PURE__*/ jsxRuntime.jsx(EmbeddedAsset, {
159
+ asset: asset,
160
+ node: node
161
+ }, key);
162
+ }
163
+ default:
164
+ return null;
165
+ }
166
+ };
167
+ return /*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {
168
+ children: render(document, 'doc')
169
+ });
170
+ };
171
+
172
+ exports.ContentBridge = bunchee_group_0.ContentBridge;
173
+ exports.useContentBridge = bunchee_group_0.useContentBridge;
174
+ exports.useContentBridgeEvent = bunchee_group_0.useContentBridgeEvent;
175
+ exports.useContentPrompt = bunchee_group_0.useContentPrompt;
176
+ exports.useLiveEntry = bunchee_group_0.useLiveEntry;
177
+ Object.defineProperty(exports, "editable", {
178
+ enumerable: true,
179
+ get: function () { return content.editable; }
180
+ });
181
+ exports.RichText = RichText;
package/dist/index.mjs ADDED
@@ -0,0 +1,171 @@
1
+ export { C as ContentBridge, u as useContentBridge, a as useContentBridgeEvent, b as useContentPrompt, c as useLiveEntry } from './bridge-opx0v5j0.mjs';
2
+ import { jsx, Fragment } from 'react/jsx-runtime';
3
+ import { Fragment as Fragment$1 } from 'react';
4
+ import { headingLevel, SAFE_HREF } from '@obelism/content';
5
+ export { editable } from '@obelism/content';
6
+
7
+ const DefaultAsset = ({ asset })=>{
8
+ if (asset.mimeType.startsWith('image/')) {
9
+ return /*#__PURE__*/ jsx("figure", {
10
+ children: /*#__PURE__*/ jsx("img", {
11
+ src: asset.url,
12
+ alt: typeof asset.alt === 'string' ? asset.alt : '',
13
+ width: asset.width ?? undefined,
14
+ height: asset.height ?? undefined
15
+ })
16
+ });
17
+ }
18
+ return /*#__PURE__*/ jsx("p", {
19
+ children: /*#__PURE__*/ jsx("a", {
20
+ href: asset.url,
21
+ children: asset.filename
22
+ })
23
+ });
24
+ };
25
+ const renderMark = (mark, children, marks)=>{
26
+ const Custom = marks?.[mark.type];
27
+ if (Custom) return /*#__PURE__*/ jsx(Custom, {
28
+ mark: mark,
29
+ children: children
30
+ });
31
+ switch(mark.type){
32
+ case 'bold':
33
+ return /*#__PURE__*/ jsx("strong", {
34
+ children: children
35
+ });
36
+ case 'italic':
37
+ return /*#__PURE__*/ jsx("em", {
38
+ children: children
39
+ });
40
+ case 'underline':
41
+ return /*#__PURE__*/ jsx("u", {
42
+ children: children
43
+ });
44
+ case 'strike':
45
+ return /*#__PURE__*/ jsx("s", {
46
+ children: children
47
+ });
48
+ case 'code':
49
+ return /*#__PURE__*/ jsx("code", {
50
+ children: children
51
+ });
52
+ case 'link':
53
+ {
54
+ const href = String(mark.attrs?.href ?? '');
55
+ if (!SAFE_HREF.test(href)) return children;
56
+ const blank = mark.attrs?.target === '_blank';
57
+ return /*#__PURE__*/ jsx("a", {
58
+ href: href,
59
+ title: mark.attrs?.title ? String(mark.attrs.title) : undefined,
60
+ target: blank ? '_blank' : undefined,
61
+ rel: blank ? 'noopener noreferrer' : undefined,
62
+ children: children
63
+ });
64
+ }
65
+ default:
66
+ return children;
67
+ }
68
+ };
69
+ // Renders a rich text document as React elements: no HTML strings, links
70
+ // limited to safe schemes, and every node, mark and embed overridable.
71
+ //
72
+ // <RichText
73
+ // document={entry.fields.body}
74
+ // embeddedEntry={({ entry }) => <Card entry={entry} />}
75
+ // />
76
+ const RichText = ({ document, nodes, marks, embeddedEntry, embeddedAsset: EmbeddedAsset = DefaultAsset })=>{
77
+ if (!document) return null;
78
+ const render = (node, key)=>{
79
+ if (node.type === 'text') {
80
+ const text = node.text ?? '';
81
+ return /*#__PURE__*/ jsx(Fragment$1, {
82
+ children: (node.marks ?? []).reduceRight((inner, mark)=>renderMark(mark, inner, marks), text)
83
+ }, key);
84
+ }
85
+ const children = node.content && node.content.length > 0 ? node.content.map((child, index)=>render(child, index)) : undefined;
86
+ const Custom = nodes?.[node.type];
87
+ if (Custom) {
88
+ return /*#__PURE__*/ jsx(Custom, {
89
+ node: node,
90
+ children: children
91
+ }, key);
92
+ }
93
+ switch(node.type){
94
+ case 'doc':
95
+ return /*#__PURE__*/ jsx(Fragment$1, {
96
+ children: children
97
+ }, key);
98
+ case 'paragraph':
99
+ return /*#__PURE__*/ jsx("p", {
100
+ children: children
101
+ }, key);
102
+ case 'heading':
103
+ {
104
+ const Heading = `h${headingLevel(node)}`;
105
+ return /*#__PURE__*/ jsx(Heading, {
106
+ children: children
107
+ }, key);
108
+ }
109
+ case 'blockquote':
110
+ return /*#__PURE__*/ jsx("blockquote", {
111
+ children: children
112
+ }, key);
113
+ case 'bulletList':
114
+ return /*#__PURE__*/ jsx("ul", {
115
+ children: children
116
+ }, key);
117
+ case 'orderedList':
118
+ {
119
+ const start = Number(node.attrs?.start ?? 1);
120
+ return /*#__PURE__*/ jsx("ol", {
121
+ start: start !== 1 ? start : undefined,
122
+ children: children
123
+ }, key);
124
+ }
125
+ case 'listItem':
126
+ return /*#__PURE__*/ jsx("li", {
127
+ children: children
128
+ }, key);
129
+ case 'codeBlock':
130
+ {
131
+ const language = node.attrs?.language;
132
+ return /*#__PURE__*/ jsx("pre", {
133
+ children: /*#__PURE__*/ jsx("code", {
134
+ className: language ? `language-${String(language)}` : undefined,
135
+ children: children
136
+ })
137
+ }, key);
138
+ }
139
+ case 'hardBreak':
140
+ return /*#__PURE__*/ jsx("br", {}, key);
141
+ case 'horizontalRule':
142
+ return /*#__PURE__*/ jsx("hr", {}, key);
143
+ case 'embeddedEntry':
144
+ {
145
+ const entry = node.attrs?.entry;
146
+ if (!entry || !embeddedEntry) return null;
147
+ const EmbeddedEntry = embeddedEntry;
148
+ return /*#__PURE__*/ jsx(EmbeddedEntry, {
149
+ entry: entry,
150
+ node: node
151
+ }, key);
152
+ }
153
+ case 'embeddedAsset':
154
+ {
155
+ const asset = node.attrs?.asset;
156
+ if (!asset) return null;
157
+ return /*#__PURE__*/ jsx(EmbeddedAsset, {
158
+ asset: asset,
159
+ node: node
160
+ }, key);
161
+ }
162
+ default:
163
+ return null;
164
+ }
165
+ };
166
+ return /*#__PURE__*/ jsx(Fragment, {
167
+ children: render(document, 'doc')
168
+ });
169
+ };
170
+
171
+ export { RichText };
package/package.json CHANGED
@@ -1,6 +1,65 @@
1
1
  {
2
- "name": "@obelism/content-react",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@obelism/content-react",
3
+ "description": "Obelism Content React SDK: live preview in the visual editor and rich text",
4
+ "version": "0.1.0",
5
+ "keywords": [
6
+ "cms",
7
+ "headless-cms",
8
+ "visual-editor",
9
+ "typescript",
10
+ "obelism",
11
+ "react"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "Obelism <hello@obelism.studio> (https://www.obelism.studio)",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/Obelism/content-sdk"
18
+ },
19
+ "sideEffects": false,
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "exports": {
24
+ "import": {
25
+ "types": "./dist/index.d.mts",
26
+ "default": "./dist/index.mjs"
27
+ },
28
+ "require": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ }
32
+ },
33
+ "scripts": {
34
+ "build": "bunchee",
35
+ "dev": "bunchee --watch --no-clean",
36
+ "test": "vitest",
37
+ "test:coverage": "vitest run --coverage",
38
+ "test:ci": "vitest run",
39
+ "lint": "npm-run-all --parallel lint:*",
40
+ "lint:oxlint": "oxlint",
41
+ "lint:types": "tsc --noEmit",
42
+ "clean": "rm -rf ./node_modules && rm -rf ./dist && rm -rf ./.turbo"
43
+ },
44
+ "peerDependencies": {
45
+ "react": ">=18.0.0"
46
+ },
47
+ "dependencies": {
48
+ "@obelism/content": "^0.1.0"
49
+ },
50
+ "devDependencies": {
51
+ "@obelism/content-typescript-config": "^0.0.0",
52
+ "@testing-library/react": "^16.3.3",
53
+ "@types/react": "^19.3.0",
54
+ "@types/react-dom": "^19.2.4",
55
+ "@typescript/typescript6": "^6.0.2",
56
+ "bunchee": "^7.0.2",
57
+ "jsdom": "^30.1.2",
58
+ "npm-run-all2": "^9.0.2",
59
+ "oxlint": "^1.88.0",
60
+ "react": "^19.3.1",
61
+ "react-dom": "^19.3.1",
62
+ "typescript": "^7.0.2",
63
+ "vitest": "^5.0.4"
64
+ }
65
+ }