@kyro-cms/admin 0.1.7 → 0.1.9
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/package.json +7 -2
- package/src/components/Admin.tsx +1 -1
- package/src/components/AutoForm.tsx +966 -337
- package/src/components/CreateView.tsx +1 -1
- package/src/components/DetailView.tsx +1 -1
- package/src/components/EnhancedListView.tsx +156 -52
- package/src/components/ListView.tsx +1 -1
- package/src/components/Modal.tsx +65 -8
- package/src/components/Sidebar.astro +2 -2
- package/src/components/ThemeProvider.tsx +8 -2
- package/src/components/blocks/AccordionBlock.tsx +20 -52
- package/src/components/blocks/ArrayBlock.tsx +40 -31
- package/src/components/blocks/BlockEditModal.tsx +170 -581
- package/src/components/blocks/ButtonBlock.tsx +27 -128
- package/src/components/blocks/CodeBlock.tsx +88 -40
- package/src/components/blocks/ColumnsBlock.tsx +27 -85
- package/src/components/blocks/FileBlock.tsx +38 -39
- package/src/components/blocks/HeadingBlock.tsx +9 -31
- package/src/components/blocks/HeroBlock.tsx +42 -100
- package/src/components/blocks/ImageBlock.tsx +6 -7
- package/src/components/blocks/LinkBlock.tsx +27 -33
- package/src/components/blocks/ListBlock.tsx +47 -26
- package/src/components/blocks/RelationshipBlock.tsx +26 -233
- package/src/components/blocks/RichTextBlock.tsx +66 -0
- package/src/components/blocks/VStackBlock.tsx +23 -37
- package/src/components/blocks/VideoBlock.tsx +52 -32
- package/src/components/fields/AccordionField.tsx +213 -0
- package/src/components/fields/ArrayField.tsx +241 -0
- package/src/components/fields/BlocksField.tsx +5 -5
- package/src/components/fields/ButtonField.tsx +53 -0
- package/src/components/fields/CheckboxField.tsx +7 -3
- package/src/components/fields/ChildrenField.tsx +48 -0
- package/src/components/fields/CodeField.tsx +154 -94
- package/src/components/fields/ColumnsField.tsx +137 -0
- package/src/components/fields/DateField.tsx +9 -24
- package/src/components/fields/EditorClient.tsx +426 -160
- package/src/components/fields/HeadingField.tsx +31 -0
- package/src/components/fields/HeroField.tsx +101 -0
- package/src/components/fields/JSONField.tsx +7 -27
- package/src/components/fields/LinkField.tsx +81 -0
- package/src/components/fields/ListField.tsx +74 -0
- package/src/components/fields/MarkdownField.tsx +4 -26
- package/src/components/fields/NumberField.tsx +9 -27
- package/src/components/fields/PortableTextField.tsx +61 -49
- package/src/components/fields/RelationshipBlockField.tsx +233 -0
- package/src/components/fields/RelationshipField.tsx +59 -13
- package/src/components/fields/SelectField.tsx +6 -4
- package/src/components/fields/TextField.tsx +9 -24
- package/src/components/fields/UploadField.tsx +613 -0
- package/src/components/fields/VideoField.tsx +73 -0
- package/src/components/fields/extensions/blockComponents.tsx +11 -1
- package/src/components/fields/extensions/blocksStore.ts +1 -1
- package/src/components/fields/index.ts +12 -1
- package/src/components/layout/Layout.tsx +1 -1
- package/src/lib/api.ts +163 -0
- package/src/lib/config.ts +1 -1
- package/src/lib/dataStore.ts +87 -30
- package/src/lib/date-utils.ts +69 -0
- package/src/lib/db/version-adapter.ts +248 -0
- package/src/lib/i18n.tsx +353 -0
- package/src/lib/slugify.ts +15 -0
- package/src/lib/validation.ts +250 -0
- package/src/pages/api/[collection]/[id]/publish.ts +12 -4
- package/src/pages/api/[collection]/[id]/versions.ts +39 -9
- package/src/pages/api/[collection]/[id].ts +13 -1
- package/src/pages/api/[collection]/index.ts +5 -6
- package/src/styles/main.css +12 -2
- package/src/components/blocks/BlockEditModal.MARKER +0 -12
- package/src/components/fields/FileField.tsx +0 -390
- package/src/components/fields/HybridContentField.tsx +0 -109
- package/src/components/fields/ImageField.tsx +0 -429
|
@@ -1,30 +1,58 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useState,
|
|
3
|
+
useCallback,
|
|
4
|
+
useRef,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
} from "react";
|
|
2
8
|
import {
|
|
3
9
|
EditorProvider,
|
|
4
10
|
PortableTextEditable,
|
|
5
11
|
useEditor,
|
|
12
|
+
type RenderStyleFunction,
|
|
13
|
+
type RenderDecoratorFunction,
|
|
14
|
+
type RenderBlockFunction,
|
|
15
|
+
type RenderListItemFunction,
|
|
16
|
+
type RenderAnnotationFunction,
|
|
6
17
|
} from "@portabletext/editor";
|
|
7
18
|
import { defineSchema } from "@portabletext/schema";
|
|
8
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 "lucide-react";
|
|
9
44
|
|
|
10
45
|
interface EditorClientProps {
|
|
11
46
|
initialValue: any[];
|
|
12
47
|
onChange: (blocks: any[]) => void;
|
|
13
48
|
disabled?: boolean;
|
|
14
|
-
theme: any;
|
|
15
49
|
}
|
|
16
50
|
|
|
17
|
-
/**
|
|
18
|
-
* Sanitize any incoming value into a valid Portable Text block array.
|
|
19
|
-
* The @portabletext/editor will throw `charCodeAt` errors if it receives
|
|
20
|
-
* undefined, null, a plain string, or blocks with missing/non-string text.
|
|
21
|
-
*/
|
|
22
51
|
function sanitizeInitialValue(value: any): any[] {
|
|
23
52
|
if (!value || !Array.isArray(value)) return [];
|
|
24
53
|
return value.filter((block) => {
|
|
25
54
|
if (!block || typeof block !== "object") return false;
|
|
26
55
|
if (!block._type) return false;
|
|
27
|
-
// For text blocks, ensure every span has a string `text` property
|
|
28
56
|
if (block._type === "block" && Array.isArray(block.children)) {
|
|
29
57
|
block.children = block.children.map((child: any) => ({
|
|
30
58
|
...child,
|
|
@@ -36,7 +64,6 @@ function sanitizeInitialValue(value: any): any[] {
|
|
|
36
64
|
});
|
|
37
65
|
}
|
|
38
66
|
|
|
39
|
-
// Define basic portable text schema
|
|
40
67
|
const schemaDefinition = defineSchema({
|
|
41
68
|
decorators: [
|
|
42
69
|
{ name: "strong", title: "Bold" },
|
|
@@ -46,161 +73,397 @@ const schemaDefinition = defineSchema({
|
|
|
46
73
|
{ name: "code", title: "Code" },
|
|
47
74
|
],
|
|
48
75
|
styles: [
|
|
49
|
-
{ name: "normal", title: "
|
|
50
|
-
{ name: "h1", title: "
|
|
51
|
-
{ name: "h2", title: "
|
|
52
|
-
{ name: "h3", title: "
|
|
76
|
+
{ name: "normal", title: "Normal" },
|
|
77
|
+
{ name: "h1", title: "H1" },
|
|
78
|
+
{ name: "h2", title: "H2" },
|
|
79
|
+
{ name: "h3", title: "H3" },
|
|
53
80
|
{ name: "blockquote", title: "Quote" },
|
|
54
81
|
],
|
|
55
82
|
lists: [
|
|
56
|
-
{ name: "bullet", title: "Bullet
|
|
57
|
-
{ name: "number", title: "
|
|
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
|
+
},
|
|
58
92
|
],
|
|
59
|
-
annotations: [],
|
|
60
93
|
inlineObjects: [],
|
|
61
94
|
blockObjects: [],
|
|
62
95
|
});
|
|
63
96
|
|
|
64
|
-
const
|
|
65
|
-
|
|
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,
|
|
66
260
|
}) => {
|
|
67
|
-
const
|
|
68
|
-
const
|
|
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 any,
|
|
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
|
+
};
|
|
69
353
|
|
|
70
354
|
return (
|
|
71
355
|
<div
|
|
72
|
-
|
|
73
|
-
|
|
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"
|
|
74
358
|
>
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
{"</>"}
|
|
119
|
-
</button>
|
|
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
|
+
};
|
|
120
402
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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";
|
|
125
411
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
<button
|
|
136
|
-
type="button"
|
|
137
|
-
onClick={() => onInsertTag("h2")}
|
|
138
|
-
className="px-2 py-1 text-sm rounded hover:bg-gray-200 font-bold"
|
|
139
|
-
style={{ color: textColor }}
|
|
140
|
-
title="Heading 2"
|
|
141
|
-
>
|
|
142
|
-
H2
|
|
143
|
-
</button>
|
|
144
|
-
<button
|
|
145
|
-
type="button"
|
|
146
|
-
onClick={() => onInsertTag("h3")}
|
|
147
|
-
className="px-2 py-1 text-sm rounded hover:bg-gray-200 font-semibold"
|
|
148
|
-
style={{ color: textColor }}
|
|
149
|
-
title="Heading 3"
|
|
150
|
-
>
|
|
151
|
-
H3
|
|
152
|
-
</button>
|
|
153
|
-
<button
|
|
154
|
-
type="button"
|
|
155
|
-
onClick={() => onInsertTag("blockquote")}
|
|
156
|
-
className="px-2 py-1 text-sm rounded hover:bg-gray-200"
|
|
157
|
-
style={{ color: textColor }}
|
|
158
|
-
title="Quote"
|
|
412
|
+
return (
|
|
413
|
+
<div className="relative">
|
|
414
|
+
<select
|
|
415
|
+
value={activeStyle}
|
|
416
|
+
onChange={(e) => {
|
|
417
|
+
send({ type: "toggle", style: e.target.value as any });
|
|
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)]"
|
|
159
421
|
>
|
|
160
|
-
|
|
161
|
-
<
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
};
|
|
169
432
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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"
|
|
176
442
|
>
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" />
|
|
185
|
-
</svg>
|
|
186
|
-
</button>
|
|
187
|
-
<button
|
|
188
|
-
type="button"
|
|
189
|
-
onClick={() => onInsertTag("number")}
|
|
190
|
-
className="px-2 py-1 text-sm rounded hover:bg-gray-200"
|
|
191
|
-
style={{ color: textColor }}
|
|
192
|
-
title="Numbered List"
|
|
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"
|
|
193
450
|
>
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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 />
|
|
204
467
|
</div>
|
|
205
468
|
);
|
|
206
469
|
};
|
|
@@ -209,24 +472,18 @@ const EditorInner: React.FC<{
|
|
|
209
472
|
onChange: (blocks: any[]) => void;
|
|
210
473
|
disabled?: boolean;
|
|
211
474
|
}> = ({ onChange, disabled }) => {
|
|
212
|
-
const editor = useEditor();
|
|
213
|
-
|
|
214
|
-
const handleInsertTag = useCallback(
|
|
215
|
-
(tag: string) => {
|
|
216
|
-
if (!editor) return;
|
|
217
|
-
console.log("Insert tag:", tag, "Editor available:", !!editor);
|
|
218
|
-
},
|
|
219
|
-
[editor],
|
|
220
|
-
);
|
|
221
|
-
|
|
222
475
|
return (
|
|
223
476
|
<>
|
|
224
|
-
<Toolbar
|
|
477
|
+
<Toolbar />
|
|
225
478
|
<PortableTextEditable
|
|
226
|
-
className="min-h-[200px] p-4 focus:outline-none"
|
|
227
|
-
style={{ backgroundColor: "white", color: "#333" }}
|
|
479
|
+
className="min-h-[200px] p-4 focus:outline-none text-[var(--kyro-text-primary)]"
|
|
228
480
|
placeholder="Start typing..."
|
|
229
481
|
readOnly={disabled}
|
|
482
|
+
renderStyle={renderStyle}
|
|
483
|
+
renderDecorator={renderDecorator}
|
|
484
|
+
renderBlock={renderBlock}
|
|
485
|
+
renderListItem={renderListItem}
|
|
486
|
+
renderAnnotation={renderAnnotation}
|
|
230
487
|
/>
|
|
231
488
|
</>
|
|
232
489
|
);
|
|
@@ -236,10 +493,18 @@ export const EditorClient: React.FC<EditorClientProps> = ({
|
|
|
236
493
|
initialValue,
|
|
237
494
|
onChange,
|
|
238
495
|
disabled,
|
|
239
|
-
theme,
|
|
240
496
|
}) => {
|
|
241
|
-
// Sanitize on mount — never pass raw/malformed data to EditorProvider
|
|
242
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]);
|
|
243
508
|
|
|
244
509
|
const handleChange = useCallback(
|
|
245
510
|
(newValue: any) => {
|
|
@@ -251,6 +516,7 @@ export const EditorClient: React.FC<EditorClientProps> = ({
|
|
|
251
516
|
|
|
252
517
|
return (
|
|
253
518
|
<EditorProvider
|
|
519
|
+
key={JSON.stringify(value)}
|
|
254
520
|
initialConfig={{
|
|
255
521
|
schemaDefinition: schemaDefinition as any,
|
|
256
522
|
initialValue: value,
|