@artkit-ai/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.
Files changed (118) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +145 -2
  3. package/dist/cache.d.ts +35 -0
  4. package/dist/cache.d.ts.map +1 -0
  5. package/dist/cache.js +116 -0
  6. package/dist/cache.js.map +1 -0
  7. package/dist/client.d.ts +79 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +260 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/components/approval.d.ts +34 -0
  12. package/dist/components/approval.d.ts.map +1 -0
  13. package/dist/components/approval.js +80 -0
  14. package/dist/components/approval.js.map +1 -0
  15. package/dist/components/card.d.ts +22 -0
  16. package/dist/components/card.d.ts.map +1 -0
  17. package/dist/components/card.js +16 -0
  18. package/dist/components/card.js.map +1 -0
  19. package/dist/components/editor.d.ts +28 -0
  20. package/dist/components/editor.d.ts.map +1 -0
  21. package/dist/components/editor.js +161 -0
  22. package/dist/components/editor.js.map +1 -0
  23. package/dist/components/icons.d.ts +8 -0
  24. package/dist/components/icons.d.ts.map +1 -0
  25. package/dist/components/icons.js +14 -0
  26. package/dist/components/icons.js.map +1 -0
  27. package/dist/components/panel.d.ts +34 -0
  28. package/dist/components/panel.d.ts.map +1 -0
  29. package/dist/components/panel.js +216 -0
  30. package/dist/components/panel.js.map +1 -0
  31. package/dist/components/shared.d.ts +71 -0
  32. package/dist/components/shared.d.ts.map +1 -0
  33. package/dist/components/shared.js +104 -0
  34. package/dist/components/shared.js.map +1 -0
  35. package/dist/components/version-diff.d.ts +35 -0
  36. package/dist/components/version-diff.d.ts.map +1 -0
  37. package/dist/components/version-diff.js +85 -0
  38. package/dist/components/version-diff.js.map +1 -0
  39. package/dist/components/view.d.ts +40 -0
  40. package/dist/components/view.d.ts.map +1 -0
  41. package/dist/components/view.js +75 -0
  42. package/dist/components/view.js.map +1 -0
  43. package/dist/context.d.ts +59 -0
  44. package/dist/context.d.ts.map +1 -0
  45. package/dist/context.js +13 -0
  46. package/dist/context.js.map +1 -0
  47. package/dist/controls.d.ts +145 -0
  48. package/dist/controls.d.ts.map +1 -0
  49. package/dist/controls.js +188 -0
  50. package/dist/controls.js.map +1 -0
  51. package/dist/download.d.ts +4 -0
  52. package/dist/download.d.ts.map +1 -0
  53. package/dist/download.js +20 -0
  54. package/dist/download.js.map +1 -0
  55. package/dist/editor-session.d.ts +184 -0
  56. package/dist/editor-session.d.ts.map +1 -0
  57. package/dist/editor-session.js +614 -0
  58. package/dist/editor-session.js.map +1 -0
  59. package/dist/hooks.d.ts +85 -0
  60. package/dist/hooks.d.ts.map +1 -0
  61. package/dist/hooks.js +297 -0
  62. package/dist/hooks.js.map +1 -0
  63. package/dist/i18n.d.ts +197 -0
  64. package/dist/i18n.d.ts.map +1 -0
  65. package/dist/i18n.js +207 -0
  66. package/dist/i18n.js.map +1 -0
  67. package/dist/index.d.ts +22 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +22 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/kit-css.d.ts +3 -0
  72. package/dist/kit-css.d.ts.map +1 -0
  73. package/dist/kit-css.js +5 -0
  74. package/dist/kit-css.js.map +1 -0
  75. package/dist/line-diff.d.ts +25 -0
  76. package/dist/line-diff.d.ts.map +1 -0
  77. package/dist/line-diff.js +96 -0
  78. package/dist/line-diff.js.map +1 -0
  79. package/dist/partial-json.d.ts +8 -0
  80. package/dist/partial-json.d.ts.map +1 -0
  81. package/dist/partial-json.js +176 -0
  82. package/dist/partial-json.js.map +1 -0
  83. package/dist/provider.d.ts +42 -0
  84. package/dist/provider.d.ts.map +1 -0
  85. package/dist/provider.js +95 -0
  86. package/dist/provider.js.map +1 -0
  87. package/dist/styles.d.ts +25 -0
  88. package/dist/styles.d.ts.map +1 -0
  89. package/dist/styles.js +203 -0
  90. package/dist/styles.js.map +1 -0
  91. package/dist/toolbar.d.ts +90 -0
  92. package/dist/toolbar.d.ts.map +1 -0
  93. package/dist/toolbar.js +233 -0
  94. package/dist/toolbar.js.map +1 -0
  95. package/package.json +50 -3
  96. package/src/cache.ts +132 -0
  97. package/src/client.ts +390 -0
  98. package/src/components/approval.tsx +219 -0
  99. package/src/components/card.tsx +87 -0
  100. package/src/components/editor.tsx +392 -0
  101. package/src/components/icons.tsx +14 -0
  102. package/src/components/panel.tsx +591 -0
  103. package/src/components/shared.tsx +207 -0
  104. package/src/components/version-diff.tsx +266 -0
  105. package/src/components/view.tsx +199 -0
  106. package/src/context.ts +82 -0
  107. package/src/controls.tsx +587 -0
  108. package/src/download.ts +23 -0
  109. package/src/editor-session.ts +738 -0
  110. package/src/hooks.ts +411 -0
  111. package/src/i18n.ts +217 -0
  112. package/src/index.ts +160 -0
  113. package/src/kit-css.ts +5 -0
  114. package/src/line-diff.ts +111 -0
  115. package/src/partial-json.ts +160 -0
  116. package/src/provider.tsx +176 -0
  117. package/src/styles.ts +222 -0
  118. package/src/toolbar.tsx +536 -0
