@artkit-ai/react 0.1.0 → 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/dist/cache.d.ts +7 -11
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +32 -53
- package/dist/cache.js.map +1 -1
- package/dist/client.d.ts +4 -15
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -29
- package/dist/client.js.map +1 -1
- package/dist/components/card.d.ts +2 -2
- package/dist/components/card.d.ts.map +1 -1
- package/dist/components/card.js +3 -3
- package/dist/components/card.js.map +1 -1
- package/dist/components/editor.d.ts +31 -18
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +81 -131
- package/dist/components/editor.js.map +1 -1
- package/dist/components/icons.d.ts +2 -2
- package/dist/components/icons.d.ts.map +1 -1
- package/dist/components/icons.js +2 -2
- package/dist/components/icons.js.map +1 -1
- package/dist/components/panel.d.ts +13 -16
- package/dist/components/panel.d.ts.map +1 -1
- package/dist/components/panel.js +13 -97
- package/dist/components/panel.js.map +1 -1
- package/dist/components/shared.d.ts +2 -6
- package/dist/components/shared.d.ts.map +1 -1
- package/dist/components/shared.js +2 -6
- package/dist/components/shared.js.map +1 -1
- package/dist/components/view.d.ts +7 -8
- package/dist/components/view.d.ts.map +1 -1
- package/dist/components/view.js +16 -15
- package/dist/components/view.js.map +1 -1
- package/dist/context.d.ts +14 -9
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js.map +1 -1
- package/dist/editor-session.d.ts +77 -165
- package/dist/editor-session.d.ts.map +1 -1
- package/dist/editor-session.js +164 -586
- package/dist/editor-session.js.map +1 -1
- package/dist/hooks.d.ts +2 -23
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +24 -116
- package/dist/hooks.js.map +1 -1
- package/dist/i18n.d.ts +4 -66
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +6 -68
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +6 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -8
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +1 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +12 -12
- package/dist/provider.js.map +1 -1
- package/dist/styles.d.ts +1 -1
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +4 -4
- package/package.json +2 -2
- package/src/cache.ts +32 -55
- package/src/client.ts +7 -55
- package/src/components/card.tsx +6 -7
- package/src/components/editor.tsx +208 -308
- package/src/components/icons.tsx +2 -2
- package/src/components/panel.tsx +43 -300
- package/src/components/shared.tsx +3 -12
- package/src/components/view.tsx +26 -29
- package/src/context.ts +16 -11
- package/src/editor-session.ts +197 -675
- package/src/hooks.ts +25 -140
- package/src/i18n.ts +6 -68
- package/src/index.ts +7 -39
- package/src/provider.tsx +13 -13
- package/src/styles.ts +4 -4
- package/dist/components/approval.d.ts +0 -34
- package/dist/components/approval.d.ts.map +0 -1
- package/dist/components/approval.js +0 -80
- package/dist/components/approval.js.map +0 -1
- package/dist/components/version-diff.d.ts +0 -35
- package/dist/components/version-diff.d.ts.map +0 -1
- package/dist/components/version-diff.js +0 -85
- package/dist/components/version-diff.js.map +0 -1
- package/dist/line-diff.d.ts +0 -25
- package/dist/line-diff.d.ts.map +0 -1
- package/dist/line-diff.js +0 -96
- package/dist/line-diff.js.map +0 -1
- package/src/components/approval.tsx +0 -219
- package/src/components/version-diff.tsx +0 -266
- package/src/line-diff.ts +0 -111
|
@@ -1,76 +1,62 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { Redo2,
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
Suspense,
|
|
7
|
-
use,
|
|
8
|
-
useCallback,
|
|
9
|
-
useEffect,
|
|
10
|
-
useId,
|
|
11
|
-
useMemo,
|
|
12
|
-
useRef,
|
|
13
|
-
useState,
|
|
14
|
-
useSyncExternalStore,
|
|
15
|
-
} from 'react';
|
|
16
|
-
import { useArtkit } from '../context.js';
|
|
17
|
-
import { EditorSession, type EditorSessionState } from '../editor-session.js';
|
|
1
|
+
import type { Artifact, EditorActions, EmitOptions } from '@artkit-ai/core';
|
|
2
|
+
import { Redo2, Save, Undo2 } from 'lucide-react';
|
|
3
|
+
import { type KeyboardEvent, Suspense, use, useEffect, useMemo, useState, useSyncExternalStore } from 'react';
|
|
4
|
+
import { type ArtkitContextValue, useArtkit } from '../context.js';
|
|
5
|
+
import { DraftSession, type DraftSnapshot } from '../editor-session.js';
|
|
18
6
|
import { useArtifact } from '../hooks.js';
|
|
19
|
-
import type { UiMessageKey } from '../i18n.js';
|
|
20
7
|
import { hashString } from '../styles.js';
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
cx,
|
|
24
|
-
ErrorBoundary,
|
|
25
|
-
ErrorMessage,
|
|
26
|
-
errorMessage,
|
|
27
|
-
formatDate,
|
|
28
|
-
Loading,
|
|
29
|
-
rootAttributes,
|
|
30
|
-
type Slots,
|
|
31
|
-
slot,
|
|
32
|
-
useAnnouncer,
|
|
33
|
-
} from './shared.js';
|
|
34
|
-
import { ArtifactView, KindRenderer, renderContext } from './view.js';
|
|
8
|
+
import { cx, ErrorBoundary, ErrorMessage, Loading, rootAttributes, type Slots, slot } from './shared.js';
|
|
9
|
+
import { ArtifactView, renderContext } from './view.js';
|
|
35
10
|
|
|
36
|
-
|
|
37
|
-
|
|
11
|
+
/**
|
|
12
|
+
* What the application receives when the user clicks Save. The library does not store anything: the application
|
|
13
|
+
* decides what saving means (a new version, a database row, a sync to another system, nothing at all).
|
|
14
|
+
*/
|
|
15
|
+
export interface SaveEvent {
|
|
16
|
+
artifactId: string;
|
|
17
|
+
format: string;
|
|
18
|
+
/** Version the draft started from. Pass it back as `baseVersion` when you apply `operations` yourself. */
|
|
19
|
+
baseVersion: number;
|
|
20
|
+
title: string;
|
|
21
|
+
locale: string;
|
|
22
|
+
themeId: string;
|
|
23
|
+
/** Full content of the draft. */
|
|
24
|
+
content: unknown;
|
|
25
|
+
/** Operations applied since `baseVersion`, in order. */
|
|
26
|
+
operations: unknown[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ArtifactEditorPart = 'toolbar' | 'status' | 'body';
|
|
38
30
|
|
|
39
31
|
export interface ArtifactEditorProps {
|
|
40
|
-
|
|
41
|
-
/** Latest known artifact (e.g. from a chat message); newer versions from the cache win. */
|
|
32
|
+
/** Artifact to edit. Its content is the starting point of the draft. */
|
|
42
33
|
artifact?: Artifact;
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
onCommitted?(result: CommitResult): void;
|
|
52
|
-
onError?(error: ArtifactError): void;
|
|
34
|
+
/** Load the artifact by id through the client when `artifact` is not given. */
|
|
35
|
+
id?: string;
|
|
36
|
+
/** Read-only edition: the format editor is hidden and the view is shown instead. */
|
|
37
|
+
readOnly?: boolean;
|
|
38
|
+
/** Save action. Called with the draft; the application persists it and passes the saved artifact back. */
|
|
39
|
+
onSave?(event: SaveEvent): void | Promise<void>;
|
|
40
|
+
/** Called after every change of the draft. */
|
|
41
|
+
onChange?(snapshot: DraftSnapshot): void;
|
|
53
42
|
className?: string;
|
|
54
43
|
slots?: Slots<ArtifactEditorPart>;
|
|
55
44
|
}
|
|
56
45
|
|
|
57
46
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* here, when editing starts.
|
|
47
|
+
* Editor of one artifact (spec §11.6). Edits are applied to a local draft; nothing reaches the server. The Save button
|
|
48
|
+
* emits the draft through `onSave`, and Ctrl/Cmd+S does the same. Undo and redo are local.
|
|
61
49
|
*/
|
|
62
50
|
export function ArtifactEditor(props: ArtifactEditorProps) {
|
|
63
51
|
const ctx = useArtkit();
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const query = useArtifact(id, props.artifact ? { initial: props.artifact } : {});
|
|
67
|
-
const artifact = query.artifact;
|
|
52
|
+
const fetched = useArtifact(props.artifact ? undefined : props.id);
|
|
53
|
+
const artifact = props.artifact ?? fetched.artifact;
|
|
68
54
|
|
|
69
55
|
if (!artifact) {
|
|
70
56
|
return (
|
|
71
57
|
<div className={cx('ak-root ak-editor', props.className)} {...rootAttributes(ctx)}>
|
|
72
|
-
{
|
|
73
|
-
<ErrorMessage error={
|
|
58
|
+
{fetched.error ? (
|
|
59
|
+
<ErrorMessage error={fetched.error} onRetry={() => void fetched.refresh().catch(() => undefined)} />
|
|
74
60
|
) : (
|
|
75
61
|
<Loading />
|
|
76
62
|
)}
|
|
@@ -78,239 +64,131 @@ export function ArtifactEditor(props: ArtifactEditorProps) {
|
|
|
78
64
|
);
|
|
79
65
|
}
|
|
80
66
|
|
|
81
|
-
const editable =
|
|
82
|
-
|
|
83
|
-
ctx.hasEditor(artifact.kind) &&
|
|
84
|
-
ctx.registry.has(artifact.kind);
|
|
85
|
-
if (!editable || props.lockedBy) {
|
|
67
|
+
const editable = !props.readOnly && ctx.hasEditor(artifact.format) && ctx.registry.has(artifact.format);
|
|
68
|
+
if (!editable) {
|
|
86
69
|
return (
|
|
87
70
|
<div
|
|
88
71
|
className={cx('ak-root ak-editor', props.className)}
|
|
89
72
|
{...rootAttributes(ctx, artifact.themeId)}
|
|
90
73
|
data-readonly="true"
|
|
91
74
|
>
|
|
92
|
-
{
|
|
93
|
-
<p {...slot(props.slots, 'notice', 'ak-notice')} role="status">
|
|
94
|
-
{t('lockedBy', { actor: t(`actor_${props.lockedBy.type}` as UiMessageKey) })}
|
|
95
|
-
</p>
|
|
96
|
-
) : null}
|
|
97
|
-
<ArtifactView
|
|
98
|
-
artifact={artifact}
|
|
99
|
-
{...(props.lockedBy && props.partial !== undefined ? { partial: props.partial } : {})}
|
|
100
|
-
/>
|
|
75
|
+
<ArtifactView artifact={artifact} />
|
|
101
76
|
</div>
|
|
102
77
|
);
|
|
103
78
|
}
|
|
104
79
|
return <EditingHost key={artifact.id} {...props} artifact={artifact} />;
|
|
105
80
|
}
|
|
106
81
|
|
|
107
|
-
function EditingHost(
|
|
82
|
+
function EditingHost({
|
|
83
|
+
artifact,
|
|
84
|
+
readOnly,
|
|
85
|
+
onSave,
|
|
86
|
+
onChange,
|
|
87
|
+
className,
|
|
88
|
+
slots,
|
|
89
|
+
}: ArtifactEditorProps & { artifact: Artifact }) {
|
|
108
90
|
const ctx = useArtkit();
|
|
109
|
-
const { t, client, cache, registry } = ctx;
|
|
110
|
-
const callbacks = useRef(props);
|
|
111
|
-
callbacks.current = props;
|
|
112
|
-
const artifactId = props.artifact.id;
|
|
113
|
-
|
|
114
91
|
const [session] = useState(
|
|
115
|
-
() =>
|
|
116
|
-
new EditorSession({
|
|
117
|
-
registry,
|
|
118
|
-
artifact: props.artifact,
|
|
119
|
-
locale: ctx.locale,
|
|
120
|
-
...(props.groupingWindowMs === undefined ? {} : { groupingWindowMs: props.groupingWindowMs }),
|
|
121
|
-
send: (request) => client.apply(artifactId, request),
|
|
122
|
-
load: (version) => cache.load(artifactId, version, version === undefined),
|
|
123
|
-
onCommitted: (result) => {
|
|
124
|
-
cache.put(result.artifact);
|
|
125
|
-
callbacks.current.onCommitted?.(result);
|
|
126
|
-
},
|
|
127
|
-
}),
|
|
92
|
+
() => new DraftSession({ registry: ctx.registry, artifact, locale: ctx.locale }),
|
|
128
93
|
);
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
useEffect(() => {
|
|
133
|
-
session.receive(props.artifact);
|
|
134
|
-
}, [session, props.artifact]);
|
|
135
|
-
|
|
136
|
-
useEffect(() => {
|
|
137
|
-
cache.put(state.confirmed);
|
|
138
|
-
}, [cache, state.confirmed]);
|
|
94
|
+
const snapshot = useSyncExternalStore(session.subscribe, session.getSnapshot, session.getSnapshot);
|
|
95
|
+
const [saving, setSaving] = useState(false);
|
|
96
|
+
const [saveError, setSaveError] = useState<unknown>();
|
|
139
97
|
|
|
140
|
-
//
|
|
141
|
-
useEffect(() => () => session.flush(), [session]);
|
|
142
|
-
|
|
143
|
-
const [live, announce] = useAnnouncer();
|
|
144
|
-
const lastSeq = useRef(0);
|
|
98
|
+
// A newer artifact from the application (after a save, or a reload) moves the draft forward.
|
|
145
99
|
useEffect(() => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
announce(t('savedVersion', { version: event.version }));
|
|
152
|
-
break;
|
|
153
|
-
case 'undone':
|
|
154
|
-
announce(t('undone'));
|
|
155
|
-
break;
|
|
156
|
-
case 'redone':
|
|
157
|
-
announce(t('redone'));
|
|
158
|
-
break;
|
|
159
|
-
case 'rebased':
|
|
160
|
-
announce(t('agentUpdated', { version: event.version }));
|
|
161
|
-
break;
|
|
162
|
-
case 'rejected':
|
|
163
|
-
case 'rolled_back':
|
|
164
|
-
announce(t('rolledBack', { message: errorMessage(event.error, event.error.message) }));
|
|
165
|
-
callbacks.current.onError?.(event.error);
|
|
166
|
-
break;
|
|
167
|
-
case 'conflict':
|
|
168
|
-
announce(t('conflictTitle'));
|
|
169
|
-
break;
|
|
170
|
-
case 'conflict_resolved':
|
|
171
|
-
break;
|
|
100
|
+
if (
|
|
101
|
+
artifact.version !== session.getSnapshot().baseVersion ||
|
|
102
|
+
artifact.id !== session.getSnapshot().artifact.id
|
|
103
|
+
) {
|
|
104
|
+
session.load(artifact);
|
|
172
105
|
}
|
|
173
|
-
}, [
|
|
106
|
+
}, [artifact, session]);
|
|
174
107
|
|
|
175
|
-
const [, forceRender] = useState(0);
|
|
176
108
|
useEffect(() => {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (delay <= 0) return;
|
|
180
|
-
const timer = setTimeout(() => forceRender((n) => n + 1), delay + 10);
|
|
181
|
-
return () => clearTimeout(timer);
|
|
182
|
-
}, [state.lockedUntil]);
|
|
183
|
-
const locked = state.lockedUntil !== undefined && Date.parse(state.lockedUntil) > Date.now();
|
|
184
|
-
const readOnly = state.status === 'conflict' || locked;
|
|
109
|
+
onChange?.(snapshot);
|
|
110
|
+
}, [snapshot, onChange]);
|
|
185
111
|
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
if (event.defaultPrevented || !(event.ctrlKey || event.metaKey) || event.altKey) return;
|
|
189
|
-
const key = event.key.toLowerCase();
|
|
190
|
-
if (key === 'z' && !event.shiftKey) {
|
|
191
|
-
event.preventDefault();
|
|
192
|
-
session.undo();
|
|
193
|
-
} else if ((key === 'z' && event.shiftKey) || key === 'y') {
|
|
194
|
-
event.preventDefault();
|
|
195
|
-
session.redo();
|
|
196
|
-
}
|
|
197
|
-
},
|
|
198
|
-
[session],
|
|
199
|
-
);
|
|
112
|
+
const draft = snapshot.artifact;
|
|
113
|
+
const canSave = Boolean(onSave) && snapshot.dirty && !saving && !readOnly;
|
|
200
114
|
|
|
201
|
-
|
|
202
|
-
(
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
[session],
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
const view = state.view;
|
|
209
|
-
const renderCtx = useMemo(
|
|
210
|
-
() => renderContext(ctx, view.themeId, view.locale, 'screen', false),
|
|
211
|
-
[ctx, view.themeId, view.locale],
|
|
212
|
-
);
|
|
213
|
-
const lint = useMemo<LintIssue[]>(() => {
|
|
115
|
+
async function save() {
|
|
116
|
+
if (!canSave || !onSave) return;
|
|
117
|
+
setSaving(true);
|
|
118
|
+
setSaveError(undefined);
|
|
214
119
|
try {
|
|
215
|
-
|
|
216
|
-
.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
120
|
+
await onSave({
|
|
121
|
+
artifactId: draft.id,
|
|
122
|
+
format: draft.format,
|
|
123
|
+
baseVersion: snapshot.baseVersion,
|
|
124
|
+
title: draft.title,
|
|
125
|
+
locale: draft.locale,
|
|
126
|
+
themeId: draft.themeId,
|
|
127
|
+
content: draft.content,
|
|
128
|
+
operations: snapshot.operations,
|
|
129
|
+
});
|
|
130
|
+
} catch (error) {
|
|
131
|
+
setSaveError(error);
|
|
132
|
+
} finally {
|
|
133
|
+
setSaving(false);
|
|
220
134
|
}
|
|
221
|
-
}
|
|
222
|
-
const emit = useCallback(
|
|
223
|
-
(operations: unknown[], options?: EmitOptions) => {
|
|
224
|
-
session.emit(operations, options);
|
|
225
|
-
},
|
|
226
|
-
[session],
|
|
227
|
-
);
|
|
135
|
+
}
|
|
228
136
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
137
|
+
function onKeyDown(event: KeyboardEvent<HTMLElement>) {
|
|
138
|
+
if (event.defaultPrevented || !(event.metaKey || event.ctrlKey)) return;
|
|
139
|
+
const key = event.key.toLowerCase();
|
|
140
|
+
if (key === 's') {
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
void save();
|
|
143
|
+
} else if (key === 'z' && !event.shiftKey) {
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
session.undo();
|
|
146
|
+
} else if (key === 'y' || (key === 'z' && event.shiftKey)) {
|
|
147
|
+
event.preventDefault();
|
|
148
|
+
session.redo();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const emit = (operations: unknown[], options?: EmitOptions) => {
|
|
153
|
+
void session.apply(operations, { ...(options?.kind ? { kind: options.kind } : {}) });
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const actions: EditorActions = {
|
|
157
|
+
canUndo: snapshot.canUndo,
|
|
158
|
+
canRedo: snapshot.canRedo,
|
|
159
|
+
undo: () => session.undo(),
|
|
160
|
+
redo: () => session.redo(),
|
|
161
|
+
dirty: snapshot.dirty,
|
|
162
|
+
saving,
|
|
163
|
+
canSave,
|
|
164
|
+
save: () => void save(),
|
|
165
|
+
};
|
|
237
166
|
|
|
238
167
|
return (
|
|
168
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: the shortcuts (Ctrl/Cmd+S, Z, Y) cover the whole editor.
|
|
239
169
|
<section
|
|
240
|
-
className={cx('ak-root ak-editor',
|
|
241
|
-
{...rootAttributes(ctx,
|
|
242
|
-
|
|
243
|
-
lang={view.locale}
|
|
170
|
+
className={cx('ak-root ak-editor', className)}
|
|
171
|
+
{...rootAttributes(ctx, draft.themeId)}
|
|
172
|
+
data-dirty={snapshot.dirty || undefined}
|
|
244
173
|
onKeyDown={onKeyDown}
|
|
245
|
-
onBlur={onBlur}
|
|
246
|
-
data-status={state.status}
|
|
247
174
|
>
|
|
248
|
-
{
|
|
249
|
-
<
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
<button
|
|
255
|
-
type="button"
|
|
256
|
-
className="ak-button"
|
|
257
|
-
onClick={session.undo}
|
|
258
|
-
disabled={!state.canUndo}
|
|
259
|
-
aria-keyshortcuts="Control+Z Meta+Z"
|
|
260
|
-
>
|
|
261
|
-
<Undo2 aria-hidden="true" />
|
|
262
|
-
{t('undo')}
|
|
263
|
-
</button>
|
|
264
|
-
<button
|
|
265
|
-
type="button"
|
|
266
|
-
className="ak-button"
|
|
267
|
-
onClick={session.redo}
|
|
268
|
-
disabled={!state.canRedo}
|
|
269
|
-
aria-keyshortcuts="Control+Shift+Z Meta+Shift+Z Control+Y"
|
|
270
|
-
>
|
|
271
|
-
<Redo2 aria-hidden="true" />
|
|
272
|
-
{t('redo')}
|
|
273
|
-
</button>
|
|
274
|
-
{lint.length > 0 ? (
|
|
275
|
-
<span className="ak-badge">{t('lintWarnings', { count: lint.length })}</span>
|
|
276
|
-
) : null}
|
|
277
|
-
<span {...slot(props.slots, 'status', 'ak-editor-status')} aria-hidden="true">
|
|
278
|
-
{statusText}
|
|
279
|
-
</span>
|
|
280
|
-
</div>
|
|
281
|
-
{locked && state.lockedUntil ? (
|
|
282
|
-
<p {...slot(props.slots, 'notice', 'ak-notice')} role="status">
|
|
283
|
-
{t('lockedUntil', { time: formatDate(state.lockedUntil, ctx.locale) })}
|
|
284
|
-
</p>
|
|
285
|
-
) : null}
|
|
286
|
-
{state.error && state.status !== 'conflict' ? (
|
|
287
|
-
<ErrorMessage
|
|
288
|
-
error={state.error}
|
|
289
|
-
onDismiss={session.dismissError}
|
|
290
|
-
{...slot(props.slots, 'error', '')}
|
|
291
|
-
/>
|
|
292
|
-
) : null}
|
|
293
|
-
{state.conflict ? <ConflictBox state={state} session={session} slots={props.slots} /> : null}
|
|
294
|
-
<div {...slot(props.slots, 'body', 'ak-view-body')}>
|
|
295
|
-
<ErrorBoundary
|
|
296
|
-
onReset={() => ctx.resetLoad('editor', props.artifact.kind)}
|
|
297
|
-
fallback={(error, reset) => (
|
|
298
|
-
<>
|
|
299
|
-
<ErrorMessage error={error} onRetry={reset} />
|
|
300
|
-
<KindRenderer kind={props.artifact.kind} content={view.content} ctx={renderCtx} />
|
|
301
|
-
</>
|
|
302
|
-
)}
|
|
303
|
-
>
|
|
304
|
-
<Suspense
|
|
305
|
-
fallback={<KindRenderer kind={props.artifact.kind} content={view.content} ctx={renderCtx} />}
|
|
306
|
-
>
|
|
175
|
+
{snapshot.error ? <ErrorMessage error={snapshot.error} onDismiss={() => session.undo()} /> : null}
|
|
176
|
+
{saveError ? <ErrorMessage error={saveError} /> : null}
|
|
177
|
+
|
|
178
|
+
<div {...slot(slots, 'body', 'ak-editor-body')}>
|
|
179
|
+
<ErrorBoundary fallback={(error) => <ErrorMessage error={error} />}>
|
|
180
|
+
<Suspense fallback={<Loading />}>
|
|
307
181
|
<LoadedEditor
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
182
|
+
slots={slots}
|
|
183
|
+
actions={actions}
|
|
184
|
+
format={draft.format}
|
|
185
|
+
content={draft.content}
|
|
186
|
+
ctx={ctx}
|
|
187
|
+
themeId={draft.themeId}
|
|
188
|
+
locale={draft.locale}
|
|
311
189
|
emit={emit}
|
|
312
|
-
readOnly={readOnly}
|
|
313
|
-
lint={lint}
|
|
190
|
+
readOnly={Boolean(readOnly)}
|
|
191
|
+
lint={snapshot.lint}
|
|
314
192
|
/>
|
|
315
193
|
</Suspense>
|
|
316
194
|
</ErrorBoundary>
|
|
@@ -319,74 +197,96 @@ function EditingHost(props: ArtifactEditorProps & { artifact: Artifact }) {
|
|
|
319
197
|
);
|
|
320
198
|
}
|
|
321
199
|
|
|
322
|
-
function ConflictBox({
|
|
323
|
-
state,
|
|
324
|
-
session,
|
|
325
|
-
slots,
|
|
326
|
-
}: {
|
|
327
|
-
state: EditorSessionState;
|
|
328
|
-
session: EditorSession;
|
|
329
|
-
slots?: Slots<ArtifactEditorPart> | undefined;
|
|
330
|
-
}) {
|
|
331
|
-
const { t } = useArtkit();
|
|
332
|
-
const conflict = state.conflict!;
|
|
333
|
-
const first = useRef<HTMLButtonElement>(null);
|
|
334
|
-
const titleId = useId();
|
|
335
|
-
useEffect(() => {
|
|
336
|
-
first.current?.focus();
|
|
337
|
-
}, []);
|
|
338
|
-
const undoing = conflict.reason === 'undo' || conflict.reason === 'redo';
|
|
339
|
-
return (
|
|
340
|
-
<section {...slot(slots, 'conflict', 'ak-conflict')} role="alert" aria-labelledby={titleId}>
|
|
341
|
-
<h3 className="ak-conflict-title" id={titleId}>
|
|
342
|
-
{t('conflictTitle')}
|
|
343
|
-
</h3>
|
|
344
|
-
<p>{undoing ? t('conflictUndoBody') : t('conflictBody', { version: conflict.latest.version })}</p>
|
|
345
|
-
<ChangeList changes={conflict.changes} />
|
|
346
|
-
<div className="ak-approval-actions">
|
|
347
|
-
<button
|
|
348
|
-
ref={first}
|
|
349
|
-
type="button"
|
|
350
|
-
className="ak-button"
|
|
351
|
-
data-variant="primary"
|
|
352
|
-
onClick={() => session.resolveConflict('keep-theirs')}
|
|
353
|
-
>
|
|
354
|
-
{t('keepTheirs')}
|
|
355
|
-
</button>
|
|
356
|
-
<button type="button" className="ak-button" onClick={() => session.resolveConflict('reapply')}>
|
|
357
|
-
<RotateCcw aria-hidden="true" />
|
|
358
|
-
{t('reapplyMine')}
|
|
359
|
-
</button>
|
|
360
|
-
</div>
|
|
361
|
-
</section>
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
200
|
function LoadedEditor({
|
|
366
|
-
|
|
201
|
+
format,
|
|
367
202
|
content,
|
|
368
|
-
ctx
|
|
203
|
+
ctx,
|
|
204
|
+
themeId,
|
|
205
|
+
locale,
|
|
369
206
|
emit,
|
|
370
207
|
readOnly,
|
|
371
208
|
lint,
|
|
209
|
+
actions,
|
|
210
|
+
slots,
|
|
372
211
|
}: {
|
|
373
|
-
|
|
212
|
+
slots?: Slots<ArtifactEditorPart>;
|
|
213
|
+
actions: EditorActions;
|
|
214
|
+
format: string;
|
|
374
215
|
content: unknown;
|
|
375
|
-
ctx:
|
|
216
|
+
ctx: ArtkitContextValue;
|
|
217
|
+
themeId: string;
|
|
218
|
+
locale: string;
|
|
376
219
|
emit(operations: unknown[], options?: EmitOptions): void;
|
|
377
220
|
readOnly: boolean;
|
|
378
|
-
lint:
|
|
221
|
+
lint: DraftSnapshot['lint'];
|
|
379
222
|
}) {
|
|
380
|
-
const
|
|
381
|
-
const
|
|
223
|
+
const { Editor, css, toolbar } = use(ctx.loadEditor(format));
|
|
224
|
+
const renderCtx = useMemo(
|
|
225
|
+
() => renderContext(ctx, themeId, locale, 'screen', false),
|
|
226
|
+
[ctx, themeId, locale],
|
|
227
|
+
);
|
|
382
228
|
return (
|
|
383
229
|
<>
|
|
384
230
|
{css ? (
|
|
385
|
-
<style href={`artkit-editor-${
|
|
231
|
+
<style href={`artkit-editor-${format}-${hashString(css)}`} precedence="artkit" nonce={ctx.nonce}>
|
|
386
232
|
{css}
|
|
387
233
|
</style>
|
|
388
234
|
) : null}
|
|
389
|
-
|
|
235
|
+
{toolbar === 'format' ? null : <HostToolbar actions={actions} slots={slots} />}
|
|
236
|
+
<Editor
|
|
237
|
+
content={content}
|
|
238
|
+
ctx={renderCtx}
|
|
239
|
+
emit={emit}
|
|
240
|
+
readOnly={readOnly}
|
|
241
|
+
lint={lint}
|
|
242
|
+
actions={actions}
|
|
243
|
+
/>
|
|
390
244
|
</>
|
|
391
245
|
);
|
|
392
246
|
}
|
|
247
|
+
|
|
248
|
+
/** Undo, redo, save status and save, drawn by the host above editors that do not draw their own toolbar. */
|
|
249
|
+
function HostToolbar({ actions, slots }: { actions: EditorActions; slots?: Slots<ArtifactEditorPart> }) {
|
|
250
|
+
const { t } = useArtkit();
|
|
251
|
+
return (
|
|
252
|
+
<header {...slot(slots, 'toolbar', 'ak-editor-toolbar')}>
|
|
253
|
+
<div className="ak-editor-history">
|
|
254
|
+
<button
|
|
255
|
+
type="button"
|
|
256
|
+
className="ak-button"
|
|
257
|
+
data-variant="quiet"
|
|
258
|
+
disabled={!actions.canUndo}
|
|
259
|
+
onClick={actions.undo}
|
|
260
|
+
aria-label={t('undo')}
|
|
261
|
+
title={t('undo')}
|
|
262
|
+
>
|
|
263
|
+
<Undo2 aria-hidden="true" />
|
|
264
|
+
</button>
|
|
265
|
+
<button
|
|
266
|
+
type="button"
|
|
267
|
+
className="ak-button"
|
|
268
|
+
data-variant="quiet"
|
|
269
|
+
disabled={!actions.canRedo}
|
|
270
|
+
onClick={actions.redo}
|
|
271
|
+
aria-label={t('redo')}
|
|
272
|
+
title={t('redo')}
|
|
273
|
+
>
|
|
274
|
+
<Redo2 aria-hidden="true" />
|
|
275
|
+
</button>
|
|
276
|
+
</div>
|
|
277
|
+
<span {...slot(slots, 'status', 'ak-editor-status')} role="status" aria-live="polite">
|
|
278
|
+
{actions.dirty ? t('unsaved') : t('saved')}
|
|
279
|
+
</span>
|
|
280
|
+
<button
|
|
281
|
+
type="button"
|
|
282
|
+
className="ak-button"
|
|
283
|
+
data-variant="primary"
|
|
284
|
+
disabled={!actions.canSave}
|
|
285
|
+
onClick={actions.save}
|
|
286
|
+
>
|
|
287
|
+
<Save aria-hidden="true" />
|
|
288
|
+
{actions.saving ? t('saving') : t('save')}
|
|
289
|
+
</button>
|
|
290
|
+
</header>
|
|
291
|
+
);
|
|
292
|
+
}
|
package/src/components/icons.tsx
CHANGED
|
@@ -8,7 +8,7 @@ export const KIND_ICONS: Record<string, LucideIcon> = {
|
|
|
8
8
|
diagram: Workflow,
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
export function KindIcon({
|
|
12
|
-
const Icon = KIND_ICONS[
|
|
11
|
+
export function KindIcon({ format, className }: { format: string; className?: string }) {
|
|
12
|
+
const Icon = KIND_ICONS[format] ?? File;
|
|
13
13
|
return <Icon aria-hidden="true" focusable="false" className={className} />;
|
|
14
14
|
}
|