@kyro-cms/admin 0.1.6 → 0.1.8
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/README.md +149 -51
- package/package.json +54 -5
- package/src/collections/auth/index.ts +2 -2
- package/src/collections/portfolio/index.ts +343 -0
- package/src/components/ActionBar.tsx +153 -16
- package/src/components/Admin.tsx +137 -28
- package/src/components/ApiExplorer.tsx +325 -0
- package/src/components/ApiKeysManager.tsx +563 -0
- package/src/components/AuditLogsPage.tsx +664 -0
- package/src/components/AutoForm.tsx +2155 -770
- package/src/components/BrandingHub.tsx +267 -0
- package/src/components/BulkActionsBar.tsx +3 -3
- package/src/components/CreateView.tsx +4 -4
- package/src/components/Dashboard.tsx +393 -0
- package/src/components/DetailView.tsx +200 -58
- package/src/components/DeveloperCenter.tsx +403 -0
- package/src/components/EnhancedListView.tsx +890 -0
- package/src/components/GraphQLExplorer.tsx +675 -0
- package/src/components/GraphQLPlayground.tsx +627 -0
- package/src/components/ListView.tsx +192 -54
- package/src/components/MediaGallery.tsx +1569 -0
- package/src/components/Modal.tsx +206 -0
- package/src/components/RestPlayground.tsx +951 -0
- package/src/components/Sidebar.astro +237 -0
- package/src/components/ThemeProvider.tsx +8 -2
- package/src/components/UserManagement.tsx +204 -0
- package/src/components/VersionHistoryPanel.tsx +3 -3
- package/src/components/WebhookManager.tsx +608 -0
- package/src/components/blocks/AccordionBlock.tsx +65 -0
- package/src/components/blocks/ArrayBlock.tsx +84 -0
- package/src/components/blocks/BlockEditModal.tsx +363 -0
- package/src/components/blocks/ButtonBlock.tsx +64 -0
- package/src/components/blocks/ChildBlocksTree.tsx +551 -0
- package/src/components/blocks/CodeBlock.tsx +114 -0
- package/src/components/blocks/ColumnsBlock.tsx +93 -0
- package/src/components/blocks/DividerBlock.tsx +43 -0
- package/src/components/blocks/FileBlock.tsx +63 -0
- package/src/components/blocks/HeadingBlock.tsx +59 -0
- package/src/components/blocks/HeroBlock.tsx +99 -0
- package/src/components/blocks/ImageBlock.tsx +82 -0
- package/src/components/blocks/LinkBlock.tsx +65 -0
- package/src/components/blocks/ListBlock.tsx +60 -0
- package/src/components/blocks/ParagraphBlock.tsx +61 -0
- package/src/components/blocks/RelationshipBlock.tsx +72 -0
- package/src/components/blocks/RichTextBlock.tsx +66 -0
- package/src/components/blocks/VStackBlock.tsx +61 -0
- package/src/components/blocks/VideoBlock.tsx +65 -0
- package/src/components/blocks/index.ts +10 -0
- package/src/components/fields/AccordionField.tsx +213 -0
- package/src/components/fields/ArrayField.tsx +241 -0
- package/src/components/fields/BlocksField.tsx +323 -0
- package/src/components/fields/ButtonField.tsx +53 -0
- package/src/components/fields/CheckboxField.tsx +18 -8
- package/src/components/fields/ChildrenField.tsx +48 -0
- package/src/components/fields/CodeField.tsx +294 -0
- package/src/components/fields/ColumnsField.tsx +137 -0
- package/src/components/fields/DateField.tsx +24 -12
- package/src/components/fields/EditorClient.tsx +537 -0
- package/src/components/fields/HeadingField.tsx +31 -0
- package/src/components/fields/HeroField.tsx +101 -0
- package/src/components/fields/JSONField.tsx +341 -0
- package/src/components/fields/LinkField.tsx +81 -0
- package/src/components/fields/ListField.tsx +74 -0
- package/src/components/fields/MarkdownField.tsx +260 -0
- package/src/components/fields/NumberField.tsx +25 -13
- package/src/components/fields/PortableTextField.tsx +155 -0
- package/src/components/fields/PortableTextRenderer.tsx +68 -0
- package/src/components/fields/RelationshipBlockField.tsx +233 -0
- package/src/components/fields/RelationshipField.tsx +278 -60
- package/src/components/fields/SelectField.tsx +28 -16
- package/src/components/fields/TextField.tsx +31 -15
- package/src/components/fields/UploadField.tsx +613 -0
- package/src/components/fields/VideoField.tsx +73 -0
- package/src/components/fields/extensions/blockComponents.tsx +247 -0
- package/src/components/fields/extensions/blocksStore.ts +273 -0
- package/src/components/fields/index.ts +24 -0
- package/src/components/index.ts +1 -2
- package/src/components/layout/Header.tsx +2 -2
- package/src/components/layout/Layout.tsx +3 -3
- package/src/components/ui/Badge.tsx +9 -4
- package/src/components/ui/BlockDrawer.tsx +79 -0
- package/src/components/ui/Button.tsx +1 -1
- package/src/components/ui/CommandPalette.tsx +362 -0
- package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
- package/src/components/ui/Dropdown.tsx +1 -1
- package/src/components/ui/Modal.tsx +37 -12
- package/src/components/ui/PromptModal.tsx +94 -0
- package/src/components/ui/SlidePanel.tsx +43 -16
- package/src/components/ui/Toast.tsx +80 -14
- package/src/env.d.ts +16 -0
- package/src/env.ts +20 -0
- package/src/index.ts +0 -1
- package/src/layouts/AdminLayout.astro +164 -170
- package/src/layouts/AuthLayout.astro +23 -6
- package/src/lib/MediaService.ts +541 -0
- package/src/lib/api.ts +163 -0
- package/src/lib/auth/sqlite-adapter.ts +319 -0
- package/src/lib/config.ts +23 -7
- package/src/lib/dataStore.ts +188 -73
- package/src/lib/date-utils.ts +69 -0
- package/src/lib/db/adapter.ts +54 -0
- package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
- package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
- package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
- package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
- package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
- package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
- package/src/lib/db/index.ts +449 -0
- package/src/lib/db/mongodb-adapter.ts +207 -0
- package/src/lib/db/mongodb-auth-adapter.ts +305 -0
- package/src/lib/db/schema/mysql-auth.ts +113 -0
- package/src/lib/db/schema/mysql-content.ts +20 -0
- package/src/lib/db/schema/postgres-auth.ts +116 -0
- package/src/lib/db/schema/postgres-content.ts +35 -0
- package/src/lib/db/schema/postgres-media.ts +52 -0
- package/src/lib/db/schema/postgres-settings.ts +11 -0
- package/src/lib/db/schema/sqlite-auth.ts +112 -0
- package/src/lib/db/schema/sqlite-content.ts +20 -0
- package/src/lib/db/version-adapter.ts +248 -0
- package/src/lib/graphql/index.ts +1 -0
- package/src/lib/graphql/schema.ts +443 -0
- package/src/lib/i18n.tsx +353 -0
- package/src/lib/rate-limit.ts +267 -0
- package/src/lib/slugify.ts +15 -0
- package/src/lib/storage.ts +374 -0
- package/src/lib/store.ts +85 -0
- package/src/lib/validation.ts +250 -0
- package/src/middleware.ts +70 -11
- package/src/pages/[collection]/[id].astro +178 -122
- package/src/pages/[collection]/index.astro +24 -156
- package/src/pages/admin/api-explorer.astro +98 -0
- package/src/pages/admin/graphql-explorer.astro +40 -0
- package/src/pages/admin/graphql.astro +97 -0
- package/src/pages/admin/index.astro +200 -139
- package/src/pages/admin/keys.astro +8 -0
- package/src/pages/admin/rest-playground.astro +44 -0
- package/src/pages/admin/webhooks.astro +8 -0
- package/src/pages/api/[collection]/[id]/publish.ts +52 -0
- package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
- package/src/pages/api/[collection]/[id]/versions.ts +66 -0
- package/src/pages/api/[collection]/[id].ts +114 -159
- package/src/pages/api/[collection]/index.ts +150 -230
- package/src/pages/api/auth/[id].ts +48 -69
- package/src/pages/api/auth/audit-logs.ts +20 -43
- package/src/pages/api/auth/login.ts +159 -45
- package/src/pages/api/auth/logout.ts +42 -24
- package/src/pages/api/auth/refresh.ts +119 -0
- package/src/pages/api/auth/register.ts +110 -40
- package/src/pages/api/auth/users.ts +22 -97
- package/src/pages/api/collections.ts +59 -0
- package/src/pages/api/globals/[slug]/test.ts +172 -0
- package/src/pages/api/globals/[slug].ts +42 -0
- package/src/pages/api/graphql.ts +90 -0
- package/src/pages/api/health.ts +417 -40
- package/src/pages/api/keys/[id].ts +26 -0
- package/src/pages/api/keys/index.ts +75 -0
- package/src/pages/api/media/[id].ts +309 -0
- package/src/pages/api/media/folders.ts +609 -0
- package/src/pages/api/media/index.ts +146 -0
- package/src/pages/api/media/resize.ts +267 -0
- package/src/pages/api/search.ts +82 -0
- package/src/pages/api/slug-availability.ts +70 -0
- package/src/pages/api/storage-config.ts +20 -0
- package/src/pages/api/storage-status.ts +206 -0
- package/src/pages/api/upload.ts +334 -0
- package/src/pages/api/webhooks/index.ts +71 -0
- package/src/pages/audit/index.astro +2 -104
- package/src/pages/login.astro +11 -11
- package/src/pages/media.astro +10 -0
- package/src/pages/preview/[collection]/[id].astro +178 -0
- package/src/pages/register.astro +13 -13
- package/src/pages/roles/index.astro +21 -21
- package/src/pages/settings/[slug].astro +162 -0
- package/src/pages/settings/index.astro +9 -0
- package/src/pages/users/[id].astro +29 -21
- package/src/pages/users/index.astro +22 -17
- package/src/pages/users/new.astro +18 -17
- package/src/styles/main.css +563 -128
- package/src/components/layout/Sidebar.tsx +0 -497
|
@@ -0,0 +1,537 @@
|
|
|
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 "lucide-react";
|
|
44
|
+
|
|
45
|
+
interface EditorClientProps {
|
|
46
|
+
initialValue: any[];
|
|
47
|
+
onChange: (blocks: any[]) => void;
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sanitizeInitialValue(value: any): any[] {
|
|
52
|
+
if (!value || !Array.isArray(value)) return [];
|
|
53
|
+
return value.filter((block) => {
|
|
54
|
+
if (!block || typeof block !== "object") return false;
|
|
55
|
+
if (!block._type) return false;
|
|
56
|
+
if (block._type === "block" && Array.isArray(block.children)) {
|
|
57
|
+
block.children = block.children.map((child: any) => ({
|
|
58
|
+
...child,
|
|
59
|
+
_type: child._type || "span",
|
|
60
|
+
text: typeof child.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 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
|
+
};
|
|
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 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)]"
|
|
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: any[]) => 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: any) => {
|
|
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 any,
|
|
522
|
+
initialValue: value,
|
|
523
|
+
}}
|
|
524
|
+
>
|
|
525
|
+
<EventListenerPlugin
|
|
526
|
+
on={(event: any) => {
|
|
527
|
+
if (event.type === "mutation" && event.value) {
|
|
528
|
+
handleChange(event.value);
|
|
529
|
+
}
|
|
530
|
+
}}
|
|
531
|
+
/>
|
|
532
|
+
<EditorInner onChange={handleChange} disabled={disabled} />
|
|
533
|
+
</EditorProvider>
|
|
534
|
+
);
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
export default EditorClient;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface HeadingFieldProps {
|
|
4
|
+
text?: string;
|
|
5
|
+
onChange: (field: string, value: string) => void;
|
|
6
|
+
compact?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const HeadingField: React.FC<HeadingFieldProps> = ({
|
|
10
|
+
text = "",
|
|
11
|
+
onChange,
|
|
12
|
+
compact = false,
|
|
13
|
+
}) => {
|
|
14
|
+
const inputClass = compact
|
|
15
|
+
? "w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
16
|
+
: "w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent";
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className={compact ? "" : "space-y-3"}>
|
|
20
|
+
<input
|
|
21
|
+
type="text"
|
|
22
|
+
value={text}
|
|
23
|
+
onChange={(e) => onChange("text", e.target.value)}
|
|
24
|
+
className={inputClass}
|
|
25
|
+
placeholder="Enter heading text..."
|
|
26
|
+
/>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default HeadingField;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface HeroFieldProps {
|
|
4
|
+
heading?: string;
|
|
5
|
+
subheading?: string;
|
|
6
|
+
ctaText?: string;
|
|
7
|
+
ctaUrl?: string;
|
|
8
|
+
onChange: (field: string, value: string) => void;
|
|
9
|
+
compact?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const HeroField: React.FC<HeroFieldProps> = ({
|
|
13
|
+
heading = "",
|
|
14
|
+
subheading = "",
|
|
15
|
+
ctaText = "",
|
|
16
|
+
ctaUrl = "",
|
|
17
|
+
onChange,
|
|
18
|
+
compact = false,
|
|
19
|
+
}) => {
|
|
20
|
+
const inputClass = compact
|
|
21
|
+
? "w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
22
|
+
: "w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent";
|
|
23
|
+
|
|
24
|
+
const textareaClass = compact
|
|
25
|
+
? "w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] min-h-[50px] resize-none focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
26
|
+
: "w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] min-h-[80px] resize-none focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent";
|
|
27
|
+
|
|
28
|
+
if (compact) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="space-y-2">
|
|
31
|
+
<input
|
|
32
|
+
type="text"
|
|
33
|
+
value={heading}
|
|
34
|
+
onChange={(e) => onChange("heading", e.target.value)}
|
|
35
|
+
className={`${inputClass} font-bold text-base`}
|
|
36
|
+
placeholder="Hero heading..."
|
|
37
|
+
/>
|
|
38
|
+
<textarea
|
|
39
|
+
value={subheading}
|
|
40
|
+
onChange={(e) => onChange("subheading", e.target.value)}
|
|
41
|
+
className={textareaClass}
|
|
42
|
+
placeholder="Hero subheading..."
|
|
43
|
+
/>
|
|
44
|
+
<div className="flex items-center gap-2">
|
|
45
|
+
<input
|
|
46
|
+
type="text"
|
|
47
|
+
value={ctaText}
|
|
48
|
+
onChange={(e) => onChange("ctaText", e.target.value)}
|
|
49
|
+
className="flex-1 px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
50
|
+
placeholder="CTA text..."
|
|
51
|
+
/>
|
|
52
|
+
<span className="text-[var(--kyro-text-muted)] text-xs">→</span>
|
|
53
|
+
<input
|
|
54
|
+
type="url"
|
|
55
|
+
value={ctaUrl}
|
|
56
|
+
onChange={(e) => onChange("ctaUrl", e.target.value)}
|
|
57
|
+
className="flex-1 px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent font-mono text-xs"
|
|
58
|
+
placeholder="https://..."
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="space-y-3">
|
|
67
|
+
<input
|
|
68
|
+
type="text"
|
|
69
|
+
value={heading}
|
|
70
|
+
onChange={(e) => onChange("heading", e.target.value)}
|
|
71
|
+
className={`${inputClass} font-bold text-base`}
|
|
72
|
+
placeholder="Hero heading..."
|
|
73
|
+
/>
|
|
74
|
+
<textarea
|
|
75
|
+
value={subheading}
|
|
76
|
+
onChange={(e) => onChange("subheading", e.target.value)}
|
|
77
|
+
className={textareaClass}
|
|
78
|
+
placeholder="Hero subheading..."
|
|
79
|
+
/>
|
|
80
|
+
<div className="flex items-center gap-2">
|
|
81
|
+
<input
|
|
82
|
+
type="text"
|
|
83
|
+
value={ctaText}
|
|
84
|
+
onChange={(e) => onChange("ctaText", e.target.value)}
|
|
85
|
+
className="flex-1 px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
86
|
+
placeholder="CTA text..."
|
|
87
|
+
/>
|
|
88
|
+
<span className="text-[var(--kyro-text-muted)] text-xs">→</span>
|
|
89
|
+
<input
|
|
90
|
+
type="url"
|
|
91
|
+
value={ctaUrl}
|
|
92
|
+
onChange={(e) => onChange("ctaUrl", e.target.value)}
|
|
93
|
+
className="flex-1 px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent font-mono text-xs"
|
|
94
|
+
placeholder="https://..."
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export default HeroField;
|