package/src/index.ts ADDED
@@ -0,0 +1,160 @@
1
+ export { ArtifactCache, type CacheEntry } from './cache.js';
2
+ export {
3
+ type ApplyInput,
4
+ type ArtifactClient,
5
+ type ClientExportResult,
6
+ createHttpClient,
7
+ createServiceClient,
8
+ type ExportInput,
9
+ errorFromResponse,
10
+ filenameFromDisposition,
11
+ type HttpClientOptions,
12
+ type ReadOptions,
13
+ type RequestOptions,
14
+ STATUS_ACTIONS,
15
+ type StatusAction,
16
+ } from './client.js';
17
+ export {
18
+ ArtifactApproval,
19
+ type ArtifactApprovalPart,
20
+ type ArtifactApprovalProps,
21
+ } from './components/approval.js';
22
+ export { ArtifactCard, type ArtifactCardPart, type ArtifactCardProps } from './components/card.js';
23
+ export {
24
+ ArtifactEditor,
25
+ type ArtifactEditorPart,
26
+ type ArtifactEditorProps,
27
+ type EditorPermission,
28
+ } from './components/editor.js';
29
+ export {
30
+ ArtifactPanel,
31
+ type ArtifactPanelPart,
32
+ type ArtifactPanelProps,
33
+ type PanelTab,
34
+ } from './components/panel.js';
35
+ export { type Slots, StatusBadge } from './components/shared.js';
36
+ export {
37
+ ArtifactVersionDiff,
38
+ type ArtifactVersionDiffPart,
39
+ type ArtifactVersionDiffProps,
40
+ compareVersions,
41
+ type DiffLayout,
42
+ type VersionComparison,
43
+ } from './components/version-diff.js';
44
+ export {
45
+ ArtifactView,
46
+ type ArtifactViewPart,
47
+ type ArtifactViewProps,
48
+ KindRenderer,
49
+ previewContent,
50
+ } from './components/view.js';
51
+ export {
52
+ type ArtifactLoaders,
53
+ type ArtkitContextValue,
54
+ type ColorScheme,
55
+ type EditorModule,
56
+ type RendererModule,
57
+ type RendererProps,
58
+ useArtkit,
59
+ useOptionalArtkit,
60
+ } from './context.js';
61
+ export {
62
+ Button,
63
+ type ButtonProps,
64
+ ButtonRow,
65
+ ColorPicker,
66
+ type ColorPickerProps,
67
+ type ControlSize,
68
+ type ControlVariant,
69
+ controlsCss,
70
+ Field,
71
+ type FieldProps,
72
+ FieldRow,
73
+ Hint,
74
+ IconButton,
75
+ type IconButtonProps,
76
+ Inspector,
77
+ type InspectorProps,
78
+ InspectorSection,
79
+ type InspectorSectionProps,
80
+ NumberField,
81
+ type NumberFieldProps,
82
+ Segmented,
83
+ type SegmentedProps,
84
+ SelectField,
85
+ type SelectFieldProps,
86
+ Switch,
87
+ type SwitchProps,
88
+ TextField,
89
+ type TextFieldProps,
90
+ Toggle,
91
+ type ToggleProps,
92
+ } from './controls.js';
93
+ export { triggerDownload } from './download.js';
94
+ export {
95
+ type ConflictChoice,
96
+ EditorSession,
97
+ type EditorSessionOptions,
98
+ type EditorSessionState,
99
+ type EnvelopeState,
100
+ envelopeOf,
101
+ operationsBetween,
102
+ type SendRequest,
103
+ type SessionConflict,
104
+ type SessionEvent,
105
+ type SessionStatus,
106
+ } from './editor-session.js';
107
+ export {
108
+ type ApplyConflict,
109
+ type ApplyOptions,
110
+ type ExportOptions,
111
+ type UseApplyOperationsResult,
112
+ type UseArtifactExportResult,
113
+ type UseArtifactHistoryResult,
114
+ type UseArtifactOptions,
115
+ type UseArtifactResult,
116
+ useApplyOperations,
117
+ useArtifact,
118
+ useArtifactExport,
119
+ useArtifactHistory,
120
+ useLatestVersion,
121
+ } from './hooks.js';
122
+ export {
123
+ createTranslator,
124
+ type Translate,
125
+ type UiMessageKey,
126
+ type UiMessageOverrides,
127
+ type UiMessages,
128
+ uiMessages,
129
+ } from './i18n.js';
130
+ export { kitCss } from './kit-css.js';
131
+ export {
132
+ diffLines,
133
+ type LineDiffEntry,
134
+ type LineDiffOptions,
135
+ type LineDiffType,
136
+ type SplitRow,
137
+ toSplitRows,
138
+ } from './line-diff.js';
139
+ export { parsePartialJson } from './partial-json.js';
140
+ export { ArtifactProvider, type ArtifactProviderProps } from './provider.js';
141
+ export { artkitCss, darkTheme, mixColors, themeStyles } from './styles.js';
142
+ export {
143
+ Toolbar,
144
+ ToolbarButton,
145
+ type ToolbarButtonProps,
146
+ ToolbarColor,
147
+ type ToolbarColorOption,
148
+ type ToolbarColorProps,
149
+ ToolbarGroup,
150
+ ToolbarMenu,
151
+ type ToolbarMenuItem,
152
+ type ToolbarMenuProps,
153
+ type ToolbarOption,
154
+ type ToolbarProps,
155
+ ToolbarSelect,
156
+ type ToolbarSelectProps,
157
+ ToolbarSpacer,
158
+ themeSwatches,
159
+ toolbarCss,
160
+ } from './toolbar.js';
package/src/kit-css.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { controlsCss } from './controls.js';
2
+ import { toolbarCss } from './toolbar.js';
3
+
4
+ /** Toolbar and form control styles shared by every kind editor and viewer (include once per kind CSS). */
5
+ export const kitCss = `${toolbarCss}${controlsCss}`;
@@ -0,0 +1,111 @@
1
+ export type LineDiffType = 'equal' | 'insert' | 'delete';
2
+
3
+ export interface LineDiffEntry {
4
+ type: LineDiffType;
5
+ text: string;
6
+ /** 1-based line number in the old text (equal and delete). */
7
+ oldLine?: number;
8
+ /** 1-based line number in the new text (equal and insert). */
9
+ newLine?: number;
10
+ }
11
+
12
+ export interface LineDiffOptions {
13
+ /**
14
+ * Upper bound for the LCS table (rows × columns) after trimming the common prefix and suffix.
15
+ * Above it the middle is reported as a block replacement, keeping memory bounded on huge projections.
16
+ */
17
+ maxCells?: number;
18
+ }
19
+
20
+ const splitLines = (text: string) => (text === '' ? [] : text.replace(/\r\n?/g, '\n').split('\n'));
21
+
22
+ /** Line diff based on the longest common subsequence, with common prefix/suffix trimmed first. */
23
+ export function diffLines(before: string, after: string, options: LineDiffOptions = {}): LineDiffEntry[] {
24
+ const a = splitLines(before);
25
+ const b = splitLines(after);
26
+ const maxCells = options.maxCells ?? 4_000_000;
27
+ let start = 0;
28
+ while (start < a.length && start < b.length && a[start] === b[start]) start++;
29
+ let endA = a.length;
30
+ let endB = b.length;
31
+ while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
32
+ endA--;
33
+ endB--;
34
+ }
35
+
36
+ const out: LineDiffEntry[] = [];
37
+ for (let i = 0; i < start; i++) out.push({ type: 'equal', text: a[i]!, oldLine: i + 1, newLine: i + 1 });
38
+
39
+ const midA = a.slice(start, endA);
40
+ const midB = b.slice(start, endB);
41
+ const n = midA.length;
42
+ const m = midB.length;
43
+ if (n > 0 && m > 0 && n * m <= maxCells) {
44
+ // lengths[i][j] = LCS length of midA[i..] and midB[j..], stored flat.
45
+ const width = m + 1;
46
+ const lengths = new Uint32Array((n + 1) * width);
47
+ for (let i = n - 1; i >= 0; i--) {
48
+ for (let j = m - 1; j >= 0; j--) {
49
+ lengths[i * width + j] =
50
+ midA[i] === midB[j]
51
+ ? lengths[(i + 1) * width + j + 1]! + 1
52
+ : Math.max(lengths[(i + 1) * width + j]!, lengths[i * width + j + 1]!);
53
+ }
54
+ }
55
+ let i = 0;
56
+ let j = 0;
57
+ while (i < n && j < m) {
58
+ if (midA[i] === midB[j]) {
59
+ out.push({ type: 'equal', text: midA[i]!, oldLine: start + i + 1, newLine: start + j + 1 });
60
+ i++;
61
+ j++;
62
+ } else if (lengths[(i + 1) * width + j]! >= lengths[i * width + j + 1]!) {
63
+ out.push({ type: 'delete', text: midA[i]!, oldLine: start + i + 1 });
64
+ i++;
65
+ } else {
66
+ out.push({ type: 'insert', text: midB[j]!, newLine: start + j + 1 });
67
+ j++;
68
+ }
69
+ }
70
+ for (; i < n; i++) out.push({ type: 'delete', text: midA[i]!, oldLine: start + i + 1 });
71
+ for (; j < m; j++) out.push({ type: 'insert', text: midB[j]!, newLine: start + j + 1 });
72
+ } else {
73
+ for (let k = 0; k < n; k++) out.push({ type: 'delete', text: midA[k]!, oldLine: start + k + 1 });
74
+ for (let k = 0; k < m; k++) out.push({ type: 'insert', text: midB[k]!, newLine: start + k + 1 });
75
+ }
76
+
77
+ for (let k = 0; k < a.length - endA; k++) {
78
+ out.push({ type: 'equal', text: a[endA + k]!, oldLine: endA + k + 1, newLine: endB + k + 1 });
79
+ }
80
+ return out;
81
+ }
82
+
83
+ export interface SplitRow {
84
+ left?: LineDiffEntry;
85
+ right?: LineDiffEntry;
86
+ }
87
+
88
+ /** Pairs deletions with the insertions that follow them, for a side-by-side layout. */
89
+ export function toSplitRows(entries: LineDiffEntry[]): SplitRow[] {
90
+ const rows: SplitRow[] = [];
91
+ let index = 0;
92
+ while (index < entries.length) {
93
+ const entry = entries[index]!;
94
+ if (entry.type === 'equal') {
95
+ rows.push({ left: entry, right: entry });
96
+ index++;
97
+ continue;
98
+ }
99
+ const deletes: LineDiffEntry[] = [];
100
+ const inserts: LineDiffEntry[] = [];
101
+ while (index < entries.length && entries[index]!.type === 'delete') deletes.push(entries[index++]!);
102
+ while (index < entries.length && entries[index]!.type === 'insert') inserts.push(entries[index++]!);
103
+ for (let k = 0; k < Math.max(deletes.length, inserts.length); k++) {
104
+ const row: SplitRow = {};
105
+ if (deletes[k]) row.left = deletes[k];
106
+ if (inserts[k]) row.right = inserts[k];
107
+ rows.push(row);
108
+ }
109
+ }
110
+ return rows;
111
+ }
@@ -0,0 +1,160 @@
1
+ const INCOMPLETE = Symbol('incomplete');
2
+ const MAX_DEPTH = 200;
3
+
4
+ class PartialJsonSyntaxError extends Error {}
5
+
6
+ /**
7
+ * Best-effort parse of a JSON prefix streamed by a model (spec §13.4). Complete values are kept, a string
8
+ * value cut midway keeps the text received so far, and anything else unfinished at the end (a key, a
9
+ * number, a literal) is dropped. Returns undefined for text that is not a JSON prefix. The kind's
10
+ * `parsePartial` then decides which trailing items are complete enough to render.
11
+ */
12
+ export function parsePartialJson(text: string): unknown {
13
+ try {
14
+ return JSON.parse(text);
15
+ } catch {
16
+ // Not complete yet: fall through to the tolerant parser.
17
+ }
18
+ let i = 0;
19
+ const length = text.length;
20
+
21
+ const skipWhitespace = () => {
22
+ while (i < length && (text[i] === ' ' || text[i] === '\n' || text[i] === '\r' || text[i] === '\t')) i++;
23
+ };
24
+
25
+ function parseString(): { value: string; complete: boolean } {
26
+ i++; // opening quote
27
+ let out = '';
28
+ while (i < length) {
29
+ const char = text[i]!;
30
+ if (char === '"') {
31
+ i++;
32
+ return { value: out, complete: true };
33
+ }
34
+ if (char === '\\') {
35
+ const next = text[i + 1];
36
+ if (next === undefined) break;
37
+ if (next === 'u') {
38
+ const hex = text.slice(i + 2, i + 6);
39
+ if (hex.length < 4) break;
40
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) throw new PartialJsonSyntaxError('bad escape');
41
+ out += String.fromCharCode(Number.parseInt(hex, 16));
42
+ i += 6;
43
+ continue;
44
+ }
45
+ const escapes: Record<string, string> = {
46
+ '"': '"',
47
+ '\\': '\\',
48
+ '/': '/',
49
+ b: '\b',
50
+ f: '\f',
51
+ n: '\n',
52
+ r: '\r',
53
+ t: '\t',
54
+ };
55
+ const mapped = escapes[next];
56
+ if (mapped === undefined) throw new PartialJsonSyntaxError('bad escape');
57
+ out += mapped;
58
+ i += 2;
59
+ continue;
60
+ }
61
+ out += char;
62
+ i++;
63
+ }
64
+ i = length;
65
+ return { value: out, complete: false };
66
+ }
67
+
68
+ function parseValue(depth: number): unknown {
69
+ if (depth > MAX_DEPTH) throw new PartialJsonSyntaxError('too deep');
70
+ skipWhitespace();
71
+ if (i >= length) return INCOMPLETE;
72
+ const char = text[i]!;
73
+ if (char === '{') return parseObject(depth);
74
+ if (char === '[') return parseArray(depth);
75
+ if (char === '"') return parseString().value;
76
+ for (const [literal, value] of [
77
+ ['true', true],
78
+ ['false', false],
79
+ ['null', null],
80
+ ] as const) {
81
+ if (text.startsWith(literal, i)) {
82
+ i += literal.length;
83
+ return value;
84
+ }
85
+ if (literal.startsWith(text.slice(i))) {
86
+ i = length;
87
+ return INCOMPLETE;
88
+ }
89
+ }
90
+ const rest = text.slice(i);
91
+ const numeric = /^[-+0-9.eE]*/.exec(rest)?.[0] ?? '';
92
+ if (numeric.length === 0) throw new PartialJsonSyntaxError('unexpected token');
93
+ // A number touching the end of the stream may still grow ("12" → "123", "1." → "1.5").
94
+ if (numeric.length === rest.length) {
95
+ i = length;
96
+ return INCOMPLETE;
97
+ }
98
+ if (!/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(numeric))
99
+ throw new PartialJsonSyntaxError('bad number');
100
+ i += numeric.length;
101
+ return Number(numeric);
102
+ }
103
+
104
+ function parseObject(depth: number): Record<string, unknown> {
105
+ i++;
106
+ const result: Record<string, unknown> = {};
107
+ for (;;) {
108
+ skipWhitespace();
109
+ if (i >= length) return result;
110
+ const char = text[i]!;
111
+ if (char === '}') {
112
+ i++;
113
+ return result;
114
+ }
115
+ if (char === ',') {
116
+ i++;
117
+ continue;
118
+ }
119
+ if (char !== '"') throw new PartialJsonSyntaxError('expected key');
120
+ const key = parseString();
121
+ if (!key.complete) return result;
122
+ skipWhitespace();
123
+ if (i >= length) return result;
124
+ if (text[i] !== ':') throw new PartialJsonSyntaxError('expected colon');
125
+ i++;
126
+ const value = parseValue(depth + 1);
127
+ if (value === INCOMPLETE) return result;
128
+ result[key.value] = value;
129
+ }
130
+ }
131
+
132
+ function parseArray(depth: number): unknown[] {
133
+ i++;
134
+ const result: unknown[] = [];
135
+ for (;;) {
136
+ skipWhitespace();
137
+ if (i >= length) return result;
138
+ const char = text[i]!;
139
+ if (char === ']') {
140
+ i++;
141
+ return result;
142
+ }
143
+ if (char === ',') {
144
+ i++;
145
+ continue;
146
+ }
147
+ const value = parseValue(depth + 1);
148
+ if (value === INCOMPLETE) return result;
149
+ result.push(value);
150
+ }
151
+ }
152
+
153
+ try {
154
+ const value = parseValue(0);
155
+ return value === INCOMPLETE ? undefined : value;
156
+ } catch (error) {
157
+ if (error instanceof PartialJsonSyntaxError) return undefined;
158
+ throw error;
159
+ }
160
+ }
@@ -0,0 +1,176 @@
1
+ import { type ArtifactRegistry, type AssetResolver, defaultTheme, type Theme } from '@artkit-ai/core';
2
+ import { soraFontFaceCss } from '@artkit-ai/core/fonts';
3
+ import { type ReactNode, useMemo, useRef } from 'react';
4
+ import { cacheFor } from './cache.js';
5
+ import type { ArtifactClient } from './client.js';
6
+ import {
7
+ type ArtifactLoaders,
8
+ ArtkitContext,
9
+ type ArtkitContextValue,
10
+ type ColorScheme,
11
+ type EditorModule,
12
+ type RendererModule,
13
+ } from './context.js';
14
+ import { createTranslator, type UiMessageOverrides } from './i18n.js';
15
+ import { artkitCss, hashString, themeStyles } from './styles.js';
16
+
17
+ export interface ArtifactProviderProps {
18
+ /** Core registry with the kinds the app shows (parsePartial, apply, project, diff, lint). */
19
+ registry: ArtifactRegistry;
20
+ client: ArtifactClient;
21
+ /** Lazy renderers, editors and exporters per kind. Read once per kind: later changes are ignored. */
22
+ loaders?: ArtifactLoaders;
23
+ /** UI locale (pt-BR or en built in). Artifacts keep their own locale for content formatting. */
24
+ locale?: string;
25
+ /** Extra or overriding UI strings, keyed by locale. */
26
+ messages?: UiMessageOverrides;
27
+ /** Application theme (object or registered id), used by the host chrome and as fallback. */
28
+ theme?: Theme | string;
29
+ /** Extra artifact themes, looked up by `artifact.themeId` before the registry. */
30
+ themes?: Theme[];
31
+ /** Dark variants keyed by theme id; others are derived automatically. */
32
+ darkThemes?: Record<string, Theme>;
33
+ /** `system` follows `prefers-color-scheme`; `data-theme` on an ancestor also works. */
34
+ colorScheme?: ColorScheme;
35
+ /** Synchronous `asset://` lookup for renderers. */
36
+ resolveAssetUrl?: (ref: string) => string | undefined;
37
+ /** Asset access for exports that run in the browser. */
38
+ assets?: AssetResolver;
39
+ /** Formats the server can export, per kind (merged with exporters available in the browser). */
40
+ exportFormats?: Record<string, readonly string[]>;
41
+ /** Set false when the app ships `artkitCss` itself. */
42
+ injectStyles?: boolean;
43
+ /** CSP nonce for the injected `<style>` elements. */
44
+ nonce?: string;
45
+ /**
46
+ * Inject the embedded Sora font (Mouts design system) when a theme uses it. Default true; set false
47
+ * when the app already serves Sora (e.g. through @fontsource).
48
+ */
49
+ injectFont?: boolean;
50
+ children?: ReactNode;
51
+ }
52
+
53
+ type LoadKey = `${'renderer' | 'editor' | 'exporters'}:${string}`;
54
+
55
+ export function ArtifactProvider(props: ArtifactProviderProps) {
56
+ const {
57
+ registry,
58
+ client,
59
+ loaders,
60
+ locale = 'en',
61
+ messages,
62
+ colorScheme = 'system',
63
+ injectStyles = true,
64
+ injectFont = true,
65
+ nonce,
66
+ } = props;
67
+ const loads = useRef(new Map<LoadKey, Promise<unknown>>());
68
+ const loadersRef = useRef(loaders);
69
+ loadersRef.current = loaders;
70
+
71
+ const appTheme = useMemo<Theme>(() => {
72
+ if (typeof props.theme === 'object') return props.theme;
73
+ if (typeof props.theme === 'string') {
74
+ const found = props.themes?.find((theme) => theme.id === props.theme);
75
+ if (found) return found;
76
+ if (registry.hasTheme(props.theme)) return registry.theme(props.theme);
77
+ }
78
+ return registry.hasTheme('default') ? registry.theme('default') : defaultTheme;
79
+ }, [props.theme, props.themes, registry]);
80
+
81
+ const themeList = useMemo(() => {
82
+ const byId = new Map<string, Theme>();
83
+ for (const theme of registry.themes()) byId.set(theme.id, theme);
84
+ for (const theme of props.themes ?? []) byId.set(theme.id, theme);
85
+ return [...byId.values()];
86
+ }, [registry, props.themes]);
87
+
88
+ const css = useMemo(
89
+ () => themeStyles(appTheme, themeList, new Map(Object.entries(props.darkThemes ?? {}))),
90
+ [appTheme, themeList, props.darkThemes],
91
+ );
92
+
93
+ const value = useMemo<ArtkitContextValue>(() => {
94
+ const byId = new Map(themeList.map((theme) => [theme.id, theme]));
95
+ function load<T>(type: 'renderer' | 'editor' | 'exporters', kind: string): Promise<T> {
96
+ const key: LoadKey = `${type}:${kind}`;
97
+ let promise = loads.current.get(key) as Promise<T> | undefined;
98
+ if (!promise) {
99
+ const group =
100
+ type === 'renderer'
101
+ ? loadersRef.current?.renderers
102
+ : type === 'editor'
103
+ ? loadersRef.current?.editors
104
+ : loadersRef.current?.exporters;
105
+ const loader = group?.[kind];
106
+ promise = loader
107
+ ? (loader() as Promise<T>)
108
+ : Promise.reject(new Error(`No ${type} loader registered for kind "${kind}".`));
109
+ loads.current.set(key, promise);
110
+ }
111
+ return promise;
112
+ }
113
+ return {
114
+ registry,
115
+ client,
116
+ cache: cacheFor(client),
117
+ locale,
118
+ t: createTranslator(locale, messages),
119
+ appTheme,
120
+ colorScheme,
121
+ themeFor: (themeId) => (themeId ? byId.get(themeId) : undefined) ?? appTheme,
122
+ ...(props.resolveAssetUrl ? { resolveAssetUrl: props.resolveAssetUrl } : {}),
123
+ ...(props.assets ? { assets: props.assets } : {}),
124
+ exportFormats: props.exportFormats ?? {},
125
+ hasRenderer: (kind) => Boolean(loadersRef.current?.renderers?.[kind]),
126
+ hasEditor: (kind) => Boolean(loadersRef.current?.editors?.[kind]),
127
+ hasExporters: (kind) => Boolean(loadersRef.current?.exporters?.[kind]),
128
+ loadRenderer: (kind) => load<RendererModule>('renderer', kind),
129
+ loadEditor: (kind) => load<EditorModule>('editor', kind),
130
+ loadExporters: (kind) => load('exporters', kind),
131
+ resetLoad: (type, kind) => {
132
+ loads.current.delete(`${type}:${kind}`);
133
+ },
134
+ ...(nonce ? { nonce } : {}),
135
+ };
136
+ }, [
137
+ registry,
138
+ client,
139
+ locale,
140
+ messages,
141
+ appTheme,
142
+ colorScheme,
143
+ themeList,
144
+ props.resolveAssetUrl,
145
+ props.assets,
146
+ props.exportFormats,
147
+ nonce,
148
+ ]);
149
+
150
+ return (
151
+ <ArtkitContext.Provider value={value}>
152
+ {injectStyles ? (
153
+ <>
154
+ {injectFont && usesSora(appTheme, themeList) ? (
155
+ <style href="artkit-font-sora" precedence="artkit" nonce={nonce}>
156
+ {soraFontFaceCss()}
157
+ </style>
158
+ ) : null}
159
+ <style href="artkit-base" precedence="artkit" nonce={nonce}>
160
+ {artkitCss}
161
+ </style>
162
+ <style href={`artkit-theme-${hashString(css)}`} precedence="artkit" nonce={nonce}>
163
+ {css}
164
+ </style>
165
+ </>
166
+ ) : null}
167
+ {props.children}
168
+ </ArtkitContext.Provider>
169
+ );
170
+ }
171
+
172
+ function usesSora(appTheme: Theme, themes: Theme[]): boolean {
173
+ return [appTheme, ...themes].some((theme) =>
174
+ [theme.fonts.heading, theme.fonts.body].some((font) => font.trim().toLowerCase() === 'sora'),
175
+ );
176
+ }