@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/hooks.ts ADDED
@@ -0,0 +1,411 @@
1
+ import {
2
+ type AnyArtifactExporter,
3
+ type Artifact,
4
+ ArtifactError,
5
+ type ChangeSummary,
6
+ type CommitResult,
7
+ isArtifactError,
8
+ silentLogger,
9
+ slugify,
10
+ type VersionEntry,
11
+ } from '@artkit-ai/core';
12
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
13
+ import type { CacheEntry } from './cache.js';
14
+ import type { ClientExportResult } from './client.js';
15
+ import { useArtkit } from './context.js';
16
+ import { triggerDownload } from './download.js';
17
+
18
+ const NO_ENTRY: CacheEntry = Object.freeze({ loading: false }) as CacheEntry;
19
+ const noop = () => {};
20
+ const noSubscription = () => noop;
21
+
22
+ function toArtifactError(error: unknown): ArtifactError {
23
+ if (isArtifactError(error)) return error;
24
+ return new ArtifactError('INTERNAL_ERROR', 'Internal error while processing the request.', {
25
+ cause: error,
26
+ });
27
+ }
28
+
29
+ function newKey(): string {
30
+ const cryptoApi = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;
31
+ return (
32
+ cryptoApi?.randomUUID?.() ?? `ak-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
33
+ );
34
+ }
35
+
36
+ export interface UseArtifactOptions {
37
+ /** A specific (immutable) version; omit for the latest. */
38
+ version?: number;
39
+ /** Already-known artifact (e.g. from a chat message), shown immediately and cached. */
40
+ initial?: Artifact;
41
+ /** Revalidate the latest version on mount (default true). */
42
+ revalidate?: boolean;
43
+ }
44
+
45
+ export interface UseArtifactResult {
46
+ artifact: Artifact | undefined;
47
+ error: unknown;
48
+ loading: boolean;
49
+ /** Fetches the latest version again (specific versions never change). */
50
+ refresh(): Promise<Artifact | undefined>;
51
+ }
52
+
53
+ /** Loads an artifact through the client with a shared cache (spec §13.2). Pass undefined to skip. */
54
+ export function useArtifact(id: string | undefined, options: UseArtifactOptions = {}): UseArtifactResult {
55
+ const { cache } = useArtkit();
56
+ const { version, initial, revalidate = true } = options;
57
+ const subscribe = useCallback(
58
+ (listener: () => void) => (id ? cache.subscribe(id, listener) : noop),
59
+ [cache, id],
60
+ );
61
+ const read = () => (id ? cache.entry(id, version) : NO_ENTRY);
62
+ const entry = useSyncExternalStore(id ? subscribe : noSubscription, read, read);
63
+
64
+ useEffect(() => {
65
+ if (initial && initial.id === id) cache.put(initial);
66
+ }, [cache, id, initial]);
67
+
68
+ useEffect(() => {
69
+ if (!id) return;
70
+ cache.load(id, version, version === undefined && revalidate).catch(noop);
71
+ }, [cache, id, version, revalidate]);
72
+
73
+ const usableInitial =
74
+ initial && initial.id === id && (version === undefined || initial.version === version)
75
+ ? initial
76
+ : undefined;
77
+ const artifact =
78
+ entry.artifact && (!usableInitial || entry.artifact.version >= usableInitial.version)
79
+ ? entry.artifact
80
+ : (usableInitial ?? entry.artifact);
81
+ const refresh = useCallback(async () => {
82
+ if (!id) return undefined;
83
+ return cache.load(id, version, true);
84
+ }, [cache, id, version]);
85
+
86
+ return {
87
+ artifact,
88
+ error: artifact ? undefined : entry.error,
89
+ loading: !artifact && !entry.error && id !== undefined,
90
+ refresh,
91
+ };
92
+ }
93
+
94
+ /** Latest known version of an artifact in the cache, updated when commits arrive. */
95
+ export function useLatestVersion(id: string | undefined): number | undefined {
96
+ const { cache } = useArtkit();
97
+ const subscribe = useCallback(
98
+ (listener: () => void) => (id ? cache.subscribe(id, listener) : noop),
99
+ [cache, id],
100
+ );
101
+ const read = () => (id ? cache.latest(id)?.version : undefined);
102
+ return useSyncExternalStore(subscribe, read, read);
103
+ }
104
+
105
+ export interface UseArtifactHistoryResult {
106
+ entries: VersionEntry[];
107
+ loading: boolean;
108
+ error: unknown;
109
+ hasMore: boolean;
110
+ loadMore(): Promise<void>;
111
+ refresh(): Promise<void>;
112
+ /** Creates a new version with the content of `version` (spec §8.5). */
113
+ restore(version: number, message?: string): Promise<CommitResult>;
114
+ restoring: number | undefined;
115
+ }
116
+
117
+ /** Version list with pagination and restore (spec §13.2). Refetches when a newer version is known. */
118
+ export function useArtifactHistory(
119
+ id: string | undefined,
120
+ options: { pageSize?: number } = {},
121
+ ): UseArtifactHistoryResult {
122
+ const { client, cache } = useArtkit();
123
+ const pageSize = options.pageSize ?? 20;
124
+ const latest = useLatestVersion(id);
125
+ const [entries, setEntries] = useState<VersionEntry[]>([]);
126
+ const [cursor, setCursor] = useState<string | undefined>(undefined);
127
+ const [loading, setLoading] = useState(Boolean(id));
128
+ const [error, setError] = useState<unknown>(undefined);
129
+ const [restoring, setRestoring] = useState<number | undefined>(undefined);
130
+ const request = useRef(0);
131
+
132
+ const refresh = useCallback(async () => {
133
+ if (!id) return;
134
+ const ticket = ++request.current;
135
+ setLoading(true);
136
+ try {
137
+ const page = await client.history(id, { limit: pageSize });
138
+ if (ticket !== request.current) return;
139
+ setEntries(page.items);
140
+ setCursor(page.nextCursor);
141
+ setError(undefined);
142
+ } catch (caught) {
143
+ if (ticket === request.current) setError(caught);
144
+ } finally {
145
+ if (ticket === request.current) setLoading(false);
146
+ }
147
+ }, [client, id, pageSize]);
148
+
149
+ // biome-ignore lint/correctness/useExhaustiveDependencies: `latest` is the revalidation trigger.
150
+ useEffect(() => {
151
+ void refresh();
152
+ }, [refresh, latest]);
153
+
154
+ const loadMore = useCallback(async () => {
155
+ if (!id || !cursor) return;
156
+ const ticket = ++request.current;
157
+ setLoading(true);
158
+ try {
159
+ const page = await client.history(id, { limit: pageSize, cursor });
160
+ if (ticket !== request.current) return;
161
+ setEntries((previous) => {
162
+ const seen = new Set(previous.map((entry) => entry.version));
163
+ return [...previous, ...page.items.filter((entry) => !seen.has(entry.version))];
164
+ });
165
+ setCursor(page.nextCursor);
166
+ } catch (caught) {
167
+ if (ticket === request.current) setError(caught);
168
+ } finally {
169
+ if (ticket === request.current) setLoading(false);
170
+ }
171
+ }, [client, id, cursor, pageSize]);
172
+
173
+ const restore = useCallback(
174
+ async (version: number, message?: string) => {
175
+ if (!id) throw new ArtifactError('NOT_FOUND', 'No artifact id.');
176
+ setRestoring(version);
177
+ try {
178
+ const result = await client.restore(id, version, message);
179
+ cache.put(result.artifact);
180
+ return result;
181
+ } finally {
182
+ setRestoring(undefined);
183
+ }
184
+ },
185
+ [client, cache, id],
186
+ );
187
+
188
+ return { entries, loading, error, hasMore: cursor !== undefined, loadMore, refresh, restore, restoring };
189
+ }
190
+
191
+ export interface ApplyConflict {
192
+ operations: unknown[];
193
+ baseVersion: number;
194
+ currentVersion?: number;
195
+ changes: ChangeSummary[];
196
+ }
197
+
198
+ export interface ApplyOptions {
199
+ /** Defaults to the latest cached version (loaded when unknown). */
200
+ baseVersion?: number;
201
+ idempotencyKey?: string;
202
+ message?: string;
203
+ }
204
+
205
+ export interface UseApplyOperationsResult {
206
+ apply(operations: unknown[], options?: ApplyOptions): Promise<CommitResult>;
207
+ pending: boolean;
208
+ error: ArtifactError | undefined;
209
+ /** Set on VERSION_CONFLICT: what changed since the base, and the operations that were refused. */
210
+ conflict: ApplyConflict | undefined;
211
+ /** Sends the refused operations again on the latest version. */
212
+ retry(): Promise<CommitResult>;
213
+ reset(): void;
214
+ }
215
+
216
+ /** Sends operations with `baseVersion` and surfaces conflicts (spec §13.2). */
217
+ export function useApplyOperations(id: string): UseApplyOperationsResult {
218
+ const { client, cache } = useArtkit();
219
+ const [state, setState] = useState<{ pending: boolean; error?: ArtifactError; conflict?: ApplyConflict }>({
220
+ pending: false,
221
+ });
222
+ const conflictRef = useRef<ApplyConflict | undefined>(undefined);
223
+
224
+ const apply = useCallback(
225
+ async (operations: unknown[], options: ApplyOptions = {}) => {
226
+ setState({ pending: true });
227
+ let baseVersion = options.baseVersion;
228
+ try {
229
+ baseVersion ??= cache.latest(id)?.version ?? (await cache.load(id)).version;
230
+ const result = await client.apply(id, {
231
+ baseVersion,
232
+ operations,
233
+ idempotencyKey: options.idempotencyKey ?? newKey(),
234
+ ...(options.message ? { message: options.message } : {}),
235
+ });
236
+ cache.put(result.artifact);
237
+ conflictRef.current = undefined;
238
+ setState({ pending: false });
239
+ return result;
240
+ } catch (caught) {
241
+ const error = toArtifactError(caught);
242
+ let conflict: ApplyConflict | undefined;
243
+ if (error.code === 'VERSION_CONFLICT') {
244
+ const changes = error.details.changesSinceBase;
245
+ const current = error.details.currentVersion;
246
+ conflict = {
247
+ operations,
248
+ baseVersion: baseVersion ?? 0,
249
+ ...(typeof current === 'number' ? { currentVersion: current } : {}),
250
+ changes: Array.isArray(changes) ? (changes as ChangeSummary[]) : [],
251
+ };
252
+ cache.load(id, undefined, true).catch(noop);
253
+ }
254
+ conflictRef.current = conflict;
255
+ setState({ pending: false, error, ...(conflict ? { conflict } : {}) });
256
+ throw error;
257
+ }
258
+ },
259
+ [client, cache, id],
260
+ );
261
+
262
+ const retry = useCallback(async () => {
263
+ const conflict = conflictRef.current;
264
+ if (!conflict) throw new ArtifactError('NOT_FOUND', 'There is no conflicting batch to retry.');
265
+ const latest = await cache.load(id, undefined, true);
266
+ return apply(conflict.operations, { baseVersion: latest.version });
267
+ }, [apply, cache, id]);
268
+
269
+ const reset = useCallback(() => {
270
+ conflictRef.current = undefined;
271
+ setState({ pending: false });
272
+ }, []);
273
+
274
+ return { apply, pending: state.pending, error: state.error, conflict: state.conflict, retry, reset };
275
+ }
276
+
277
+ export interface ExportOptions {
278
+ version?: number;
279
+ /** Triggers a browser download (default true). */
280
+ download?: boolean;
281
+ options?: Record<string, unknown>;
282
+ /** `auto` (default) exports in the browser when a browser-capable exporter is loaded. */
283
+ prefer?: 'auto' | 'browser' | 'server';
284
+ signal?: AbortSignal;
285
+ }
286
+
287
+ export interface UseArtifactExportResult {
288
+ exportArtifact(format: string, options?: ExportOptions): Promise<ClientExportResult>;
289
+ pending: string | undefined;
290
+ error: ArtifactError | undefined;
291
+ warnings: string[];
292
+ /** Formats available for the artifact's kind (server formats plus browser exporters). */
293
+ formats: string[];
294
+ /** Loads the kind's browser exporters so `formats` includes them. */
295
+ loadFormats(): Promise<void>;
296
+ }
297
+
298
+ /**
299
+ * Exports in the browser when a loaded exporter with runtime `any`/`browser` handles the format, else
300
+ * through the client (spec §13.2), and triggers a download.
301
+ */
302
+ export function useArtifactExport(id: string, kind?: string): UseArtifactExportResult {
303
+ const ctx = useArtkit();
304
+ const { client, cache, registry } = ctx;
305
+ const [pending, setPending] = useState<string | undefined>(undefined);
306
+ const [error, setError] = useState<ArtifactError | undefined>(undefined);
307
+ const [warnings, setWarnings] = useState<string[]>([]);
308
+ const [loaded, setLoaded] = useState<AnyArtifactExporter[]>([]);
309
+ const knownKind = kind ?? cache.latest(id)?.kind;
310
+
311
+ const browserExporters = useCallback(
312
+ async (artifactKind: string): Promise<AnyArtifactExporter[]> => {
313
+ const fromRegistry = registry.exporters(artifactKind).filter((exporter) => exporter.runtime !== 'node');
314
+ if (!ctx.hasExporters(artifactKind)) return fromRegistry;
315
+ const lazy = (await ctx.loadExporters(artifactKind)).filter(
316
+ (exporter) => exporter.kind === artifactKind && exporter.runtime !== 'node',
317
+ );
318
+ return [...lazy, ...fromRegistry];
319
+ },
320
+ [ctx, registry],
321
+ );
322
+
323
+ const loadFormats = useCallback(async () => {
324
+ if (!knownKind) return;
325
+ try {
326
+ setLoaded(await browserExporters(knownKind));
327
+ } catch {
328
+ // Without browser exporters the server formats still work.
329
+ }
330
+ }, [browserExporters, knownKind]);
331
+
332
+ const formats = useMemo(() => {
333
+ if (!knownKind) return [];
334
+ const set = new Set<string>(ctx.exportFormats[knownKind] ?? []);
335
+ for (const exporter of registry.exporters(knownKind))
336
+ if (exporter.runtime !== 'node') set.add(exporter.format);
337
+ for (const exporter of loaded) set.add(exporter.format);
338
+ return [...set];
339
+ }, [ctx.exportFormats, knownKind, loaded, registry]);
340
+
341
+ const exportArtifact = useCallback(
342
+ async (format: string, options: ExportOptions = {}) => {
343
+ setPending(format);
344
+ setError(undefined);
345
+ setWarnings([]);
346
+ try {
347
+ const artifact =
348
+ options.version === undefined
349
+ ? (cache.latest(id) ?? (await cache.load(id)))
350
+ : await cache.load(id, options.version);
351
+ let result: ClientExportResult | undefined;
352
+ if (options.prefer !== 'server') {
353
+ const exporter = (await browserExporters(artifact.kind).catch(() => [])).find(
354
+ (candidate) => candidate.format === format,
355
+ );
356
+ if (exporter) {
357
+ const collected: string[] = [];
358
+ let bytes: Uint8Array;
359
+ try {
360
+ bytes = await exporter.export(artifact.content, {
361
+ title: artifact.title,
362
+ theme: ctx.themeFor(artifact.themeId),
363
+ locale: artifact.locale,
364
+ assets: ctx.assets ?? {
365
+ resolve: (ref) =>
366
+ Promise.reject(
367
+ new ArtifactError('NOT_FOUND', `Asset not available in the browser: ${ref}.`),
368
+ ),
369
+ },
370
+ logger: silentLogger,
371
+ warn: (message) => collected.push(message),
372
+ ...(options.signal ? { signal: options.signal } : {}),
373
+ ...(options.options ? { options: options.options } : {}),
374
+ });
375
+ } catch (cause) {
376
+ throw isArtifactError(cause)
377
+ ? cause
378
+ : new ArtifactError('EXPORT_FAILED', `Failed to export to ${format}.`, { cause });
379
+ }
380
+ result = {
381
+ bytes,
382
+ mimeType: exporter.mimeType,
383
+ filename: `${slugify(artifact.title)}.${exporter.extension}`,
384
+ warnings: collected,
385
+ };
386
+ } else if (options.prefer === 'browser') {
387
+ throw new ArtifactError('EXPORT_FAILED', `No browser exporter for format ${format}.`);
388
+ }
389
+ }
390
+ result ??= await client.export(id, {
391
+ format,
392
+ ...(options.version === undefined ? {} : { version: options.version }),
393
+ ...(options.options ? { options: options.options } : {}),
394
+ ...(options.signal ? { signal: options.signal } : {}),
395
+ });
396
+ setWarnings(result.warnings);
397
+ if (options.download !== false) triggerDownload(result);
398
+ return result;
399
+ } catch (caught) {
400
+ const failure = toArtifactError(caught);
401
+ setError(failure);
402
+ throw failure;
403
+ } finally {
404
+ setPending(undefined);
405
+ }
406
+ },
407
+ [browserExporters, cache, client, ctx, id],
408
+ );
409
+
410
+ return { exportArtifact, pending, error, warnings, formats, loadFormats };
411
+ }
package/src/i18n.ts ADDED
@@ -0,0 +1,217 @@
1
+ /** UI strings in pt-BR and en (spec §18.1). Templates use `{name}` placeholders. */
2
+ export const uiMessages = {
3
+ en: {
4
+ loading: 'Loading…',
5
+ generating: 'Generating…',
6
+ provisional: 'Preview — not saved yet',
7
+ noRenderer: 'No renderer is registered for kind "{kind}". Showing the raw content.',
8
+ loadFailed: 'Could not load this artifact.',
9
+ retry: 'Retry',
10
+ close: 'Close',
11
+ open: 'Open',
12
+ openTitle: 'Open {title}',
13
+ version: 'Version',
14
+ versionShort: 'v{version}',
15
+ latest: 'latest',
16
+ tabView: 'View',
17
+ tabCode: 'Code',
18
+ tabHistory: 'History',
19
+ tabsLabel: 'Artifact sections',
20
+ copy: 'Copy',
21
+ copied: 'Copied',
22
+ export: 'Export',
23
+ exportAs: 'Export as {format}',
24
+ exporting: 'Exporting…',
25
+ exported: 'Exported {filename}',
26
+ exportWarnings: 'Export finished with warnings:',
27
+ noExportFormats: 'No export formats available',
28
+ status: 'Status',
29
+ status_draft: 'Draft',
30
+ status_pending_approval: 'Pending approval',
31
+ status_published: 'Published',
32
+ status_archived: 'Archived',
33
+ action_submit: 'Submit for approval',
34
+ action_approve: 'Approve',
35
+ action_reject: 'Reject',
36
+ action_publish: 'Publish',
37
+ action_archive: 'Archive',
38
+ statusChanged: 'Status changed to {status}',
39
+ rejectReason: 'Reason for rejection',
40
+ rejectReasonRequired: 'Explain why the version is being rejected.',
41
+ confirmReject: 'Confirm rejection',
42
+ cancel: 'Cancel',
43
+ approvalTitle: 'Review version {version}',
44
+ approvalComparedTo: 'Compared with version {version}',
45
+ approvalFirstVersion: 'First version: nothing to compare with.',
46
+ notPending: 'This artifact is not waiting for approval.',
47
+ approved: 'Version {version} approved',
48
+ rejected: 'Version {version} rejected',
49
+ historyEmpty: 'No versions yet.',
50
+ loadMore: 'Load more',
51
+ restore: 'Restore',
52
+ restoreVersion: 'Restore version {version}',
53
+ restored: 'Version {version} restored as version {newVersion}',
54
+ viewVersion: 'View version {version}',
55
+ restoredFrom: 'restored from v{version}',
56
+ by: 'by {actor}',
57
+ actor_agent: 'agent',
58
+ actor_user: 'user',
59
+ actor_system: 'system',
60
+ changes: 'Changes',
61
+ noChanges: 'No changes.',
62
+ change_added: 'Added',
63
+ change_removed: 'Removed',
64
+ change_changed: 'Changed',
65
+ change_moved: 'Moved',
66
+ diffUnified: 'Highlighted',
67
+ diffSplit: 'Side by side',
68
+ diffViewLabel: 'Diff layout',
69
+ diffBefore: 'Version {version}',
70
+ diffAfter: 'Version {version}',
71
+ lineAdded: 'added',
72
+ lineRemoved: 'removed',
73
+ undo: 'Undo',
74
+ redo: 'Redo',
75
+ undone: 'Change undone',
76
+ redone: 'Change redone',
77
+ saving: 'Saving…',
78
+ saved: 'Saved',
79
+ savedVersion: 'Saved version {version}',
80
+ unsaved: 'Unsaved changes',
81
+ rolledBack: 'Change rejected: {message}',
82
+ dismiss: 'Dismiss',
83
+ readOnly: 'Read only',
84
+ lockedBy: 'Locked by the {actor} — editing is disabled.',
85
+ lockedUntil: 'Locked until {time} — editing is disabled.',
86
+ agentUpdated: 'The artifact was updated to version {version}',
87
+ conflictTitle: 'Your edit conflicts with a newer version',
88
+ conflictBody: 'Version {version} changed the same items you edited.',
89
+ conflictUndoBody: 'The artifact changed after your edit, so undoing it would also discard those changes.',
90
+ keepTheirs: 'Keep agent version',
91
+ reapplyMine: 'Reapply my edit',
92
+ editorLabel: 'Editor for {title}',
93
+ lintWarnings: '{count} lint warnings',
94
+ },
95
+ 'pt-BR': {
96
+ loading: 'Carregando…',
97
+ generating: 'Gerando…',
98
+ provisional: 'Prévia — ainda não salva',
99
+ noRenderer: 'Não há renderer registrado para o tipo "{kind}". Exibindo o conteúdo bruto.',
100
+ loadFailed: 'Não foi possível carregar este artefato.',
101
+ retry: 'Tentar novamente',
102
+ close: 'Fechar',
103
+ open: 'Abrir',
104
+ openTitle: 'Abrir {title}',
105
+ version: 'Versão',
106
+ versionShort: 'v{version}',
107
+ latest: 'atual',
108
+ tabView: 'Visualização',
109
+ tabCode: 'Código',
110
+ tabHistory: 'Histórico',
111
+ tabsLabel: 'Seções do artefato',
112
+ copy: 'Copiar',
113
+ copied: 'Copiado',
114
+ export: 'Exportar',
115
+ exportAs: 'Exportar como {format}',
116
+ exporting: 'Exportando…',
117
+ exported: '{filename} exportado',
118
+ exportWarnings: 'Exportação concluída com avisos:',
119
+ noExportFormats: 'Nenhum formato de exportação disponível',
120
+ status: 'Status',
121
+ status_draft: 'Rascunho',
122
+ status_pending_approval: 'Aguardando aprovação',
123
+ status_published: 'Publicado',
124
+ status_archived: 'Arquivado',
125
+ action_submit: 'Enviar para aprovação',
126
+ action_approve: 'Aprovar',
127
+ action_reject: 'Rejeitar',
128
+ action_publish: 'Publicar',
129
+ action_archive: 'Arquivar',
130
+ statusChanged: 'Status alterado para {status}',
131
+ rejectReason: 'Motivo da rejeição',
132
+ rejectReasonRequired: 'Explique por que a versão está sendo rejeitada.',
133
+ confirmReject: 'Confirmar rejeição',
134
+ cancel: 'Cancelar',
135
+ approvalTitle: 'Revisar versão {version}',
136
+ approvalComparedTo: 'Comparada com a versão {version}',
137
+ approvalFirstVersion: 'Primeira versão: não há com o que comparar.',
138
+ notPending: 'Este artefato não está aguardando aprovação.',
139
+ approved: 'Versão {version} aprovada',
140
+ rejected: 'Versão {version} rejeitada',
141
+ historyEmpty: 'Ainda não há versões.',
142
+ loadMore: 'Carregar mais',
143
+ restore: 'Restaurar',
144
+ restoreVersion: 'Restaurar versão {version}',
145
+ restored: 'Versão {version} restaurada como versão {newVersion}',
146
+ viewVersion: 'Ver versão {version}',
147
+ restoredFrom: 'restaurada da v{version}',
148
+ by: 'por {actor}',
149
+ actor_agent: 'agente',
150
+ actor_user: 'usuário',
151
+ actor_system: 'sistema',
152
+ changes: 'Alterações',
153
+ noChanges: 'Nenhuma alteração.',
154
+ change_added: 'Inserido',
155
+ change_removed: 'Removido',
156
+ change_changed: 'Alterado',
157
+ change_moved: 'Movido',
158
+ diffUnified: 'Destacado',
159
+ diffSplit: 'Lado a lado',
160
+ diffViewLabel: 'Layout do diff',
161
+ diffBefore: 'Versão {version}',
162
+ diffAfter: 'Versão {version}',
163
+ lineAdded: 'inserida',
164
+ lineRemoved: 'removida',
165
+ undo: 'Desfazer',
166
+ redo: 'Refazer',
167
+ undone: 'Alteração desfeita',
168
+ redone: 'Alteração refeita',
169
+ saving: 'Salvando…',
170
+ saved: 'Salvo',
171
+ savedVersion: 'Versão {version} salva',
172
+ unsaved: 'Alterações não salvas',
173
+ rolledBack: 'Alteração rejeitada: {message}',
174
+ dismiss: 'Dispensar',
175
+ readOnly: 'Somente leitura',
176
+ lockedBy: 'Bloqueado pelo {actor} — edição desativada.',
177
+ lockedUntil: 'Bloqueado até {time} — edição desativada.',
178
+ agentUpdated: 'O artefato foi atualizado para a versão {version}',
179
+ conflictTitle: 'Sua edição conflita com uma versão mais nova',
180
+ conflictBody: 'A versão {version} alterou os mesmos itens que você editou.',
181
+ conflictUndoBody: 'O artefato mudou depois da sua edição; desfazê-la também descartaria essas mudanças.',
182
+ keepTheirs: 'Manter versão do agente',
183
+ reapplyMine: 'Reaplicar minha edição',
184
+ editorLabel: 'Editor de {title}',
185
+ lintWarnings: '{count} avisos de lint',
186
+ },
187
+ } satisfies Record<string, Record<string, string>>;
188
+
189
+ export type UiMessageKey = keyof (typeof uiMessages)['en'];
190
+ export type UiMessages = Record<UiMessageKey, string>;
191
+
192
+ /** Extra or overriding catalogs, keyed by BCP-47 tag (`es`, `pt-PT`) or language. */
193
+ export type UiMessageOverrides = Record<string, Partial<UiMessages>>;
194
+
195
+ export type Translate = (key: UiMessageKey, params?: Record<string, string | number>) => string;
196
+
197
+ function format(template: string, params: Record<string, string | number> = {}): string {
198
+ return template.replace(/\{(\w+)\}/g, (match, name: string) =>
199
+ name in params ? String(params[name]) : match,
200
+ );
201
+ }
202
+
203
+ /**
204
+ * Resolution order: exact override, language override, built-in catalog (pt* → pt-BR, else en), English.
205
+ */
206
+ export function createTranslator(locale: string, overrides: UiMessageOverrides = {}): Translate {
207
+ const language = locale.split('-')[0]?.toLowerCase() ?? '';
208
+ const builtIn: UiMessages = language === 'pt' ? uiMessages['pt-BR'] : uiMessages.en;
209
+ const layers = [overrides[locale], overrides[language], builtIn, uiMessages.en];
210
+ return (key, params) => {
211
+ for (const layer of layers) {
212
+ const template = layer?.[key];
213
+ if (template !== undefined) return format(template, params);
214
+ }
215
+ return key;
216
+ };
217
+ }