@kyro-cms/admin 0.8.0 → 0.9.1

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 (100) hide show
  1. package/dist/index.cjs +11960 -11006
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +67 -65
  4. package/dist/index.css.map +1 -1
  5. package/dist/index.d.cts +563 -0
  6. package/dist/index.d.ts +7 -7
  7. package/dist/index.js +12183 -11238
  8. package/dist/index.js.map +1 -1
  9. package/package.json +15 -11
  10. package/src/components/ActionBar.tsx +27 -14
  11. package/src/components/Admin.tsx +1 -1
  12. package/src/components/ApiKeysManager.tsx +5 -5
  13. package/src/components/AutoForm.tsx +585 -369
  14. package/src/components/BrandingHub.tsx +7 -4
  15. package/src/components/CreateView.tsx +2 -0
  16. package/src/components/DetailView.tsx +71 -56
  17. package/src/components/DeveloperCenter.tsx +8 -6
  18. package/src/components/FieldRenderer.tsx +94 -19
  19. package/src/components/ListView.tsx +33 -20
  20. package/src/components/MediaGallery.tsx +219 -194
  21. package/src/components/PluginsManager.tsx +197 -70
  22. package/src/components/RestPlayground.tsx +7 -7
  23. package/src/components/SessionsManager.tsx +1 -1
  24. package/src/components/SettingsPage.tsx +22 -0
  25. package/src/components/Sidebar.astro +13 -41
  26. package/src/components/UserManagement.tsx +153 -15
  27. package/src/components/UserMenu.tsx +30 -4
  28. package/src/components/VersionHistoryPanel.tsx +112 -119
  29. package/src/components/WebhookManager.tsx +6 -4
  30. package/src/components/blocks/ArrayBlock.tsx +6 -23
  31. package/src/components/blocks/BlockEditModal.tsx +82 -309
  32. package/src/components/blocks/CardBlock.tsx +35 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +57 -31
  34. package/src/components/blocks/GenericBlock.tsx +44 -0
  35. package/src/components/blocks/HeadingSubheadingBlock.tsx +32 -0
  36. package/src/components/blocks/HeroBlock.tsx +5 -14
  37. package/src/components/blocks/RichTextBlock.tsx +5 -5
  38. package/src/components/blocks/index.ts +5 -3
  39. package/src/components/fields/AccordionField.tsx +2 -2
  40. package/src/components/fields/ArrayField.tsx +1 -1
  41. package/src/components/fields/ArrayLayout.tsx +120 -29
  42. package/src/components/fields/BlocksField.tsx +430 -50
  43. package/src/components/fields/CardField.tsx +73 -0
  44. package/src/components/fields/CheckboxField.tsx +7 -3
  45. package/src/components/fields/DateField.tsx +4 -1
  46. package/src/components/fields/GroupLayout.tsx +2 -2
  47. package/src/components/fields/HeadingSubheadingField.tsx +43 -0
  48. package/src/components/fields/ListField.tsx +2 -2
  49. package/src/components/fields/NumberField.tsx +4 -1
  50. package/src/components/fields/RelationshipField.tsx +153 -87
  51. package/src/components/fields/RichTextField.tsx +781 -0
  52. package/src/components/fields/SecretField.tsx +102 -0
  53. package/src/components/fields/SelectField.tsx +19 -6
  54. package/src/components/fields/TabsLayout.tsx +19 -9
  55. package/src/components/fields/TextField.tsx +4 -1
  56. package/src/components/fields/UploadField.tsx +122 -56
  57. package/src/components/fields/extensions/blockComponents.tsx +103 -174
  58. package/src/components/fields/extensions/blocksStore.ts +8 -1
  59. package/src/components/fields/index.ts +4 -2
  60. package/src/components/ui/PageHeader.tsx +5 -5
  61. package/src/components/ui/SlidePanel.tsx +8 -3
  62. package/src/components/ui/icons.tsx +109 -109
  63. package/src/components/users/UserDetail.tsx +79 -16
  64. package/src/hooks/useAutoFormState.ts +125 -62
  65. package/src/integration.ts +148 -46
  66. package/src/kyro-cms.d.ts +7 -2
  67. package/src/layouts/AuthLayout.astro +14 -2
  68. package/src/lib/autoform-store.ts +85 -52
  69. package/src/lib/change-source.ts +9 -0
  70. package/src/lib/config.ts +104 -8
  71. package/src/lib/globals.ts +44 -9
  72. package/src/lib/normalize-upload-fields.ts +41 -0
  73. package/src/lib/paths.ts +2 -2
  74. package/src/lib/resolve-field-value.ts +110 -0
  75. package/src/lib/shim/use-sync-external-store-with-selector.js +30 -0
  76. package/src/lib/shim/use-sync-external-store.js +1 -0
  77. package/src/lib/stores/index.ts +1 -0
  78. package/src/lib/useResourceManager.ts +4 -4
  79. package/src/lib/vite-shim-plugin.ts +100 -0
  80. package/src/pages/[collection]/[id].astro +1 -1
  81. package/src/pages/preview/[collection]/[id].astro +4 -4
  82. package/src/pages/settings/[slug].astro +2 -2
  83. package/src/styles/main.css +60 -54
  84. package/README.md +0 -46
  85. package/dist/EditorClient-Q23UXR37.cjs +0 -468
  86. package/dist/EditorClient-Q23UXR37.cjs.map +0 -1
  87. package/dist/EditorClient-T5PASFNR.js +0 -466
  88. package/dist/EditorClient-T5PASFNR.js.map +0 -1
  89. package/dist/chunk-3BGDYKTD.cjs +0 -348
  90. package/dist/chunk-3BGDYKTD.cjs.map +0 -1
  91. package/dist/chunk-EEFXLQVT.js +0 -3
  92. package/dist/chunk-EEFXLQVT.js.map +0 -1
  93. package/src/components/blocks/ButtonBlock.tsx +0 -64
  94. package/src/components/blocks/ColumnsBlock.tsx +0 -55
  95. package/src/components/blocks/DividerBlock.tsx +0 -43
  96. package/src/components/blocks/LinkBlock.tsx +0 -65
  97. package/src/components/blocks/VStackBlock.tsx +0 -29
  98. package/src/components/fields/EditorClient.tsx +0 -535
  99. package/src/components/fields/PortableTextField.tsx +0 -155
  100. package/src/components/fields/PortableTextRenderer.tsx +0 -68
