@artkit-ai/react 0.0.0-stage → 0.2.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 +202 -0
- package/README.md +145 -2
- package/dist/cache.d.ts +31 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +95 -0
- package/dist/cache.js.map +1 -0
- package/dist/client.d.ts +68 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +233 -0
- package/dist/client.js.map +1 -0
- package/dist/components/card.d.ts +22 -0
- package/dist/components/card.d.ts.map +1 -0
- package/dist/components/card.js +16 -0
- package/dist/components/card.js.map +1 -0
- package/dist/components/editor.d.ts +41 -0
- package/dist/components/editor.d.ts.map +1 -0
- package/dist/components/editor.js +111 -0
- package/dist/components/editor.js.map +1 -0
- package/dist/components/icons.d.ts +8 -0
- package/dist/components/icons.d.ts.map +1 -0
- package/dist/components/icons.js +14 -0
- package/dist/components/icons.js.map +1 -0
- package/dist/components/panel.d.ts +31 -0
- package/dist/components/panel.d.ts.map +1 -0
- package/dist/components/panel.js +132 -0
- package/dist/components/panel.js.map +1 -0
- package/dist/components/shared.d.ts +67 -0
- package/dist/components/shared.d.ts.map +1 -0
- package/dist/components/shared.js +100 -0
- package/dist/components/shared.js.map +1 -0
- package/dist/components/view.d.ts +39 -0
- package/dist/components/view.d.ts.map +1 -0
- package/dist/components/view.js +76 -0
- package/dist/components/view.js.map +1 -0
- package/dist/context.d.ts +64 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +13 -0
- package/dist/context.js.map +1 -0
- package/dist/controls.d.ts +145 -0
- package/dist/controls.d.ts.map +1 -0
- package/dist/controls.js +188 -0
- package/dist/controls.js.map +1 -0
- package/dist/download.d.ts +4 -0
- package/dist/download.d.ts.map +1 -0
- package/dist/download.js +20 -0
- package/dist/download.js.map +1 -0
- package/dist/editor-session.d.ts +96 -0
- package/dist/editor-session.d.ts.map +1 -0
- package/dist/editor-session.js +192 -0
- package/dist/editor-session.js.map +1 -0
- package/dist/hooks.d.ts +64 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +205 -0
- package/dist/hooks.js.map +1 -0
- package/dist/i18n.d.ts +135 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +145 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/kit-css.d.ts +3 -0
- package/dist/kit-css.d.ts.map +1 -0
- package/dist/kit-css.js +5 -0
- package/dist/kit-css.js.map +1 -0
- package/dist/partial-json.d.ts +8 -0
- package/dist/partial-json.d.ts.map +1 -0
- package/dist/partial-json.js +176 -0
- package/dist/partial-json.js.map +1 -0
- package/dist/provider.d.ts +42 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +95 -0
- package/dist/provider.js.map +1 -0
- package/dist/styles.d.ts +25 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +203 -0
- package/dist/styles.js.map +1 -0
- package/dist/toolbar.d.ts +90 -0
- package/dist/toolbar.d.ts.map +1 -0
- package/dist/toolbar.js +233 -0
- package/dist/toolbar.js.map +1 -0
- package/package.json +50 -3
- package/src/cache.ts +109 -0
- package/src/client.ts +342 -0
- package/src/components/card.tsx +86 -0
- package/src/components/editor.tsx +292 -0
- package/src/components/icons.tsx +14 -0
- package/src/components/panel.tsx +334 -0
- package/src/components/shared.tsx +198 -0
- package/src/components/view.tsx +196 -0
- package/src/context.ts +87 -0
- package/src/controls.tsx +587 -0
- package/src/download.ts +23 -0
- package/src/editor-session.ts +260 -0
- package/src/hooks.ts +296 -0
- package/src/i18n.ts +155 -0
- package/src/index.ts +128 -0
- package/src/kit-css.ts +5 -0
- package/src/partial-json.ts +160 -0
- package/src/provider.tsx +176 -0
- package/src/styles.ts +222 -0
- package/src/toolbar.tsx +536 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { ChangeSummary } from '@artkit-ai/core';
|
|
2
|
+
import { isArtifactError } from '@artkit-ai/core';
|
|
3
|
+
import { RotateCw } from 'lucide-react';
|
|
4
|
+
import {
|
|
5
|
+
Component,
|
|
6
|
+
type CSSProperties,
|
|
7
|
+
type ErrorInfo,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
useCallback,
|
|
10
|
+
useEffect,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
} from 'react';
|
|
14
|
+
import { type ArtkitContextValue, useArtkit } from '../context.js';
|
|
15
|
+
import type { UiMessageKey } from '../i18n.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Customization without forking (spec §13.5): each named part of a component accepts extra classes and
|
|
19
|
+
* inline styles. Base classes (`ak-*`) stay, so overrides compose with the default CSS.
|
|
20
|
+
*/
|
|
21
|
+
export type Slots<Part extends string> = Partial<Record<Part, { className?: string; style?: CSSProperties }>>;
|
|
22
|
+
|
|
23
|
+
export function cx(...classes: (string | false | null | undefined)[]): string {
|
|
24
|
+
return classes.filter(Boolean).join(' ');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function slot<Part extends string>(
|
|
28
|
+
slots: Slots<Part> | undefined,
|
|
29
|
+
part: Part,
|
|
30
|
+
base: string,
|
|
31
|
+
): { className: string; style?: CSSProperties } {
|
|
32
|
+
const custom = slots?.[part];
|
|
33
|
+
return { className: cx(base, custom?.className), ...(custom?.style ? { style: custom.style } : {}) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Attributes of every artkit root element: theme scope and forced light/dark. */
|
|
37
|
+
export function rootAttributes(ctx: ArtkitContextValue, themeId?: string): Record<string, string> {
|
|
38
|
+
return {
|
|
39
|
+
...(themeId ? { 'data-ak-theme': themeId } : {}),
|
|
40
|
+
...(ctx.colorScheme !== 'system' ? { 'data-theme': ctx.colorScheme } : {}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function errorMessage(error: unknown, fallback: string): string {
|
|
45
|
+
if (isArtifactError(error)) {
|
|
46
|
+
const hint = error.issues.find((issue) => issue.hint)?.hint;
|
|
47
|
+
return hint ? `${error.message} ${hint}` : error.message;
|
|
48
|
+
}
|
|
49
|
+
return fallback;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function ErrorMessage({
|
|
53
|
+
error,
|
|
54
|
+
onRetry,
|
|
55
|
+
onDismiss,
|
|
56
|
+
className,
|
|
57
|
+
style,
|
|
58
|
+
}: {
|
|
59
|
+
error: unknown;
|
|
60
|
+
onRetry?: () => void;
|
|
61
|
+
onDismiss?: () => void;
|
|
62
|
+
className?: string;
|
|
63
|
+
style?: CSSProperties;
|
|
64
|
+
}) {
|
|
65
|
+
const { t } = useArtkit();
|
|
66
|
+
return (
|
|
67
|
+
<div role="alert" className={cx('ak-alert', className)} style={style}>
|
|
68
|
+
<span>{errorMessage(error, t('loadFailed'))}</span>{' '}
|
|
69
|
+
{onRetry ? (
|
|
70
|
+
<button type="button" className="ak-button" onClick={onRetry}>
|
|
71
|
+
<RotateCw aria-hidden="true" />
|
|
72
|
+
{t('retry')}
|
|
73
|
+
</button>
|
|
74
|
+
) : null}
|
|
75
|
+
{onDismiss ? (
|
|
76
|
+
<button type="button" className="ak-button" onClick={onDismiss}>
|
|
77
|
+
{t('dismiss')}
|
|
78
|
+
</button>
|
|
79
|
+
) : null}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function Loading({
|
|
85
|
+
label,
|
|
86
|
+
className,
|
|
87
|
+
style,
|
|
88
|
+
}: {
|
|
89
|
+
label?: string;
|
|
90
|
+
className?: string;
|
|
91
|
+
style?: CSSProperties;
|
|
92
|
+
}) {
|
|
93
|
+
const { t } = useArtkit();
|
|
94
|
+
return (
|
|
95
|
+
<div className={cx('ak-loading', className)} style={style} role="status" aria-busy="true">
|
|
96
|
+
{label ?? t('loading')}
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Polite live region for screen readers (spec §18.2). The text is cleared first so repeating the same
|
|
103
|
+
* message is announced again.
|
|
104
|
+
*/
|
|
105
|
+
export function useAnnouncer(): [ReactNode, (message: string) => void] {
|
|
106
|
+
const [message, setMessage] = useState('');
|
|
107
|
+
const frame = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
108
|
+
useEffect(() => () => clearTimeout(frame.current), []);
|
|
109
|
+
const announce = useCallback((text: string) => {
|
|
110
|
+
setMessage('');
|
|
111
|
+
clearTimeout(frame.current);
|
|
112
|
+
frame.current = setTimeout(() => setMessage(text), 30);
|
|
113
|
+
}, []);
|
|
114
|
+
const node = (
|
|
115
|
+
<div className="ak-sr-only" role="status" aria-live="polite" aria-atomic="true">
|
|
116
|
+
{message}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
return [node, announce];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function ChangeList({
|
|
123
|
+
changes,
|
|
124
|
+
className,
|
|
125
|
+
style,
|
|
126
|
+
}: {
|
|
127
|
+
changes: ChangeSummary[];
|
|
128
|
+
className?: string;
|
|
129
|
+
style?: CSSProperties;
|
|
130
|
+
}) {
|
|
131
|
+
const { t } = useArtkit();
|
|
132
|
+
if (changes.length === 0) return <p className="ak-notice">{t('noChanges')}</p>;
|
|
133
|
+
return (
|
|
134
|
+
<ul className={cx('ak-changes', className)} style={style}>
|
|
135
|
+
{withKeys(changes).map(({ change, key }) => (
|
|
136
|
+
<li key={key} className="ak-change" data-type={change.type}>
|
|
137
|
+
<span className="ak-change-type">{t(`change_${change.type}` as UiMessageKey)}</span>
|
|
138
|
+
<span className="ak-change-target">{change.target}</span>
|
|
139
|
+
<span className="ak-change-summary">{change.summary}</span>
|
|
140
|
+
</li>
|
|
141
|
+
))}
|
|
142
|
+
</ul>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Targets can repeat (changed + moved); a per-target counter keeps keys stable across renders.
|
|
147
|
+
function withKeys(changes: ChangeSummary[]): { change: ChangeSummary; key: string }[] {
|
|
148
|
+
const seen = new Map<string, number>();
|
|
149
|
+
return changes.map((change) => {
|
|
150
|
+
const base = `${change.type}:${change.target}`;
|
|
151
|
+
const count = seen.get(base) ?? 0;
|
|
152
|
+
seen.set(base, count + 1);
|
|
153
|
+
return { change, key: count === 0 ? base : `${base}#${count}` };
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
interface BoundaryProps {
|
|
158
|
+
fallback: (error: unknown, reset: () => void) => ReactNode;
|
|
159
|
+
onReset?: () => void;
|
|
160
|
+
children?: ReactNode;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Catches loader and renderer failures so one broken kind does not take the page down. */
|
|
164
|
+
export class ErrorBoundary extends Component<BoundaryProps, { error: unknown; failed: boolean }> {
|
|
165
|
+
override state = { error: undefined as unknown, failed: false };
|
|
166
|
+
|
|
167
|
+
static getDerivedStateFromError(error: unknown) {
|
|
168
|
+
return { error, failed: true };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
override componentDidCatch(_error: unknown, _info: ErrorInfo): void {
|
|
172
|
+
// Rendering the fallback is enough; the host app can wrap its own reporting boundary around us.
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
reset = () => {
|
|
176
|
+
this.props.onReset?.();
|
|
177
|
+
this.setState({ error: undefined, failed: false });
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
override render() {
|
|
181
|
+
if (this.state.failed) return this.props.fallback(this.state.error, this.reset);
|
|
182
|
+
return this.props.children;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function formatDate(value: string, locale: string): string {
|
|
187
|
+
const date = new Date(value);
|
|
188
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
189
|
+
try {
|
|
190
|
+
return new Intl.DateTimeFormat(locale, { dateStyle: 'short', timeStyle: 'short' }).format(date);
|
|
191
|
+
} catch {
|
|
192
|
+
return date.toISOString();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function formatLabel(ctx: ArtkitContextValue, format: string): string {
|
|
197
|
+
return ctx.registry.has(format) ? ctx.registry.get(format).label : format;
|
|
198
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { Artifact, RenderContext } from '@artkit-ai/core';
|
|
2
|
+
import { Suspense, use, useMemo, useRef } from 'react';
|
|
3
|
+
import { type ArtkitContextValue, useArtkit } from '../context.js';
|
|
4
|
+
import { useArtifact } from '../hooks.js';
|
|
5
|
+
import { parsePartialJson } from '../partial-json.js';
|
|
6
|
+
import { hashString } from '../styles.js';
|
|
7
|
+
import { cx, ErrorBoundary, ErrorMessage, Loading, rootAttributes, type Slots, slot } from './shared.js';
|
|
8
|
+
|
|
9
|
+
export type ArtifactViewPart = 'body' | 'badge' | 'loading' | 'error';
|
|
10
|
+
|
|
11
|
+
export interface ArtifactViewProps {
|
|
12
|
+
/** Committed artifact to show. */
|
|
13
|
+
artifact?: Artifact;
|
|
14
|
+
/** Alternatively, load the artifact by id. */
|
|
15
|
+
id?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Content still being streamed by the model: a partial object or the raw JSON text so far. It goes
|
|
18
|
+
* through `format.parsePartial` and shows as a provisional preview until a newer committed version
|
|
19
|
+
* arrives (spec §13.4).
|
|
20
|
+
*/
|
|
21
|
+
partial?: unknown;
|
|
22
|
+
/** Kind of the partial content when no artifact exists yet. */
|
|
23
|
+
format?: string;
|
|
24
|
+
/** Theme and locale of the partial content when no artifact exists yet. */
|
|
25
|
+
themeId?: string;
|
|
26
|
+
locale?: string;
|
|
27
|
+
mode?: RenderContext['mode'];
|
|
28
|
+
className?: string;
|
|
29
|
+
slots?: Slots<ArtifactViewPart>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Turns streamed content into renderable content, or undefined while nothing complete arrived yet. */
|
|
33
|
+
export function previewContent(ctx: ArtkitContextValue, format: string, partial: unknown): unknown {
|
|
34
|
+
if (!ctx.registry.has(format)) return undefined;
|
|
35
|
+
const raw = typeof partial === 'string' && /^\s*[[{]/.test(partial) ? parsePartialJson(partial) : partial;
|
|
36
|
+
if (raw === undefined) return undefined;
|
|
37
|
+
try {
|
|
38
|
+
// Adapters take the streamed text; structured partials (e.g. a workbook JSON) are serialized first.
|
|
39
|
+
return ctx.registry.get(format).parsePartial(typeof raw === 'string' ? raw : JSON.stringify(raw));
|
|
40
|
+
} catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function renderContext(
|
|
46
|
+
ctx: ArtkitContextValue,
|
|
47
|
+
themeId: string | undefined,
|
|
48
|
+
locale: string,
|
|
49
|
+
mode: RenderContext['mode'],
|
|
50
|
+
provisional: boolean,
|
|
51
|
+
): RenderContext {
|
|
52
|
+
return {
|
|
53
|
+
theme: ctx.themeFor(themeId),
|
|
54
|
+
locale,
|
|
55
|
+
mode,
|
|
56
|
+
...(provisional ? { provisional: true } : {}),
|
|
57
|
+
...(ctx.resolveAssetUrl ? { resolveAssetUrl: ctx.resolveAssetUrl } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Read-only view of any artifact by kind (spec §13.1). */
|
|
62
|
+
export function ArtifactView(props: ArtifactViewProps) {
|
|
63
|
+
const ctx = useArtkit();
|
|
64
|
+
const { t } = ctx;
|
|
65
|
+
const loaded = useArtifact(props.artifact ? undefined : props.id);
|
|
66
|
+
const committed = props.artifact ?? loaded.artifact;
|
|
67
|
+
|
|
68
|
+
// Remember which committed version was on screen when streaming started: any other version arriving
|
|
69
|
+
// afterwards is the commit that replaces the preview, even if the caller still passes `partial`.
|
|
70
|
+
const streamStart = useRef<{ version: number | undefined } | null>(null);
|
|
71
|
+
const stillLoading = !props.artifact && props.id !== undefined && loaded.loading;
|
|
72
|
+
if (props.partial === undefined) streamStart.current = null;
|
|
73
|
+
else if (streamStart.current === null && !stillLoading)
|
|
74
|
+
streamStart.current = { version: committed?.version };
|
|
75
|
+
const showPartial =
|
|
76
|
+
props.partial !== undefined &&
|
|
77
|
+
(streamStart.current === null || committed?.version === streamStart.current.version);
|
|
78
|
+
|
|
79
|
+
const format = props.format ?? committed?.format;
|
|
80
|
+
const partialContent = useMemo(
|
|
81
|
+
() => (showPartial && format ? previewContent(ctx, format, props.partial) : undefined),
|
|
82
|
+
[ctx, format, props.partial, showPartial],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const themeId = committed?.themeId ?? props.themeId;
|
|
86
|
+
const locale = committed?.locale ?? props.locale ?? ctx.locale;
|
|
87
|
+
const mode = props.mode ?? 'screen';
|
|
88
|
+
const rootClass = cx('ak-root ak-view', props.className);
|
|
89
|
+
const attrs = rootAttributes(ctx, themeId);
|
|
90
|
+
|
|
91
|
+
if (showPartial) {
|
|
92
|
+
return (
|
|
93
|
+
<div className={rootClass} {...attrs} data-provisional="true" lang={locale} aria-busy="true">
|
|
94
|
+
<span
|
|
95
|
+
{...slot(props.slots, 'badge', 'ak-badge ak-view-badge')}
|
|
96
|
+
data-variant="provisional"
|
|
97
|
+
role="status"
|
|
98
|
+
>
|
|
99
|
+
{t('provisional')}
|
|
100
|
+
</span>
|
|
101
|
+
{partialContent === undefined || !format ? (
|
|
102
|
+
<Loading label={t('generating')} {...slot(props.slots, 'loading', '')} />
|
|
103
|
+
) : (
|
|
104
|
+
<div {...slot(props.slots, 'body', 'ak-view-body')}>
|
|
105
|
+
<FormatRenderer
|
|
106
|
+
format={format!}
|
|
107
|
+
content={partialContent}
|
|
108
|
+
ctx={renderContext(ctx, themeId, locale, mode, true)}
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!committed) {
|
|
117
|
+
return (
|
|
118
|
+
<div className={rootClass} {...attrs}>
|
|
119
|
+
{loaded.error ? (
|
|
120
|
+
<ErrorMessage
|
|
121
|
+
error={loaded.error}
|
|
122
|
+
onRetry={() => void loaded.refresh().catch(() => undefined)}
|
|
123
|
+
{...slot(props.slots, 'error', '')}
|
|
124
|
+
/>
|
|
125
|
+
) : (
|
|
126
|
+
<Loading {...slot(props.slots, 'loading', '')} />
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div
|
|
134
|
+
className={rootClass}
|
|
135
|
+
{...attrs}
|
|
136
|
+
lang={committed.locale}
|
|
137
|
+
data-format={committed.format}
|
|
138
|
+
data-version={committed.version}
|
|
139
|
+
>
|
|
140
|
+
<div {...slot(props.slots, 'body', 'ak-view-body')}>
|
|
141
|
+
<FormatRenderer
|
|
142
|
+
format={committed.format}
|
|
143
|
+
content={committed.content}
|
|
144
|
+
ctx={renderContext(ctx, themeId, locale, mode, false)}
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface FormatRendererProps {
|
|
152
|
+
format: string;
|
|
153
|
+
content: unknown;
|
|
154
|
+
ctx: RenderContext;
|
|
155
|
+
className?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Lazy kind renderer with a JSON fallback when the kind has no renderer loader. */
|
|
159
|
+
export function FormatRenderer(props: FormatRendererProps) {
|
|
160
|
+
const ctx = useArtkit();
|
|
161
|
+
if (!ctx.hasRenderer(props.format)) {
|
|
162
|
+
return (
|
|
163
|
+
<div className={props.className}>
|
|
164
|
+
<p className="ak-notice">{ctx.t('noRenderer', { format: props.format })}</p>
|
|
165
|
+
<pre className="ak-code">
|
|
166
|
+
<code>{JSON.stringify(props.content, null, 2)}</code>
|
|
167
|
+
</pre>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return (
|
|
172
|
+
<ErrorBoundary
|
|
173
|
+
onReset={() => ctx.resetLoad('renderer', props.format)}
|
|
174
|
+
fallback={(error, reset) => <ErrorMessage error={error} onRetry={reset} />}
|
|
175
|
+
>
|
|
176
|
+
<Suspense fallback={<Loading />}>
|
|
177
|
+
<LoadedRenderer {...props} />
|
|
178
|
+
</Suspense>
|
|
179
|
+
</ErrorBoundary>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function LoadedRenderer({ format, content, ctx: renderCtx, className }: FormatRendererProps) {
|
|
184
|
+
const ctx = useArtkit();
|
|
185
|
+
const { Renderer, css } = use(ctx.loadRenderer(format));
|
|
186
|
+
return (
|
|
187
|
+
<>
|
|
188
|
+
{css ? (
|
|
189
|
+
<style href={`artkit-kind-${format}-${hashString(css)}`} precedence="artkit" nonce={ctx.nonce}>
|
|
190
|
+
{css}
|
|
191
|
+
</style>
|
|
192
|
+
) : null}
|
|
193
|
+
<Renderer content={content} ctx={renderCtx} {...(className ? { className } : {})} />
|
|
194
|
+
</>
|
|
195
|
+
);
|
|
196
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AnyArtifactExporter,
|
|
3
|
+
ArtifactRegistry,
|
|
4
|
+
AssetResolver,
|
|
5
|
+
EditorBindings,
|
|
6
|
+
RenderContext,
|
|
7
|
+
Theme,
|
|
8
|
+
} from '@artkit-ai/core';
|
|
9
|
+
import { type ComponentType, createContext, useContext } from 'react';
|
|
10
|
+
import type { ArtifactCache } from './cache.js';
|
|
11
|
+
import type { ArtifactClient } from './client.js';
|
|
12
|
+
import type { Translate } from './i18n.js';
|
|
13
|
+
|
|
14
|
+
export interface RendererProps<Content = unknown> {
|
|
15
|
+
content: Content;
|
|
16
|
+
ctx: RenderContext;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** What a kind's `/react` entry provides. */
|
|
21
|
+
export interface RendererModule {
|
|
22
|
+
// biome-ignore lint/suspicious/noExplicitAny: renderers are typed by their own format.
|
|
23
|
+
Renderer: ComponentType<RendererProps<any>>;
|
|
24
|
+
css?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** What a kind's `/edit` entry provides. */
|
|
28
|
+
export interface EditorModule {
|
|
29
|
+
// biome-ignore lint/suspicious/noExplicitAny: editors are typed by their own format.
|
|
30
|
+
Editor: ComponentType<EditorBindings<any, any>>;
|
|
31
|
+
css?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Who draws the undo, redo and save controls. `host` (default): the editor host shows them above the format editor.
|
|
34
|
+
* `format`: the format editor receives `actions` and draws them inside its own toolbar.
|
|
35
|
+
*/
|
|
36
|
+
toolbar?: 'host' | 'format';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Lazy loaders per format, so the bundle only pulls what is shown (spec §13.6). Typical use:
|
|
41
|
+
* `renderers: { document: () => import('@artkit-ai/kind-document/react').then((m) => ({ Renderer: m.DocumentRenderer, css: m.documentCss })) }`.
|
|
42
|
+
*/
|
|
43
|
+
export interface ArtifactLoaders {
|
|
44
|
+
renderers?: Record<string, () => Promise<RendererModule>>;
|
|
45
|
+
editors?: Record<string, () => Promise<EditorModule>>;
|
|
46
|
+
exporters?: Record<string, () => Promise<AnyArtifactExporter[]>>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type ColorScheme = 'light' | 'dark' | 'system';
|
|
50
|
+
|
|
51
|
+
export interface ArtkitContextValue {
|
|
52
|
+
registry: ArtifactRegistry;
|
|
53
|
+
client: ArtifactClient;
|
|
54
|
+
cache: ArtifactCache;
|
|
55
|
+
locale: string;
|
|
56
|
+
t: Translate;
|
|
57
|
+
appTheme: Theme;
|
|
58
|
+
colorScheme: ColorScheme;
|
|
59
|
+
/** Theme of an artifact: provider themes, then registry themes, then the application theme. */
|
|
60
|
+
themeFor(themeId: string | undefined): Theme;
|
|
61
|
+
resolveAssetUrl?: (ref: string) => string | undefined;
|
|
62
|
+
assets?: AssetResolver;
|
|
63
|
+
/** Formats the server can export, per format. */
|
|
64
|
+
exportFormats: Record<string, readonly string[]>;
|
|
65
|
+
hasRenderer(format: string): boolean;
|
|
66
|
+
hasEditor(format: string): boolean;
|
|
67
|
+
hasExporters(format: string): boolean;
|
|
68
|
+
loadRenderer(format: string): Promise<RendererModule>;
|
|
69
|
+
loadEditor(format: string): Promise<EditorModule>;
|
|
70
|
+
loadExporters(format: string): Promise<AnyArtifactExporter[]>;
|
|
71
|
+
/** Forgets a failed load so it can be retried. */
|
|
72
|
+
resetLoad(type: 'renderer' | 'editor' | 'exporters', format: string): void;
|
|
73
|
+
nonce?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const ArtkitContext = createContext<ArtkitContextValue | null>(null);
|
|
77
|
+
|
|
78
|
+
/** The artkit context, or null outside `<ArtifactProvider>` (for components that also work standalone). */
|
|
79
|
+
export function useOptionalArtkit(): ArtkitContextValue | null {
|
|
80
|
+
return useContext(ArtkitContext);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function useArtkit(): ArtkitContextValue {
|
|
84
|
+
const value = useContext(ArtkitContext);
|
|
85
|
+
if (!value) throw new Error('artkit components and hooks must be rendered inside <ArtifactProvider>.');
|
|
86
|
+
return value;
|
|
87
|
+
}
|