@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.
- package/LICENSE +202 -0
- package/README.md +145 -2
- package/dist/cache.d.ts +35 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +116 -0
- package/dist/cache.js.map +1 -0
- package/dist/client.d.ts +79 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +260 -0
- package/dist/client.js.map +1 -0
- package/dist/components/approval.d.ts +34 -0
- package/dist/components/approval.d.ts.map +1 -0
- package/dist/components/approval.js +80 -0
- package/dist/components/approval.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 +28 -0
- package/dist/components/editor.d.ts.map +1 -0
- package/dist/components/editor.js +161 -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 +34 -0
- package/dist/components/panel.d.ts.map +1 -0
- package/dist/components/panel.js +216 -0
- package/dist/components/panel.js.map +1 -0
- package/dist/components/shared.d.ts +71 -0
- package/dist/components/shared.d.ts.map +1 -0
- package/dist/components/shared.js +104 -0
- package/dist/components/shared.js.map +1 -0
- package/dist/components/version-diff.d.ts +35 -0
- package/dist/components/version-diff.d.ts.map +1 -0
- package/dist/components/version-diff.js +85 -0
- package/dist/components/version-diff.js.map +1 -0
- package/dist/components/view.d.ts +40 -0
- package/dist/components/view.d.ts.map +1 -0
- package/dist/components/view.js +75 -0
- package/dist/components/view.js.map +1 -0
- package/dist/context.d.ts +59 -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 +184 -0
- package/dist/editor-session.d.ts.map +1 -0
- package/dist/editor-session.js +614 -0
- package/dist/editor-session.js.map +1 -0
- package/dist/hooks.d.ts +85 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +297 -0
- package/dist/hooks.js.map +1 -0
- package/dist/i18n.d.ts +197 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +207 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -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/line-diff.d.ts +25 -0
- package/dist/line-diff.d.ts.map +1 -0
- package/dist/line-diff.js +96 -0
- package/dist/line-diff.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 +132 -0
- package/src/client.ts +390 -0
- package/src/components/approval.tsx +219 -0
- package/src/components/card.tsx +87 -0
- package/src/components/editor.tsx +392 -0
- package/src/components/icons.tsx +14 -0
- package/src/components/panel.tsx +591 -0
- package/src/components/shared.tsx +207 -0
- package/src/components/version-diff.tsx +266 -0
- package/src/components/view.tsx +199 -0
- package/src/context.ts +82 -0
- package/src/controls.tsx +587 -0
- package/src/download.ts +23 -0
- package/src/editor-session.ts +738 -0
- package/src/hooks.ts +411 -0
- package/src/i18n.ts +217 -0
- package/src/index.ts +160 -0
- package/src/kit-css.ts +5 -0
- package/src/line-diff.ts +111 -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,392 @@
|
|
|
1
|
+
import type { Actor, Artifact, ArtifactError, CommitResult, EmitOptions, LintIssue } from '@artkit-ai/core';
|
|
2
|
+
import { Redo2, RotateCcw, Undo2 } from 'lucide-react';
|
|
3
|
+
import {
|
|
4
|
+
type FocusEvent,
|
|
5
|
+
type KeyboardEvent,
|
|
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';
|
|
18
|
+
import { useArtifact } from '../hooks.js';
|
|
19
|
+
import type { UiMessageKey } from '../i18n.js';
|
|
20
|
+
import { hashString } from '../styles.js';
|
|
21
|
+
import {
|
|
22
|
+
ChangeList,
|
|
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';
|
|
35
|
+
|
|
36
|
+
export type EditorPermission = 'read' | 'edit';
|
|
37
|
+
export type ArtifactEditorPart = 'toolbar' | 'status' | 'body' | 'conflict' | 'error' | 'notice';
|
|
38
|
+
|
|
39
|
+
export interface ArtifactEditorProps {
|
|
40
|
+
id?: string;
|
|
41
|
+
/** Latest known artifact (e.g. from a chat message); newer versions from the cache win. */
|
|
42
|
+
artifact?: Artifact;
|
|
43
|
+
/** Set by the application (spec §13.6). `comment` arrives in phase 3. Default `edit`. */
|
|
44
|
+
permission?: EditorPermission;
|
|
45
|
+
/** Another actor holds the lock: the editor turns read-only and shows `partial` as a streaming preview. */
|
|
46
|
+
lockedBy?: Actor | null;
|
|
47
|
+
/** Streamed content shown while locked (see `ArtifactView`). */
|
|
48
|
+
partial?: unknown;
|
|
49
|
+
/** Typing batches are grouped within this window (default 600 ms). */
|
|
50
|
+
groupingWindowMs?: number;
|
|
51
|
+
onCommitted?(result: CommitResult): void;
|
|
52
|
+
onError?(error: ArtifactError): void;
|
|
53
|
+
className?: string;
|
|
54
|
+
slots?: Slots<ArtifactEditorPart>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Kind editor host (spec §13.1, §13.6). Falls back to `ArtifactView` when the kind has no editor loader,
|
|
59
|
+
* when the permission is `read` or while another actor holds the lock. The editor module is only loaded
|
|
60
|
+
* here, when editing starts.
|
|
61
|
+
*/
|
|
62
|
+
export function ArtifactEditor(props: ArtifactEditorProps) {
|
|
63
|
+
const ctx = useArtkit();
|
|
64
|
+
const { t } = ctx;
|
|
65
|
+
const id = props.id ?? props.artifact?.id;
|
|
66
|
+
const query = useArtifact(id, props.artifact ? { initial: props.artifact } : {});
|
|
67
|
+
const artifact = query.artifact;
|
|
68
|
+
|
|
69
|
+
if (!artifact) {
|
|
70
|
+
return (
|
|
71
|
+
<div className={cx('ak-root ak-editor', props.className)} {...rootAttributes(ctx)}>
|
|
72
|
+
{query.error ? (
|
|
73
|
+
<ErrorMessage error={query.error} onRetry={() => void query.refresh().catch(() => undefined)} />
|
|
74
|
+
) : (
|
|
75
|
+
<Loading />
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const editable =
|
|
82
|
+
(props.permission ?? 'edit') === 'edit' &&
|
|
83
|
+
ctx.hasEditor(artifact.kind) &&
|
|
84
|
+
ctx.registry.has(artifact.kind);
|
|
85
|
+
if (!editable || props.lockedBy) {
|
|
86
|
+
return (
|
|
87
|
+
<div
|
|
88
|
+
className={cx('ak-root ak-editor', props.className)}
|
|
89
|
+
{...rootAttributes(ctx, artifact.themeId)}
|
|
90
|
+
data-readonly="true"
|
|
91
|
+
>
|
|
92
|
+
{props.lockedBy ? (
|
|
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
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return <EditingHost key={artifact.id} {...props} artifact={artifact} />;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function EditingHost(props: ArtifactEditorProps & { artifact: Artifact }) {
|
|
108
|
+
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
|
+
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
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
const state = useSyncExternalStore(session.subscribe, session.getSnapshot, session.getSnapshot);
|
|
130
|
+
|
|
131
|
+
// Newer versions from the cache or props (agent commits, refetches) are rebased into the session.
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
session.receive(props.artifact);
|
|
134
|
+
}, [session, props.artifact]);
|
|
135
|
+
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
cache.put(state.confirmed);
|
|
138
|
+
}, [cache, state.confirmed]);
|
|
139
|
+
|
|
140
|
+
// Unmounting must not lose the last keystrokes still waiting in the grouping window.
|
|
141
|
+
useEffect(() => () => session.flush(), [session]);
|
|
142
|
+
|
|
143
|
+
const [live, announce] = useAnnouncer();
|
|
144
|
+
const lastSeq = useRef(0);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const event = state.event;
|
|
147
|
+
if (!event || event.seq === lastSeq.current) return;
|
|
148
|
+
lastSeq.current = event.seq;
|
|
149
|
+
switch (event.type) {
|
|
150
|
+
case 'saved':
|
|
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;
|
|
172
|
+
}
|
|
173
|
+
}, [state.event, announce, t]);
|
|
174
|
+
|
|
175
|
+
const [, forceRender] = useState(0);
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (!state.lockedUntil) return;
|
|
178
|
+
const delay = Date.parse(state.lockedUntil) - Date.now();
|
|
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;
|
|
185
|
+
|
|
186
|
+
const onKeyDown = useCallback(
|
|
187
|
+
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
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
|
+
);
|
|
200
|
+
|
|
201
|
+
const onBlur = useCallback(
|
|
202
|
+
(event: FocusEvent<HTMLDivElement>) => {
|
|
203
|
+
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) session.flush();
|
|
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[]>(() => {
|
|
214
|
+
try {
|
|
215
|
+
return registry
|
|
216
|
+
.get(props.artifact.kind)
|
|
217
|
+
.lint(view.content, { locale: view.locale, theme: ctx.themeFor(view.themeId) });
|
|
218
|
+
} catch {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
}, [ctx, registry, props.artifact.kind, view]);
|
|
222
|
+
const emit = useCallback(
|
|
223
|
+
(operations: unknown[], options?: EmitOptions) => {
|
|
224
|
+
session.emit(operations, options);
|
|
225
|
+
},
|
|
226
|
+
[session],
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const statusText =
|
|
230
|
+
state.status === 'saving'
|
|
231
|
+
? t('saving')
|
|
232
|
+
: state.status === 'editing'
|
|
233
|
+
? t('unsaved')
|
|
234
|
+
: state.status === 'conflict'
|
|
235
|
+
? t('conflictTitle')
|
|
236
|
+
: t('saved');
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<section
|
|
240
|
+
className={cx('ak-root ak-editor', props.className)}
|
|
241
|
+
{...rootAttributes(ctx, view.themeId)}
|
|
242
|
+
aria-label={t('editorLabel', { title: view.title })}
|
|
243
|
+
lang={view.locale}
|
|
244
|
+
onKeyDown={onKeyDown}
|
|
245
|
+
onBlur={onBlur}
|
|
246
|
+
data-status={state.status}
|
|
247
|
+
>
|
|
248
|
+
{live}
|
|
249
|
+
<div
|
|
250
|
+
{...slot(props.slots, 'toolbar', 'ak-editor-toolbar')}
|
|
251
|
+
role="toolbar"
|
|
252
|
+
aria-label={t('editorLabel', { title: view.title })}
|
|
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
|
+
>
|
|
307
|
+
<LoadedEditor
|
|
308
|
+
kind={props.artifact.kind}
|
|
309
|
+
content={view.content}
|
|
310
|
+
ctx={renderCtx}
|
|
311
|
+
emit={emit}
|
|
312
|
+
readOnly={readOnly}
|
|
313
|
+
lint={lint}
|
|
314
|
+
/>
|
|
315
|
+
</Suspense>
|
|
316
|
+
</ErrorBoundary>
|
|
317
|
+
</div>
|
|
318
|
+
</section>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
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
|
+
function LoadedEditor({
|
|
366
|
+
kind,
|
|
367
|
+
content,
|
|
368
|
+
ctx: renderCtx,
|
|
369
|
+
emit,
|
|
370
|
+
readOnly,
|
|
371
|
+
lint,
|
|
372
|
+
}: {
|
|
373
|
+
kind: string;
|
|
374
|
+
content: unknown;
|
|
375
|
+
ctx: ReturnType<typeof renderContext>;
|
|
376
|
+
emit(operations: unknown[], options?: EmitOptions): void;
|
|
377
|
+
readOnly: boolean;
|
|
378
|
+
lint: LintIssue[];
|
|
379
|
+
}) {
|
|
380
|
+
const ctx = useArtkit();
|
|
381
|
+
const { Editor, css } = use(ctx.loadEditor(kind));
|
|
382
|
+
return (
|
|
383
|
+
<>
|
|
384
|
+
{css ? (
|
|
385
|
+
<style href={`artkit-editor-${kind}-${hashString(css)}`} precedence="artkit" nonce={ctx.nonce}>
|
|
386
|
+
{css}
|
|
387
|
+
</style>
|
|
388
|
+
) : null}
|
|
389
|
+
<Editor content={content} ctx={renderCtx} emit={emit} readOnly={readOnly} lint={lint} />
|
|
390
|
+
</>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { File, FileText, type LucideIcon, Presentation, Sheet, Workflow } from 'lucide-react';
|
|
2
|
+
|
|
3
|
+
/** Lucide icon per native kind; unknown kinds get a generic file. */
|
|
4
|
+
export const KIND_ICONS: Record<string, LucideIcon> = {
|
|
5
|
+
document: FileText,
|
|
6
|
+
sheet: Sheet,
|
|
7
|
+
slides: Presentation,
|
|
8
|
+
diagram: Workflow,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function KindIcon({ kind, className }: { kind: string; className?: string }) {
|
|
12
|
+
const Icon = KIND_ICONS[kind] ?? File;
|
|
13
|
+
return <Icon aria-hidden="true" focusable="false" className={className} />;
|
|
14
|
+
}
|