@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.
Files changed (179) hide show
  1. package/README.md +149 -51
  2. package/package.json +54 -5
  3. package/src/collections/auth/index.ts +2 -2
  4. package/src/collections/portfolio/index.ts +343 -0
  5. package/src/components/ActionBar.tsx +153 -16
  6. package/src/components/Admin.tsx +137 -28
  7. package/src/components/ApiExplorer.tsx +325 -0
  8. package/src/components/ApiKeysManager.tsx +563 -0
  9. package/src/components/AuditLogsPage.tsx +664 -0
  10. package/src/components/AutoForm.tsx +2155 -770
  11. package/src/components/BrandingHub.tsx +267 -0
  12. package/src/components/BulkActionsBar.tsx +3 -3
  13. package/src/components/CreateView.tsx +4 -4
  14. package/src/components/Dashboard.tsx +393 -0
  15. package/src/components/DetailView.tsx +200 -58
  16. package/src/components/DeveloperCenter.tsx +403 -0
  17. package/src/components/EnhancedListView.tsx +890 -0
  18. package/src/components/GraphQLExplorer.tsx +675 -0
  19. package/src/components/GraphQLPlayground.tsx +627 -0
  20. package/src/components/ListView.tsx +192 -54
  21. package/src/components/MediaGallery.tsx +1569 -0
  22. package/src/components/Modal.tsx +206 -0
  23. package/src/components/RestPlayground.tsx +951 -0
  24. package/src/components/Sidebar.astro +237 -0
  25. package/src/components/ThemeProvider.tsx +8 -2
  26. package/src/components/UserManagement.tsx +204 -0
  27. package/src/components/VersionHistoryPanel.tsx +3 -3
  28. package/src/components/WebhookManager.tsx +608 -0
  29. package/src/components/blocks/AccordionBlock.tsx +65 -0
  30. package/src/components/blocks/ArrayBlock.tsx +84 -0
  31. package/src/components/blocks/BlockEditModal.tsx +363 -0
  32. package/src/components/blocks/ButtonBlock.tsx +64 -0
  33. package/src/components/blocks/ChildBlocksTree.tsx +551 -0
  34. package/src/components/blocks/CodeBlock.tsx +114 -0
  35. package/src/components/blocks/ColumnsBlock.tsx +93 -0
  36. package/src/components/blocks/DividerBlock.tsx +43 -0
  37. package/src/components/blocks/FileBlock.tsx +63 -0
  38. package/src/components/blocks/HeadingBlock.tsx +59 -0
  39. package/src/components/blocks/HeroBlock.tsx +99 -0
  40. package/src/components/blocks/ImageBlock.tsx +82 -0
  41. package/src/components/blocks/LinkBlock.tsx +65 -0
  42. package/src/components/blocks/ListBlock.tsx +60 -0
  43. package/src/components/blocks/ParagraphBlock.tsx +61 -0
  44. package/src/components/blocks/RelationshipBlock.tsx +72 -0
  45. package/src/components/blocks/RichTextBlock.tsx +66 -0
  46. package/src/components/blocks/VStackBlock.tsx +61 -0
  47. package/src/components/blocks/VideoBlock.tsx +65 -0
  48. package/src/components/blocks/index.ts +10 -0
  49. package/src/components/fields/AccordionField.tsx +213 -0
  50. package/src/components/fields/ArrayField.tsx +241 -0
  51. package/src/components/fields/BlocksField.tsx +323 -0
  52. package/src/components/fields/ButtonField.tsx +53 -0
  53. package/src/components/fields/CheckboxField.tsx +18 -8
  54. package/src/components/fields/ChildrenField.tsx +48 -0
  55. package/src/components/fields/CodeField.tsx +294 -0
  56. package/src/components/fields/ColumnsField.tsx +137 -0
  57. package/src/components/fields/DateField.tsx +24 -12
  58. package/src/components/fields/EditorClient.tsx +537 -0
  59. package/src/components/fields/HeadingField.tsx +31 -0
  60. package/src/components/fields/HeroField.tsx +101 -0
  61. package/src/components/fields/JSONField.tsx +341 -0
  62. package/src/components/fields/LinkField.tsx +81 -0
  63. package/src/components/fields/ListField.tsx +74 -0
  64. package/src/components/fields/MarkdownField.tsx +260 -0
  65. package/src/components/fields/NumberField.tsx +25 -13
  66. package/src/components/fields/PortableTextField.tsx +155 -0
  67. package/src/components/fields/PortableTextRenderer.tsx +68 -0
  68. package/src/components/fields/RelationshipBlockField.tsx +233 -0
  69. package/src/components/fields/RelationshipField.tsx +278 -60
  70. package/src/components/fields/SelectField.tsx +28 -16
  71. package/src/components/fields/TextField.tsx +31 -15
  72. package/src/components/fields/UploadField.tsx +613 -0
  73. package/src/components/fields/VideoField.tsx +73 -0
  74. package/src/components/fields/extensions/blockComponents.tsx +247 -0
  75. package/src/components/fields/extensions/blocksStore.ts +273 -0
  76. package/src/components/fields/index.ts +24 -0
  77. package/src/components/index.ts +1 -2
  78. package/src/components/layout/Header.tsx +2 -2
  79. package/src/components/layout/Layout.tsx +3 -3
  80. package/src/components/ui/Badge.tsx +9 -4
  81. package/src/components/ui/BlockDrawer.tsx +79 -0
  82. package/src/components/ui/Button.tsx +1 -1
  83. package/src/components/ui/CommandPalette.tsx +362 -0
  84. package/src/components/ui/CommandPaletteWrapper.tsx +97 -0
  85. package/src/components/ui/Dropdown.tsx +1 -1
  86. package/src/components/ui/Modal.tsx +37 -12
  87. package/src/components/ui/PromptModal.tsx +94 -0
  88. package/src/components/ui/SlidePanel.tsx +43 -16
  89. package/src/components/ui/Toast.tsx +80 -14
  90. package/src/env.d.ts +16 -0
  91. package/src/env.ts +20 -0
  92. package/src/index.ts +0 -1
  93. package/src/layouts/AdminLayout.astro +164 -170
  94. package/src/layouts/AuthLayout.astro +23 -6
  95. package/src/lib/MediaService.ts +541 -0
  96. package/src/lib/api.ts +163 -0
  97. package/src/lib/auth/sqlite-adapter.ts +319 -0
  98. package/src/lib/config.ts +23 -7
  99. package/src/lib/dataStore.ts +188 -73
  100. package/src/lib/date-utils.ts +69 -0
  101. package/src/lib/db/adapter.ts +54 -0
  102. package/src/lib/db/drizzle-mysql-adapter.ts +194 -0
  103. package/src/lib/db/drizzle-mysql-auth-adapter.ts +327 -0
  104. package/src/lib/db/drizzle-postgres-adapter.ts +202 -0
  105. package/src/lib/db/drizzle-postgres-auth-adapter.ts +304 -0
  106. package/src/lib/db/drizzle-sqlite-adapter.ts +227 -0
  107. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +548 -0
  108. package/src/lib/db/index.ts +449 -0
  109. package/src/lib/db/mongodb-adapter.ts +207 -0
  110. package/src/lib/db/mongodb-auth-adapter.ts +305 -0
  111. package/src/lib/db/schema/mysql-auth.ts +113 -0
  112. package/src/lib/db/schema/mysql-content.ts +20 -0
  113. package/src/lib/db/schema/postgres-auth.ts +116 -0
  114. package/src/lib/db/schema/postgres-content.ts +35 -0
  115. package/src/lib/db/schema/postgres-media.ts +52 -0
  116. package/src/lib/db/schema/postgres-settings.ts +11 -0
  117. package/src/lib/db/schema/sqlite-auth.ts +112 -0
  118. package/src/lib/db/schema/sqlite-content.ts +20 -0
  119. package/src/lib/db/version-adapter.ts +248 -0
  120. package/src/lib/graphql/index.ts +1 -0
  121. package/src/lib/graphql/schema.ts +443 -0
  122. package/src/lib/i18n.tsx +353 -0
  123. package/src/lib/rate-limit.ts +267 -0
  124. package/src/lib/slugify.ts +15 -0
  125. package/src/lib/storage.ts +374 -0
  126. package/src/lib/store.ts +85 -0
  127. package/src/lib/validation.ts +250 -0
  128. package/src/middleware.ts +70 -11
  129. package/src/pages/[collection]/[id].astro +178 -122
  130. package/src/pages/[collection]/index.astro +24 -156
  131. package/src/pages/admin/api-explorer.astro +98 -0
  132. package/src/pages/admin/graphql-explorer.astro +40 -0
  133. package/src/pages/admin/graphql.astro +97 -0
  134. package/src/pages/admin/index.astro +200 -139
  135. package/src/pages/admin/keys.astro +8 -0
  136. package/src/pages/admin/rest-playground.astro +44 -0
  137. package/src/pages/admin/webhooks.astro +8 -0
  138. package/src/pages/api/[collection]/[id]/publish.ts +52 -0
  139. package/src/pages/api/[collection]/[id]/unpublish.ts +42 -0
  140. package/src/pages/api/[collection]/[id]/versions.ts +66 -0
  141. package/src/pages/api/[collection]/[id].ts +114 -159
  142. package/src/pages/api/[collection]/index.ts +150 -230
  143. package/src/pages/api/auth/[id].ts +48 -69
  144. package/src/pages/api/auth/audit-logs.ts +20 -43
  145. package/src/pages/api/auth/login.ts +159 -45
  146. package/src/pages/api/auth/logout.ts +42 -24
  147. package/src/pages/api/auth/refresh.ts +119 -0
  148. package/src/pages/api/auth/register.ts +110 -40
  149. package/src/pages/api/auth/users.ts +22 -97
  150. package/src/pages/api/collections.ts +59 -0
  151. package/src/pages/api/globals/[slug]/test.ts +172 -0
  152. package/src/pages/api/globals/[slug].ts +42 -0
  153. package/src/pages/api/graphql.ts +90 -0
  154. package/src/pages/api/health.ts +417 -40
  155. package/src/pages/api/keys/[id].ts +26 -0
  156. package/src/pages/api/keys/index.ts +75 -0
  157. package/src/pages/api/media/[id].ts +309 -0
  158. package/src/pages/api/media/folders.ts +609 -0
  159. package/src/pages/api/media/index.ts +146 -0
  160. package/src/pages/api/media/resize.ts +267 -0
  161. package/src/pages/api/search.ts +82 -0
  162. package/src/pages/api/slug-availability.ts +70 -0
  163. package/src/pages/api/storage-config.ts +20 -0
  164. package/src/pages/api/storage-status.ts +206 -0
  165. package/src/pages/api/upload.ts +334 -0
  166. package/src/pages/api/webhooks/index.ts +71 -0
  167. package/src/pages/audit/index.astro +2 -104
  168. package/src/pages/login.astro +11 -11
  169. package/src/pages/media.astro +10 -0
  170. package/src/pages/preview/[collection]/[id].astro +178 -0
  171. package/src/pages/register.astro +13 -13
  172. package/src/pages/roles/index.astro +21 -21
  173. package/src/pages/settings/[slug].astro +162 -0
  174. package/src/pages/settings/index.astro +9 -0
  175. package/src/pages/users/[id].astro +29 -21
  176. package/src/pages/users/index.astro +22 -17
  177. package/src/pages/users/new.astro +18 -17
  178. package/src/styles/main.css +563 -128
  179. package/src/components/layout/Sidebar.tsx +0 -497
