@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,72 @@
1
+ import React from "react";
2
+ import {
3
+ useBlockById,
4
+ useBlockActions,
5
+ } from "../fields/extensions/blocksStore";
6
+ import { ChevronRight, X } from "lucide-react";
7
+ import { RelationshipBlockField } from "../fields/RelationshipBlockField";
8
+
9
+ export const RelationshipBlock: React.FC<{ block: any; index: number }> = ({
10
+ block,
11
+ index,
12
+ }) => {
13
+ const blockData = useBlockById(block.id);
14
+ const { updateBlock, removeBlock, moveBlock } = useBlockActions();
15
+
16
+ const data = blockData?.data ?? block.data ?? {};
17
+
18
+ const handleChange = (field: string, value: any) => {
19
+ updateBlock(block.id, { data: { ...data, [field]: value } });
20
+ };
21
+
22
+ return (
23
+ <div className="block-relationship border border-[var(--kyro-border)] rounded-md p-3 mb-2 relative group">
24
+ <div className="flex items-center justify-between mb-1">
25
+ <div className="flex items-center gap-2">
26
+ <span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
27
+ Relationship
28
+ </span>
29
+ <span className="text-[10px] text-[var(--kyro-text-muted)]">
30
+ → {data.relationTo || "pages"}
31
+ </span>
32
+ </div>
33
+ <div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
34
+ <button
35
+ type="button"
36
+ onClick={() => moveBlock(block.id, "up")}
37
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
38
+ title="Move up"
39
+ >
40
+ <ChevronRight className="w-3 h-3 rotate-90" />
41
+ </button>
42
+ <button
43
+ type="button"
44
+ onClick={() => moveBlock(block.id, "down")}
45
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
46
+ title="Move down"
47
+ >
48
+ <ChevronRight className="w-3 h-3" />
49
+ </button>
50
+ <button
51
+ type="button"
52
+ onClick={() => removeBlock(block.id)}
53
+ className="p-1 hover:bg-red-50 rounded text-red-500"
54
+ title="Remove"
55
+ >
56
+ <X className="w-3 h-3" />
57
+ </button>
58
+ </div>
59
+ </div>
60
+
61
+ <RelationshipBlockField
62
+ relationTo={data.relationTo || "pages"}
63
+ hasMany={data.hasMany || false}
64
+ selectedIds={Array.isArray(data.selectedIds) ? data.selectedIds : []}
65
+ selectedId={data.selectedId}
66
+ labelField={data.labelField || "title"}
67
+ onChange={handleChange}
68
+ compact
69
+ />
70
+ </div>
71
+ );
72
+ };
@@ -0,0 +1,66 @@
1
+ import React from "react";
2
+ import {
3
+ useBlockById,
4
+ useBlockActions,
5
+ } from "../fields/extensions/blocksStore";
6
+ import { ChevronRight, X, AlignLeft } from "lucide-react";
7
+ import PortableTextField from "../fields/PortableTextField";
8
+
9
+ export const RichTextBlock: React.FC<{ block: any; index: number }> = ({
10
+ block,
11
+ index,
12
+ }) => {
13
+ const blockData = useBlockById(block.id);
14
+ const { updateBlock, removeBlock, moveBlock } = useBlockActions();
15
+
16
+ const data = blockData?.data || block.data || {};
17
+
18
+ const handleChange = (newValue: any) => {
19
+ updateBlock(block.id, { data: { ...data, content: newValue } });
20
+ };
21
+
22
+ return (
23
+ <div className="block-richtext border border-[var(--kyro-border)] rounded-md p-3 mb-2 relative group">
24
+ <div className="flex items-center justify-between mb-2">
25
+ <div className="flex items-center gap-2">
26
+ <AlignLeft className="w-3.5 h-3.5 text-[var(--kyro-primary)]" />
27
+ <span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
28
+ Rich Text
29
+ </span>
30
+ </div>
31
+ <div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
32
+ <button
33
+ type="button"
34
+ onClick={() => moveBlock(block.id, "up")}
35
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
36
+ title="Move up"
37
+ >
38
+ <ChevronRight className="w-3 h-3 rotate-90" />
39
+ </button>
40
+ <button
41
+ type="button"
42
+ onClick={() => moveBlock(block.id, "down")}
43
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
44
+ title="Move down"
45
+ >
46
+ <ChevronRight className="w-3 h-3" />
47
+ </button>
48
+ <button
49
+ type="button"
50
+ onClick={() => removeBlock(block.id)}
51
+ className="p-1 hover:bg-red-50 rounded text-red-500"
52
+ title="Remove"
53
+ >
54
+ <X className="w-3 h-3" />
55
+ </button>
56
+ </div>
57
+ </div>
58
+
59
+ <PortableTextField
60
+ field={{ name: "content", label: "Content" }}
61
+ value={data.content}
62
+ onChange={handleChange}
63
+ />
64
+ </div>
65
+ );
66
+ };
@@ -0,0 +1,61 @@
1
+ import React from "react";
2
+ import {
3
+ useBlockById,
4
+ useBlockActions,
5
+ } from "../fields/extensions/blocksStore";
6
+ import { ChevronRight, X } from "lucide-react";
7
+ import { ChildrenField } from "../fields/ChildrenField";
8
+
9
+ export const VStackBlock: React.FC<{ block: any; index: number }> = ({
10
+ block,
11
+ index,
12
+ }) => {
13
+ const blockData = useBlockById(block.id);
14
+ const { updateBlock, removeBlock, moveBlock } = useBlockActions();
15
+
16
+ const data = blockData?.data ?? block.data ?? {};
17
+ const children = blockData?.children ?? block.children ?? [];
18
+
19
+ return (
20
+ <div className="block-vstack border border-[var(--kyro-border)] rounded-md p-3 mb-2 relative group">
21
+ <div className="flex items-center justify-between mb-1">
22
+ <span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
23
+ Stack
24
+ </span>
25
+ <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
26
+ <button
27
+ type="button"
28
+ onClick={() => moveBlock(block.id, "up")}
29
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
30
+ title="Move up"
31
+ >
32
+ <ChevronRight className="w-3 h-3 rotate-90" />
33
+ </button>
34
+ <button
35
+ type="button"
36
+ onClick={() => moveBlock(block.id, "down")}
37
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
38
+ title="Move down"
39
+ >
40
+ <ChevronRight className="w-3 h-3" />
41
+ </button>
42
+ <button
43
+ type="button"
44
+ onClick={() => removeBlock(block.id)}
45
+ className="p-1 hover:bg-red-50 rounded text-red-500"
46
+ title="Remove"
47
+ >
48
+ <X className="w-3 h-3" />
49
+ </button>
50
+ </div>
51
+ </div>
52
+
53
+ <ChildrenField
54
+ blockId={block.id}
55
+ children={children}
56
+ onUpdateChildren={(c) => updateBlock(block.id, { children: c })}
57
+ compact
58
+ />
59
+ </div>
60
+ );
61
+ };
@@ -0,0 +1,65 @@
1
+ import React from "react";
2
+ import {
3
+ useBlockById,
4
+ useBlockActions,
5
+ } from "../fields/extensions/blocksStore";
6
+ import { ChevronRight, X } from "lucide-react";
7
+ import { VideoField } from "../fields/VideoField";
8
+
9
+ export const VideoBlock: React.FC<{ block: any; index: number }> = ({
10
+ block,
11
+ index,
12
+ }) => {
13
+ const blockData = useBlockById(block.id);
14
+ const { updateBlock, removeBlock, moveBlock } = useBlockActions();
15
+
16
+ const data = blockData?.data ?? block.data ?? {};
17
+
18
+ const handleChange = (field: string, value: any) => {
19
+ updateBlock(block.id, { data: { ...data, [field]: value } });
20
+ };
21
+
22
+ return (
23
+ <div className="block-video border border-[var(--kyro-border)] rounded-md p-3 mb-2 relative group">
24
+ <div className="flex items-center justify-between mb-1">
25
+ <span className="text-xs font-semibold text-[var(--kyro-text-muted)] uppercase">
26
+ Video
27
+ </span>
28
+ <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
29
+ <button
30
+ type="button"
31
+ onClick={() => moveBlock(block.id, "up")}
32
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
33
+ title="Move up"
34
+ >
35
+ <ChevronRight className="w-3 h-3 rotate-90" />
36
+ </button>
37
+ <button
38
+ type="button"
39
+ onClick={() => moveBlock(block.id, "down")}
40
+ className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded"
41
+ title="Move down"
42
+ >
43
+ <ChevronRight className="w-3 h-3" />
44
+ </button>
45
+ <button
46
+ type="button"
47
+ onClick={() => removeBlock(block.id)}
48
+ className="p-1 hover:bg-red-50 rounded text-red-500"
49
+ title="Remove"
50
+ >
51
+ <X className="w-3 h-3" />
52
+ </button>
53
+ </div>
54
+ </div>
55
+
56
+ <VideoField
57
+ src={data.src || ""}
58
+ title={data.title || ""}
59
+ onChange={handleChange}
60
+ onUploadChange={(v) => handleChange("src", v)}
61
+ compact
62
+ />
63
+ </div>
64
+ );
65
+ };
@@ -0,0 +1,10 @@
1
+ export { ColumnsBlock } from './ColumnsBlock';
2
+ export { HeadingBlock } from './HeadingBlock';
3
+ export { ParagraphBlock } from './ParagraphBlock';
4
+ export { DividerBlock } from './DividerBlock';
5
+ export { ImageBlock } from './ImageBlock';
6
+ export { VideoBlock } from './VideoBlock';
7
+ export { ListBlock } from './ListBlock';
8
+ export { CodeBlock } from './CodeBlock';
9
+ export { LinkBlock } from './LinkBlock';
10
+ export { FileBlock } from './FileBlock';
@@ -0,0 +1,213 @@
1
+ import React from "react";
2
+ import { ChevronDown, ChevronUp, Plus, X } from "lucide-react";
3
+
4
+ interface AccordionItem {
5
+ title: string;
6
+ content: string;
7
+ }
8
+
9
+ interface AccordionFieldProps {
10
+ items?: AccordionItem[];
11
+ onChange: (items: AccordionItem[]) => void;
12
+ compact?: boolean;
13
+ }
14
+
15
+ export const AccordionField: React.FC<AccordionFieldProps> = ({
16
+ items = [],
17
+ onChange,
18
+ compact = false,
19
+ }) => {
20
+ const [openIndex, setOpenIndex] = React.useState<number | null>(0);
21
+
22
+ const handleTitleChange = (index: number, value: string) => {
23
+ const newItems = [...items];
24
+ newItems[index] = { ...newItems[index], title: value };
25
+ onChange(newItems);
26
+ };
27
+
28
+ const handleContentChange = (index: number, value: string) => {
29
+ const newItems = [...items];
30
+ newItems[index] = { ...newItems[index], content: value };
31
+ onChange(newItems);
32
+ };
33
+
34
+ const handleRemove = (index: number) => {
35
+ const newItems = items.filter((_, i) => i !== index);
36
+ onChange(newItems);
37
+ if (openIndex === index) setOpenIndex(null);
38
+ else if (openIndex !== null && openIndex > index)
39
+ setOpenIndex(openIndex - 1);
40
+ };
41
+
42
+ const handleAdd = () => {
43
+ onChange([...items, { title: `Item ${items.length + 1}`, content: "" }]);
44
+ setOpenIndex(items.length);
45
+ };
46
+
47
+ const baseInputClass =
48
+ "w-full px-3 py-2 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";
49
+ const smallInputClass =
50
+ "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";
51
+
52
+ if (compact) {
53
+ return (
54
+ <div className="space-y-2">
55
+ {items.length === 0 ? (
56
+ <div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-lg">
57
+ No items. Click "Add Item" to create one.
58
+ </div>
59
+ ) : (
60
+ <div className="space-y-1.5">
61
+ {items.map((item: AccordionItem, index: number) => {
62
+ const isOpen = openIndex === index;
63
+ return (
64
+ <div
65
+ key={index}
66
+ className="border border-[var(--kyro-border)] rounded-lg overflow-hidden group"
67
+ >
68
+ <button
69
+ type="button"
70
+ onClick={() => setOpenIndex(isOpen ? null : index)}
71
+ className="w-full flex items-center justify-between p-2.5 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-sidebar-active)]/10 transition-colors"
72
+ >
73
+ <span className="text-sm font-medium text-[var(--kyro-text-primary)] truncate">
74
+ {item.title || `Item ${index + 1}`}
75
+ </span>
76
+ <div className="flex items-center gap-1">
77
+ <button
78
+ type="button"
79
+ onClick={(e) => {
80
+ e.stopPropagation();
81
+ handleRemove(index);
82
+ }}
83
+ className="opacity-0 group-hover:opacity-100 p-1 hover:bg-[var(--kyro-danger-bg)] rounded text-[var(--kyro-error)] transition-opacity"
84
+ title="Remove"
85
+ >
86
+ <X className="w-3.5 h-3.5" />
87
+ </button>
88
+ {isOpen ? (
89
+ <ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
90
+ ) : (
91
+ <ChevronDown className="w-4 h-4 text-[var(--kyro-text-muted)]" />
92
+ )}
93
+ </div>
94
+ </button>
95
+ {isOpen && (
96
+ <div className="p-2.5 bg-[var(--kyro-surface)] space-y-2">
97
+ <input
98
+ type="text"
99
+ value={item.title || ""}
100
+ onChange={(e) =>
101
+ handleTitleChange(index, e.target.value)
102
+ }
103
+ onClick={(e) => e.stopPropagation()}
104
+ className={smallInputClass}
105
+ placeholder="Item title..."
106
+ />
107
+ <textarea
108
+ value={item.content || ""}
109
+ onChange={(e) =>
110
+ handleContentChange(index, e.target.value)
111
+ }
112
+ onClick={(e) => e.stopPropagation()}
113
+ className={`${smallInputClass} min-h-[60px] resize-none`}
114
+ placeholder="Item content..."
115
+ />
116
+ </div>
117
+ )}
118
+ </div>
119
+ );
120
+ })}
121
+ </div>
122
+ )}
123
+ <button
124
+ type="button"
125
+ onClick={handleAdd}
126
+ className="flex items-center justify-center gap-1.5 w-full px-3 py-2 text-xs font-medium rounded-lg border border-dashed border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:border-[var(--kyro-sidebar-active)] hover:text-[var(--kyro-text-primary)] transition-colors"
127
+ >
128
+ <Plus className="w-3.5 h-3.5" />
129
+ Add Item
130
+ </button>
131
+ </div>
132
+ );
133
+ }
134
+
135
+ return (
136
+ <div className="space-y-2">
137
+ {items.length === 0 ? (
138
+ <div className="text-center py-4 text-[var(--kyro-text-muted)] text-sm border border-dashed border-[var(--kyro-border)] rounded-lg">
139
+ No items. Click "Add Item" to create one.
140
+ </div>
141
+ ) : (
142
+ <div className="space-y-2">
143
+ {items.map((item: AccordionItem, index: number) => {
144
+ const isOpen = openIndex === index;
145
+ return (
146
+ <div
147
+ key={index}
148
+ className="border border-[var(--kyro-border)] rounded-lg overflow-hidden group"
149
+ >
150
+ <button
151
+ type="button"
152
+ onClick={() => setOpenIndex(isOpen ? null : index)}
153
+ className="w-full flex items-center justify-between p-3 bg-[var(--kyro-surface-accent)] hover:bg-[var(--kyro-sidebar-active)]/10 transition-colors"
154
+ >
155
+ <span className="text-sm font-medium text-[var(--kyro-text-primary)] truncate">
156
+ {item.title || `Item ${index + 1}`}
157
+ </span>
158
+ <div className="flex items-center gap-1">
159
+ <button
160
+ type="button"
161
+ onClick={(e) => {
162
+ e.stopPropagation();
163
+ handleRemove(index);
164
+ }}
165
+ className="opacity-0 group-hover:opacity-100 p-1.5 hover:bg-[var(--kyro-danger-bg)] rounded text-[var(--kyro-error)] transition-opacity"
166
+ title="Remove"
167
+ >
168
+ <X className="w-4 h-4" />
169
+ </button>
170
+ {isOpen ? (
171
+ <ChevronUp className="w-4 h-4 text-[var(--kyro-text-muted)]" />
172
+ ) : (
173
+ <ChevronDown className="w-4 h-4 text-[var(--kyro-text-muted)]" />
174
+ )}
175
+ </div>
176
+ </button>
177
+ {isOpen && (
178
+ <div className="p-3 bg-[var(--kyro-surface)] space-y-2">
179
+ <input
180
+ type="text"
181
+ value={item.title || ""}
182
+ onChange={(e) => handleTitleChange(index, e.target.value)}
183
+ className={baseInputClass}
184
+ placeholder="Item title..."
185
+ />
186
+ <textarea
187
+ value={item.content || ""}
188
+ onChange={(e) =>
189
+ handleContentChange(index, e.target.value)
190
+ }
191
+ className={`${baseInputClass} min-h-[60px] resize-none`}
192
+ placeholder="Item content..."
193
+ />
194
+ </div>
195
+ )}
196
+ </div>
197
+ );
198
+ })}
199
+ </div>
200
+ )}
201
+ <button
202
+ type="button"
203
+ onClick={handleAdd}
204
+ className="flex items-center justify-center gap-1.5 w-full px-3 py-2 text-xs font-medium rounded-lg border border-dashed border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:border-[var(--kyro-sidebar-active)] hover:text-[var(--kyro-text-primary)] transition-colors"
205
+ >
206
+ <Plus className="w-3.5 h-3.5" />
207
+ Add Item
208
+ </button>
209
+ </div>
210
+ );
211
+ };
212
+
213
+ export default AccordionField;