@@ -1,29 +0,0 @@
1
- import React from "react";
2
- import {
3
- useBlockById,
4
- useBlockActions,
5
- } from "../fields/extensions/blocksStore";
6
- import { ChildrenField } from "../fields/ChildrenField";
7
- import { BlockWrapper } from "./BlockWrapper";
8
-
9
- export const VStackBlock: React.FC<{ block: Record<string, unknown>; index: number }> = ({
10
- block,
11
- index,
12
- }) => {
13
- const blockData = useBlockById(block.id);
14
- const { updateBlock } = useBlockActions();
15
-
16
- const data = blockData?.data ?? block.data ?? {};
17
- const children = blockData?.children ?? block.children ?? [];
18
-
19
- return (
20
- <BlockWrapper id={block.id} type="vstack" label="Stack">
21
- <ChildrenField
22
- blockId={block.id}
23
- children={children}
24
- onUpdateChildren={(c) => updateBlock(block.id, { children: c })}
25
- compact
26
- />
27
- </BlockWrapper>
28
- );
29
- };
@@ -1,535 +0,0 @@
1
- import React, {
2
- useState,
3
- useCallback,
4
- useRef,
5
- useEffect,
6
- useMemo,
7
- } from "react";
8
- import {
9
- EditorProvider,
10
- PortableTextEditable,
11
- useEditor,
12
- type RenderStyleFunction,
13
- type RenderDecoratorFunction,
14
- type RenderBlockFunction,
15
- type RenderListItemFunction,
16
- type RenderAnnotationFunction,
17
- } from "@portabletext/editor";
18
- import { defineSchema } from "@portabletext/schema";
19
- import { EventListenerPlugin } from "@portabletext/editor/plugins";
20
- import {
21
- useToolbarSchema,
22
- useDecoratorButton,
23
- useListButton,
24
- useStyleSelector,
25
- useHistoryButtons,
26
- useAnnotationButton,
27
- useAnnotationPopover,
28
- } from "@portabletext/toolbar";
29
- import {
30
- Bold,
31
- Italic,
32
- Underline,
33
- Strikethrough,
34
- Code,
35
- Link,
36
- List,
37
- ListOrdered,
38
- Undo,
39
- Redo,
40
- ChevronDown,
41
- X,
42
- ExternalLink,
43
- } from "../ui/icons";
44
-
45
- interface EditorClientProps {
46
- initialValue: Record<string, unknown>[];
47
- onChange: (blocks: Record<string, unknown>[]) => void;
48
- disabled?: boolean;
49
- }
50
-
51
- function sanitizeInitialValue(value: unknown): Record<string, unknown>[] {
52
- if (!value || !Array.isArray(value)) return [];
53
- return (value as Record<string, unknown>[]).filter((block) => {
54
- if (!block || typeof block !== "object") return false;
55
- if (!("_type" in block)) return false;
56
- if ((block as { _type?: string })._type === "block" && Array.isArray((block as { children?: unknown[] }).children)) {
57
- (block as { children: Record<string, unknown>[] }).children = (block.children as Record<string, unknown>[]).map((child) => ({
58
- ...child,
59
- _type: (child as { _type?: string })._type || "span",
60
- text: typeof (child as { text?: unknown }).text === "string" ? child.text : "",
61
- }));
62
- }
63
- return true;
64
- });
65
- }
66
-
67
- const schemaDefinition = defineSchema({
68
- decorators: [
69
- { name: "strong", title: "Bold" },
70
- { name: "em", title: "Italic" },
71
- { name: "underline", title: "Underline" },
72
- { name: "strikeThrough", title: "Strikethrough" },
73
- { name: "code", title: "Code" },
74
- ],
75
- styles: [
76
- { name: "normal", title: "Normal" },
77
- { name: "h1", title: "H1" },
78
- { name: "h2", title: "H2" },
79
- { name: "h3", title: "H3" },
80
- { name: "blockquote", title: "Quote" },
81
- ],
82
- lists: [
83
- { name: "bullet", title: "Bullet" },
84
- { name: "number", title: "Number" },
85
- ],
86
- annotations: [
87
- {
88
- name: "link",
89
- title: "Link",
90
- fields: [{ name: "href", type: "string", title: "URL" }],
91
- },
92
- ],
93
- inlineObjects: [],
94
- blockObjects: [],
95
- });
96
-
97
- const renderStyle: RenderStyleFunction = (props) => {
98
- if (props.schemaType.value === "h1") {
99
- return <h1 className="text-2xl font-bold mb-2">{props.children}</h1>;
100
- }
101
- if (props.schemaType.value === "h2") {
102
- return <h2 className="text-xl font-bold mb-2">{props.children}</h2>;
103
- }
104
- if (props.schemaType.value === "h3") {
105
- return <h3 className="text-lg font-semibold mb-1">{props.children}</h3>;
106
- }
107
- if (props.schemaType.value === "blockquote") {
108
- return (
109
- <blockquote className="border-l-2 border-[var(--kyro-primary)] pl-4 italic text-[var(--kyro-text-muted)] my-2">
110
- {props.children}
111
- </blockquote>
112
- );
113
- }
114
- return <>{props.children}</>;
115
- };
116
-
117
- const renderDecorator: RenderDecoratorFunction = (props) => {
118
- if (props.value === "strong") {
119
- return <strong>{props.children}</strong>;
120
- }
121
- if (props.value === "em") {
122
- return <em>{props.children}</em>;
123
- }
124
- if (props.value === "underline") {
125
- return <u>{props.children}</u>;
126
- }
127
- if (props.value === "strikeThrough") {
128
- return <s>{props.children}</s>;
129
- }
130
- if (props.value === "code") {
131
- return (
132
- <code className="px-1 py-0.5 rounded bg-[var(--kyro-surface-accent)] text-[var(--kyro-primary)] text-sm font-mono">
133
- {props.children}
134
- </code>
135
- );
136
- }
137
- return <>{props.children}</>;
138
- };
139
-
140
- const renderBlock: RenderBlockFunction = (props) => {
141
- return <div>{props.children}</div>;
142
- };
143
-
144
- const renderListItem: RenderListItemFunction = (props) => {
145
- if (props.schemaType.value === "bullet") {
146
- return <li className="list-disc ml-4">{props.children}</li>;
147
- }
148
- if (props.schemaType.value === "number") {
149
- return <li className="list-decimal ml-4">{props.children}</li>;
150
- }
151
- return <li>{props.children}</li>;
152
- };
153
-
154
- const renderAnnotation: RenderAnnotationFunction = (props) => {
155
- if (props.schemaType.name === "link") {
156
- return (
157
- <a
158
- href={props.value.href as string}
159
- className="text-[var(--kyro-primary)] underline hover:opacity-80"
160
- target="_blank"
161
- rel="noopener noreferrer"
162
- >
163
- {props.children}
164
- </a>
165
- );
166
- }
167
- return <>{props.children}</>;
168
- };
169
-
170
- function FocusRestoringButton({
171
- onClick,
172
- children,
173
- ...props
174
- }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
175
- const editor = useEditor();
176
- return (
177
- <button
178
- type="button"
179
- onClick={(e) => {
180
- onClick?.(e);
181
- editor.send({ type: "focus" });
182
- }}
183
- {...props}
184
- >
185
- {children}
186
- </button>
187
- );
188
- }
189
-
190
- const decoratorIcons: Record<
191
- string,
192
- React.ComponentType<{ className?: string }>
193
- > = {
194
- strong: Bold,
195
- em: Italic,
196
- underline: Underline,
197
- strikeThrough: Strikethrough,
198
- code: Code,
199
- };
200
-
201
- const DecoratorButton: React.FC<{ name: string; title: string }> = ({
202
- name,
203
- title,
204
- }) => {
205
- const schema = useToolbarSchema({});
206
- const decoratorSchema = schema.decorators?.find((d) => d.name === name);
207
- if (!decoratorSchema) return null;
208
- const { snapshot, send } = useDecoratorButton({
209
- schemaType: decoratorSchema,
210
- });
211
- const Icon = decoratorIcons[name];
212
- const isActive =
213
- snapshot.matches({ enabled: "active" }) ||
214
- snapshot.matches({ disabled: "active" });
215
- const isEnabled = snapshot.matches("enabled");
216
-
217
- return (
218
- <FocusRestoringButton
219
- disabled={!isEnabled}
220
- data-state={isActive ? "on" : "off"}
221
- onClick={() => send({ type: "toggle" })}
222
- title={title}
223
- className="p-1.5 rounded transition-colors disabled:opacity-30 hover:bg-[var(--kyro-surface-accent)] data-[state=on]:bg-[var(--kyro-primary)] data-[state=on]:text-[var(--kyro-sidebar-text-active)]"
224
- >
225
- {Icon && <Icon className="w-4 h-4" />}
226
- </FocusRestoringButton>
227
- );
228
- };
229
-
230
- const ListButton: React.FC<{ name: string; title: string }> = ({
231
- name,
232
- title,
233
- }) => {
234
- const schema = useToolbarSchema({});
235
- const listSchema = schema.lists?.find((l) => l.name === name);
236
- if (!listSchema) return null;
237
- const { snapshot, send } = useListButton({ schemaType: listSchema });
238
- const Icon = name === "bullet" ? List : ListOrdered;
239
- const isActive =
240
- snapshot.matches({ enabled: "active" }) ||
241
- snapshot.matches({ disabled: "active" });
242
- const isEnabled = snapshot.matches("enabled");
243
-
244
- return (
245
- <FocusRestoringButton
246
- disabled={!isEnabled}
247
- data-state={isActive ? "on" : "off"}
248
- onClick={() => send({ type: "toggle" })}
249
- title={title}
250
- className="p-1.5 rounded transition-colors disabled:opacity-30 hover:bg-[var(--kyro-surface-accent)] data-[state=on]:bg-[var(--kyro-primary)] data-[state=on]:text-[var(--kyro-sidebar-text-active)]"
251
- >
252
- {Icon && <Icon className="w-4 h-4" />}
253
- </FocusRestoringButton>
254
- );
255
- };
256
-
257
- const AnnotationButton: React.FC<{ name: string; title: string }> = ({
258
- name,
259
- title,
260
- }) => {
261
- const schema = useToolbarSchema({});
262
- const annotationSchema = schema.annotations?.find((a) => a.name === name);
263
- if (!annotationSchema) return null;
264
- const { snapshot, send } = useAnnotationButton({
265
- schemaType: annotationSchema,
266
- });
267
- const isActive =
268
- snapshot.matches({ enabled: "active" }) ||
269
- snapshot.matches({ disabled: "active" });
270
- const isEnabled = snapshot.matches("enabled");
271
- const isShowingDialog = snapshot.matches({
272
- enabled: { inactive: "showing dialog" },
273
- });
274
-
275
- return (
276
- <FocusRestoringButton
277
- disabled={!isEnabled}
278
- data-state={isActive ? "on" : "off"}
279
- onClick={() =>
280
- send({
281
- type: isShowingDialog ? "close dialog" : "open dialog",
282
- })
283
- }
284
- title={title}
285
- className="p-1.5 rounded transition-colors disabled:opacity-30 hover:bg-[var(--kyro-surface-accent)] data-[state=on]:bg-[var(--kyro-primary)] data-[state=on]:text-[var(--kyro-sidebar-text-active)]"
286
- >
287
- {name === "link" && <Link className="w-4 h-4" />}
288
- </FocusRestoringButton>
289
- );
290
- };
291
-
292
- const LinkDialog: React.FC = () => {
293
- const schema = useToolbarSchema({});
294
- const popover = useAnnotationPopover({
295
- schemaTypes: schema.annotations || [],
296
- });
297
- const dialogRef = useRef<HTMLDivElement>(null);
298
- const inputRef = useRef<HTMLInputElement>(null);
299
-
300
- useEffect(() => {
301
- if (popover.snapshot.matches({ enabled: "active" }) && inputRef.current) {
302
- inputRef.current.focus();
303
- }
304
- }, [popover.snapshot]);
305
-
306
- if (!popover.snapshot.matches({ enabled: "active" })) return null;
307
-
308
- const activeAnnotations = popover.snapshot.context.annotations || [];
309
- const activeLink = activeAnnotations.find(
310
- (a) => a.schemaType.name === "link",
311
- );
312
- const currentHref = activeLink?.value?.href as string | undefined;
313
-
314
- const handleSubmit = (e: React.FormEvent) => {
315
- e.preventDefault();
316
- const formData = new FormData(e.target as HTMLFormElement);
317
- const href = formData.get("href") as string;
318
-
319
- if (activeLink) {
320
- if (href.trim()) {
321
- popover.send({
322
- type: "edit",
323
- at: activeLink.at,
324
- props: { href: href.trim() },
325
- });
326
- } else {
327
- popover.send({
328
- type: "remove",
329
- schemaType: activeLink.schemaType,
330
- });
331
- }
332
- } else {
333
- if (href.trim()) {
334
- popover.send({
335
- type: "edit",
336
- at: [] as string[],
337
- props: { href: href.trim() },
338
- });
339
- }
340
- }
341
- popover.send({ type: "close" });
342
- };
343
-
344
- const handleRemove = () => {
345
- if (activeLink) {
346
- popover.send({
347
- type: "remove",
348
- schemaType: activeLink.schemaType,
349
- });
350
- }
351
- popover.send({ type: "close" });
352
- };
353
-
354
- return (
355
- <div
356
- ref={dialogRef}
357
- className="absolute top-full left-0 z-50 mt-1 w-72 p-3 rounded-lg border border-[var(--kyro-border)] bg-[var(--kyro-bg-primary)] shadow-lg"
358
- >
359
- <div className="flex items-center justify-between mb-2">
360
- <span className="text-sm font-medium text-[var(--kyro-text-primary)]">
361
- Link
362
- </span>
363
- <button
364
- type="button"
365
- onClick={() => popover.send({ type: "close" })}
366
- className="p-1 rounded hover:bg-[var(--kyro-surface-accent)]"
367
- >
368
- <X className="w-3.5 h-3.5 text-[var(--kyro-text-muted)]" />
369
- </button>
370
- </div>
371
- <form onSubmit={handleSubmit} className="space-y-2">
372
- <div className="flex gap-1.5">
373
- <input
374
- ref={inputRef}
375
- name="href"
376
- type="url"
377
- defaultValue={currentHref || "https://"}
378
- placeholder="Enter URL..."
379
- className="flex-1 px-2.5 py-1.5 text-sm rounded border border-[var(--kyro-border)] bg-transparent text-[var(--kyro-text-primary)] placeholder:text-[var(--kyro-text-muted)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-primary)]"
380
- />
381
- <button
382
- type="submit"
383
- className="px-2.5 py-1.5 text-sm rounded bg-[var(--kyro-primary)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90"
384
- >
385
- {activeLink ? "Update" : "Add"}
386
- </button>
387
- </div>
388
- {activeLink && (
389
- <button
390
- type="button"
391
- onClick={handleRemove}
392
- className="w-full text-xs text-[var(--kyro-error)] hover:opacity-80 flex items-center justify-center gap-1 py-1"
393
- >
394
- <ExternalLink className="w-3 h-3" />
395
- Remove link
396
- </button>
397
- )}
398
- </form>
399
- </div>
400
- );
401
- };
402
-
403
- const StyleSelector: React.FC = () => {
404
- const schema = useToolbarSchema({});
405
- const editor = useEditor();
406
- const { snapshot, send } = useStyleSelector({
407
- schemaTypes: schema.styles || [],
408
- });
409
- if (!snapshot.matches("enabled")) return null;
410
- const activeStyle = snapshot.context.activeStyle || "normal";
411
-
412
- return (
413
- <div className="relative">
414
- <select
415
- value={activeStyle}
416
- onChange={(e) => {
417
- send({ type: "toggle", style: e.target.value as string });
418
- editor.send({ type: "focus" });
419
- }}
420
- className="appearance-none bg-transparent text-sm pr-6 pl-2 py-1 rounded hover:bg-[var(--kyro-surface-accent)] cursor-pointer focus:outline-none focus:ring-1 focus:ring-[var(--kyro-primary)]"
421
- >
422
- {schema.styles?.map((s) => (
423
- <option key={s.name} value={s.name}>
424
- {s.title}
425
- </option>
426
- ))}
427
- </select>
428
- <ChevronDown className="w-3 h-3 absolute right-1.5 top-1/2 -translate-y-1/2 pointer-events-none text-[var(--kyro-text-muted)]" />
429
- </div>
430
- );
431
- };
432
-
433
- const Toolbar: React.FC = () => {
434
- const { snapshot: historySnapshot, send: historySend } = useHistoryButtons();
435
- return (
436
- <div className="relative flex items-center gap-0.5 p-1.5 border-b border-[var(--kyro-border)]">
437
- <FocusRestoringButton
438
- disabled={!historySnapshot.matches("enabled")}
439
- onClick={() => historySend({ type: "history.undo" })}
440
- title="Undo"
441
- className="p-1.5 rounded transition-colors hover:bg-[var(--kyro-surface-accent)] disabled:opacity-30"
442
- >
443
- <Undo className="w-4 h-4 text-[var(--kyro-text-secondary)]" />
444
- </FocusRestoringButton>
445
- <FocusRestoringButton
446
- disabled={!historySnapshot.matches("enabled")}
447
- onClick={() => historySend({ type: "history.redo" })}
448
- title="Redo"
449
- className="p-1.5 rounded transition-colors hover:bg-[var(--kyro-surface-accent)] disabled:opacity-30"
450
- >
451
- <Redo className="w-4 h-4 text-[var(--kyro-text-secondary)]" />
452
- </FocusRestoringButton>
453
- <div className="w-px h-5 bg-[var(--kyro-border)] mx-1" />
454
- <StyleSelector />
455
- <div className="w-px h-5 bg-[var(--kyro-border)] mx-1" />
456
- <DecoratorButton name="strong" title="Bold" />
457
- <DecoratorButton name="em" title="Italic" />
458
- <DecoratorButton name="underline" title="Underline" />
459
- <DecoratorButton name="strikeThrough" title="Strikethrough" />
460
- <DecoratorButton name="code" title="Code" />
461
- <div className="w-px h-5 bg-[var(--kyro-border)] mx-1" />
462
- <AnnotationButton name="link" title="Link" />
463
- <div className="w-px h-5 bg-[var(--kyro-border)] mx-1" />
464
- <ListButton name="bullet" title="Bullet List" />
465
- <ListButton name="number" title="Numbered List" />
466
- <LinkDialog />
467
- </div>
468
- );
469
- };
470
-
471
- const EditorInner: React.FC<{
472
- onChange: (blocks: Record<string, unknown>[]) => void;
473
- disabled?: boolean;
474
- }> = ({ onChange, disabled }) => {
475
- return (
476
- <>
477
- <Toolbar />
478
- <PortableTextEditable
479
- className="min-h-[200px] p-4 focus:outline-none text-[var(--kyro-text-primary)]"
480
- placeholder="Start typing..."
481
- readOnly={disabled}
482
- renderStyle={renderStyle}
483
- renderDecorator={renderDecorator}
484
- renderBlock={renderBlock}
485
- renderListItem={renderListItem}
486
- renderAnnotation={renderAnnotation}
487
- />
488
- </>
489
- );
490
- };
491
-
492
- export const EditorClient: React.FC<EditorClientProps> = ({
493
- initialValue,
494
- onChange,
495
- disabled,
496
- }) => {
497
- const [value, setValue] = useState(() => sanitizeInitialValue(initialValue));
498
- const prevInitialValueRef = useRef(initialValue);
499
-
500
- useEffect(() => {
501
- const sanitized = sanitizeInitialValue(initialValue);
502
- const prevSanitized = sanitizeInitialValue(prevInitialValueRef.current);
503
- if (JSON.stringify(sanitized) !== JSON.stringify(prevSanitized)) {
504
- setValue(sanitized);
505
- prevInitialValueRef.current = initialValue;
506
- }
507
- }, [initialValue]);
508
-
509
- const handleChange = useCallback(
510
- (newValue: Record<string, unknown>[]) => {
511
- setValue(newValue);
512
- onChange(newValue);
513
- },
514
- [onChange],
515
- );
516
-
517
- return (
518
- <EditorProvider
519
- key={JSON.stringify(value)}
520
- initialConfig={{
521
- schemaDefinition: schemaDefinition as Record<string, unknown>,
522
- initialValue: value,
523
- }}
524
- >
525
- <EventListenerPlugin
526
- on={(event: Record<string, unknown>) => {
527
- if (event.type === "mutation" && event.value) {
528
- handleChange(event.value as Record<string, unknown>);
529
- }
530
- }}
531
- />
532
- <EditorInner onChange={handleChange} disabled={disabled} />
533
- </EditorProvider>
534
- );
535
- };