@@ -0,0 +1,341 @@
1
+ import React, { useState, useCallback, useMemo, useEffect } from "react";
2
+ import type { JSONField as JSONFieldType } from "@kyro-cms/core/client";
3
+
4
+ interface JSONFieldProps {
5
+ field: JSONFieldType;
6
+ value?: any;
7
+ onChange?: (value: any) => void;
8
+ error?: string;
9
+ disabled?: boolean;
10
+ }
11
+
12
+ interface TreeNode {
13
+ key: string;
14
+ value: any;
15
+ type: "object" | "array" | "string" | "number" | "boolean" | "null";
16
+ path: string[];
17
+ collapsed?: boolean;
18
+ }
19
+
20
+ export const JSONField: React.FC<JSONFieldProps> = ({
21
+ field,
22
+ value,
23
+ onChange,
24
+ error,
25
+ disabled,
26
+ }) => {
27
+ const [textValue, setTextValue] = useState<string>(() => {
28
+ if (typeof value === "string") return value;
29
+ return JSON.stringify(value || {}, null, 2);
30
+ });
31
+ const [parseError, setParseError] = useState<string | null>(null);
32
+ const [viewMode, setViewMode] = useState<"text" | "tree">("text");
33
+ const [isMounted, setIsMounted] = useState(false);
34
+
35
+ useEffect(() => {
36
+ setIsMounted(true);
37
+ }, []);
38
+
39
+ const handleTextChange = useCallback(
40
+ (text: string) => {
41
+ setTextValue(text);
42
+ try {
43
+ const parsed = JSON.parse(text);
44
+ setParseError(null);
45
+ onChange?.(parsed);
46
+ } catch (e) {
47
+ if (text.trim()) {
48
+ setParseError((e as Error).message);
49
+ }
50
+ }
51
+ },
52
+ [onChange],
53
+ );
54
+
55
+ const formatJSON = useCallback(() => {
56
+ try {
57
+ const parsed = JSON.parse(textValue);
58
+ const formatted = JSON.stringify(parsed, null, 2);
59
+ setTextValue(formatted);
60
+ onChange?.(parsed);
61
+ setParseError(null);
62
+ } catch (e) {
63
+ setParseError((e as Error).message);
64
+ }
65
+ }, [textValue, onChange]);
66
+
67
+ const minifyJSON = useCallback(() => {
68
+ try {
69
+ const parsed = JSON.parse(textValue);
70
+ const minified = JSON.stringify(parsed);
71
+ setTextValue(minified);
72
+ onChange?.(parsed);
73
+ setParseError(null);
74
+ } catch (e) {
75
+ setParseError((e as Error).message);
76
+ }
77
+ }, [textValue, onChange]);
78
+
79
+ const treeData = useMemo(() => {
80
+ try {
81
+ const parsed = JSON.parse(textValue);
82
+ return buildTree(parsed, []);
83
+ } catch {
84
+ return null;
85
+ }
86
+ }, [textValue]);
87
+
88
+ if (!isMounted) {
89
+ return (
90
+ <div className="kyro-form-field">
91
+ <label className="kyro-form-label">
92
+ {field.label || field.name}
93
+ {field.required && (
94
+ <span className="kyro-form-label-required">*</span>
95
+ )}
96
+ </label>
97
+ <div className="h-[200px] bg-[var(--kyro-surface)] animate-pulse rounded-md border border-[var(--kyro-border)]" />
98
+ </div>
99
+ );
100
+ }
101
+
102
+ return (
103
+ <div className="kyro-form-field">
104
+ <div className="flex items-center justify-between mb-2">
105
+ <label className="kyro-form-label">
106
+ {field.label || field.name}
107
+ {field.required && (
108
+ <span className="kyro-form-label-required">*</span>
109
+ )}
110
+ </label>
111
+ <div className="flex items-center gap-2">
112
+ {/* View mode toggle */}
113
+ <div className="flex rounded-md overflow-hidden border border-[var(--kyro-border)]">
114
+ <button
115
+ type="button"
116
+ onClick={() => setViewMode("text")}
117
+ className={`px-3 py-1 text-xs font-medium transition-colors ${
118
+ viewMode === "text"
119
+ ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)]"
120
+ : "bg-[var(--kyro-surface)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
121
+ }`}
122
+ >
123
+ Text
124
+ </button>
125
+ <button
126
+ type="button"
127
+ onClick={() => setViewMode("tree")}
128
+ className={`px-3 py-1 text-xs font-medium transition-colors ${
129
+ viewMode === "tree"
130
+ ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)]"
131
+ : "bg-[var(--kyro-surface)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
132
+ }`}
133
+ >
134
+ Tree
135
+ </button>
136
+ </div>
137
+ </div>
138
+ </div>
139
+
140
+ {/* Action buttons */}
141
+ <div className="flex items-center gap-2 mb-2">
142
+ <button
143
+ type="button"
144
+ onClick={formatJSON}
145
+ className="px-3 py-1.5 text-xs font-medium bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] rounded-md hover:bg-[var(--kyro-surface)] hover:text-[var(--kyro-text-primary)] transition-colors"
146
+ >
147
+ Format
148
+ </button>
149
+ <button
150
+ type="button"
151
+ onClick={minifyJSON}
152
+ className="px-3 py-1.5 text-xs font-medium bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] rounded-md hover:bg-[var(--kyro-surface)] hover:text-[var(--kyro-text-primary)] transition-colors"
153
+ >
154
+ Minify
155
+ </button>
156
+ <button
157
+ type="button"
158
+ onClick={() => {
159
+ navigator.clipboard.writeText(textValue);
160
+ }}
161
+ className="px-3 py-1.5 text-xs font-medium bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] rounded-md hover:bg-[var(--kyro-surface)] hover:text-[var(--kyro-text-primary)] transition-colors"
162
+ >
163
+ Copy
164
+ </button>
165
+ {parseError && (
166
+ <span className="text-xs text-[var(--kyro-error)] ml-auto">
167
+ {parseError}
168
+ </span>
169
+ )}
170
+ </div>
171
+
172
+ {/* Editor area */}
173
+ <div
174
+ className={`border border-[var(--kyro-border)] rounded-md overflow-hidden ${
175
+ disabled ? "opacity-50 cursor-not-allowed" : ""
176
+ } ${error || parseError ? "border-[var(--kyro-error)]" : ""}`}
177
+ >
178
+ {viewMode === "text" ? (
179
+ <textarea
180
+ value={textValue}
181
+ onChange={(e) => handleTextChange(e.target.value)}
182
+ disabled={disabled}
183
+ rows={8}
184
+ className={`w-full p-4 font-mono text-sm resize-y focus:outline-none bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]`}
185
+ placeholder='{"key": "value"}'
186
+ />
187
+ ) : (
188
+ <div
189
+ className={`p-4 max-h-[400px] overflow-auto bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]`}
190
+ >
191
+ {treeData ? (
192
+ <TreeView data={treeData} />
193
+ ) : (
194
+ <p className="text-sm text-[var(--kyro-text-muted)] italic">
195
+ Invalid JSON - switch to Text view to fix
196
+ </p>
197
+ )}
198
+ </div>
199
+ )}
200
+ </div>
201
+
202
+ {field.admin?.description && !error && !parseError && (
203
+ <p className="kyro-form-help">{field.admin.description}</p>
204
+ )}
205
+ {(error || parseError) && (
206
+ <p className="kyro-form-error">{error || parseError}</p>
207
+ )}
208
+ </div>
209
+ );
210
+ };
211
+
212
+ // Tree view component
213
+ const TreeView: React.FC<{ data: TreeNode }> = ({ data }) => {
214
+ const [collapsed, setCollapsed] = useState(false);
215
+
216
+ const renderValue = (node: TreeNode) => {
217
+ if (node.type === "object") {
218
+ const entries = Object.entries(node.value);
219
+ return (
220
+ <div className="pl-4 border-l border-[var(--kyro-border)]">
221
+ {entries.map(([key, val]) => (
222
+ <TreeNodeView
223
+ key={key}
224
+ name={key}
225
+ value={val}
226
+ path={[...node.path, key]}
227
+ />
228
+ ))}
229
+ </div>
230
+ );
231
+ }
232
+ if (node.type === "array") {
233
+ return (
234
+ <div className="pl-4 border-l border-[var(--kyro-border)]">
235
+ {node.value.map((item: any, index: number) => (
236
+ <TreeNodeView
237
+ key={index}
238
+ name={String(index)}
239
+ value={item}
240
+ path={[...node.path, String(index)]}
241
+ />
242
+ ))}
243
+ </div>
244
+ );
245
+ }
246
+ return null;
247
+ };
248
+
249
+ const getTypeColor = (type: string) => {
250
+ switch (type) {
251
+ case "string":
252
+ return "text-green-600";
253
+ case "number":
254
+ return "text-blue-600";
255
+ case "boolean":
256
+ return "text-purple-600";
257
+ case "null":
258
+ return "text-gray-500";
259
+ default:
260
+ return "text-[var(--kyro-text-primary)]";
261
+ }
262
+ };
263
+
264
+ const getValueDisplay = (item: TreeNode) => {
265
+ if (item.type === "string") return `"${item.value}"`;
266
+ if (item.type === "null") return "null";
267
+ return String(item.value);
268
+ };
269
+
270
+ const getPreview = (item: TreeNode) => {
271
+ if (item.type === "object") {
272
+ const keys = Object.keys(item.value);
273
+ return `{${keys.length} ${keys.length === 1 ? "key" : "keys"}}`;
274
+ }
275
+ if (item.type === "array") {
276
+ return `[${item.value.length} ${item.value.length === 1 ? "item" : "items"}]`;
277
+ }
278
+ return null;
279
+ };
280
+
281
+ const isExpandable = data.type === "object" || data.type === "array";
282
+
283
+ return (
284
+ <div className="py-0.5">
285
+ <div
286
+ className="flex items-center gap-2 cursor-pointer hover:bg-[var(--kyro-surface-accent)] rounded px-1 -ml-1"
287
+ onClick={() => isExpandable && setCollapsed(!collapsed)}
288
+ >
289
+ {isExpandable && (
290
+ <span className="text-[var(--kyro-text-muted)] text-xs">
291
+ {collapsed ? "▶" : "▼"}
292
+ </span>
293
+ )}
294
+ <span className="font-medium text-[var(--kyro-text-primary)]">
295
+ {data.key}
296
+ </span>
297
+ <span className="text-[var(--kyro-text-muted)]">:</span>
298
+ {isExpandable ? (
299
+ <span className="text-xs text-[var(--kyro-text-muted)]">
300
+ {collapsed ? getPreview(data) : data.type === "object" ? "{" : "["}
301
+ </span>
302
+ ) : (
303
+ <span className={`font-mono text-sm ${getTypeColor(data.type)}`}>
304
+ {getValueDisplay(data)}
305
+ </span>
306
+ )}
307
+ </div>
308
+ {!collapsed && isExpandable && renderValue(data)}
309
+ {collapsed && isExpandable && (
310
+ <span className="text-[var(--kyro-text-muted)] ml-6 text-xs">
311
+ {data.type === "object" ? "}" : "]"}
312
+ </span>
313
+ )}
314
+ </div>
315
+ );
316
+ };
317
+
318
+ const TreeNodeView: React.FC<{ name: string; value: any; path: string[] }> = ({
319
+ name,
320
+ value,
321
+ path,
322
+ }) => {
323
+ const type = getType(value);
324
+ const treeNode: TreeNode = { key: name, value, type, path };
325
+ return <TreeView data={treeNode} />;
326
+ };
327
+
328
+ function getType(value: any): TreeNode["type"] {
329
+ if (value === null) return "null";
330
+ if (Array.isArray(value)) return "array";
331
+ return typeof value as TreeNode["type"];
332
+ }
333
+
334
+ function buildTree(value: any, path: string[]): TreeNode {
335
+ return {
336
+ key: path[path.length - 1] || "root",
337
+ value,
338
+ type: getType(value),
339
+ path,
340
+ };
341
+ }
@@ -0,0 +1,81 @@
1
+ import React from "react";
2
+ import { ExternalLink } from "lucide-react";
3
+
4
+ interface LinkFieldProps {
5
+ text?: string;
6
+ url?: string;
7
+ onChange: (field: string, value: string) => void;
8
+ compact?: boolean;
9
+ }
10
+
11
+ export const LinkField: React.FC<LinkFieldProps> = ({
12
+ text = "",
13
+ url = "",
14
+ onChange,
15
+ compact = false,
16
+ }) => {
17
+ return (
18
+ <div className={compact ? "flex items-center gap-2" : "space-y-2"}>
19
+ {compact ? (
20
+ <>
21
+ <input
22
+ type="text"
23
+ value={text}
24
+ onChange={(e) => onChange("text", e.target.value)}
25
+ className="flex-1 min-w-0 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"
26
+ placeholder="Link text..."
27
+ />
28
+ <span className="text-[var(--kyro-text-muted)] text-xs">→</span>
29
+ <input
30
+ type="url"
31
+ value={url}
32
+ onChange={(e) => onChange("url", e.target.value)}
33
+ className="flex-1 min-w-0 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"
34
+ placeholder="https://..."
35
+ />
36
+ {text && url && (
37
+ <a
38
+ href={url}
39
+ target="_blank"
40
+ rel="noopener noreferrer"
41
+ className="shrink-0 p-1.5 rounded text-[var(--kyro-text-muted)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-surface-accent)] transition-colors"
42
+ title={url}
43
+ >
44
+ <ExternalLink className="w-3.5 h-3.5" />
45
+ </a>
46
+ )}
47
+ </>
48
+ ) : (
49
+ <>
50
+ <input
51
+ type="text"
52
+ value={text}
53
+ onChange={(e) => onChange("text", e.target.value)}
54
+ className="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"
55
+ placeholder="Link text..."
56
+ />
57
+ <input
58
+ type="url"
59
+ value={url}
60
+ onChange={(e) => onChange("url", e.target.value)}
61
+ className="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 font-mono text-xs"
62
+ placeholder="https://..."
63
+ />
64
+ {text && url && (
65
+ <a
66
+ href={url}
67
+ target="_blank"
68
+ rel="noopener noreferrer"
69
+ className="p-2 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-surface-accent)] transition-colors"
70
+ title={url}
71
+ >
72
+ <ExternalLink className="w-4 h-4" />
73
+ </a>
74
+ )}
75
+ </>
76
+ )}
77
+ </div>
78
+ );
79
+ };
80
+
81
+ export default LinkField;
@@ -0,0 +1,74 @@
1
+ import React from "react";
2
+
3
+ interface ListFieldProps {
4
+ items?: string[];
5
+ onChange: (items: string[]) => void;
6
+ compact?: boolean;
7
+ }
8
+
9
+ export const ListField: React.FC<ListFieldProps> = ({
10
+ items = [],
11
+ onChange,
12
+ compact = false,
13
+ }) => {
14
+ const [inputValue, setInputValue] = React.useState("");
15
+
16
+ const handleAdd = () => {
17
+ if (inputValue.trim()) {
18
+ onChange([...items, inputValue.trim()]);
19
+ setInputValue("");
20
+ }
21
+ };
22
+
23
+ const handleKeyDown = (e: React.KeyboardEvent) => {
24
+ if (e.key === "Enter") {
25
+ e.preventDefault();
26
+ handleAdd();
27
+ }
28
+ };
29
+
30
+ const handleRemove = (index: number) => {
31
+ onChange(items.filter((_, i) => i !== index));
32
+ };
33
+
34
+ const inputClass = compact
35
+ ? "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"
36
+ : "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";
37
+
38
+ return (
39
+ <div className={compact ? "space-y-1.5" : "space-y-2"}>
40
+ {items.length === 0 ? (
41
+ <div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-lg">
42
+ No items. Type below to add.
43
+ </div>
44
+ ) : (
45
+ <div className="space-y-1">
46
+ {items.map((item, index) => (
47
+ <div key={index} className="flex items-center gap-2 group/item">
48
+ <span className="text-sm text-[var(--kyro-text-primary)] flex-1">
49
+ • {item}
50
+ </span>
51
+ <button
52
+ type="button"
53
+ onClick={() => handleRemove(index)}
54
+ className="opacity-0 group-hover/item:opacity-100 p-1 hover:bg-[var(--kyro-danger-bg)] rounded text-[var(--kyro-error)] transition-opacity"
55
+ >
56
+ ×
57
+ </button>
58
+ </div>
59
+ ))}
60
+ </div>
61
+ )}
62
+ <input
63
+ type="text"
64
+ value={inputValue}
65
+ onChange={(e) => setInputValue(e.target.value)}
66
+ onKeyDown={handleKeyDown}
67
+ className={inputClass}
68
+ placeholder="Type and press Enter to add..."
69
+ />
70
+ </div>
71
+ );
72
+ };
73
+
74
+ export default ListField;