@kyro-cms/admin 0.10.13 → 0.10.18

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kyro-cms/admin",
3
- "version": "0.10.13",
3
+ "version": "0.10.18",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },
@@ -272,7 +272,9 @@ export function AutoForm({
272
272
 
273
273
  const result = await resp.json();
274
274
  if (result.data) {
275
- const restoredData = { ...formData, ...result.data };
275
+ // Omit historical timestamps so OCC doesn't panic on save
276
+ const { updatedAt, createdAt, ...historicalData } = result.data;
277
+ const restoredData = { ...formData, ...historicalData };
276
278
  setFormData(restoredData);
277
279
  useAutoFormStore.getState().loadDocument(restoredData, restoredData);
278
280
  onActionSuccess?.("Version restored successfully");
@@ -540,6 +542,8 @@ export function AutoForm({
540
542
  setLocalSaveStatus("saving");
541
543
 
542
544
  try {
545
+ let dataToPublish = { ...formData };
546
+
543
547
  if (isNewDoc && !globalSlug) {
544
548
  // Create then immediately publish
545
549
  const data = normalizeUploadFields({ ...formData }) as Record<string, unknown>;
@@ -560,10 +564,11 @@ export function AutoForm({
560
564
  const savedData = result.data || data;
561
565
  setFormData({ ...formData, ...savedData });
562
566
  setLastSavedData({ ...formData, ...savedData });
567
+ dataToPublish = { ...formData, ...savedData };
563
568
  }
564
569
 
565
570
  // Save and publish (X-Draft: false writes to main doc + versions table)
566
- const data = normalizeUploadFields({ ...formData }) as Record<string, unknown>;
571
+ const data = normalizeUploadFields(dataToPublish) as Record<string, unknown>;
567
572
  const response = await saveDocument(data, false);
568
573
 
569
574
  if (response?.ok) {
@@ -33,7 +33,7 @@ export function AutoFormApiView({
33
33
  <label className="text-[10px] font-bold tracking-[0.1em] text-[var(--kyro-text-secondary)] opacity-50 block mb-2">
34
34
  Reference Path
35
35
  </label>
36
- <div className="bg-[var(--kyro-bg-secondary)] px-4 py-3 rounded-xl border border-[var(--kyro-border)] text-[11px] font-mono break-all selection:bg-[var(--kyro-primary)]/20 text-[var(--kyro-text-primary)]">
36
+ <div className="bg-[var(--kyro-bg-secondary)] px-4 py-3 rounded-md border border-[var(--kyro-border)] text-[11px] font-mono break-all selection:bg-[var(--kyro-primary)]/20 text-[var(--kyro-text-primary)]">
37
37
  {globalSlug
38
38
  ? `kyro.globals('${globalSlug}').get()`
39
39
  : formData.id
@@ -167,7 +167,7 @@ export function AutoFormVersionView({
167
167
  )}
168
168
  </div>
169
169
  <div className="text-[11px] text-[var(--kyro-text-muted)]">
170
- {new Date(v.createdAt as string).toLocaleString("en-US", {
170
+ {new Date((v.createdAt || (v as any).created_at) as string).toLocaleString("en-US", {
171
171
  month: "short",
172
172
  day: "numeric",
173
173
  hour: "2-digit",
@@ -236,7 +236,7 @@ export function AutoFormVersionView({
236
236
  </div>
237
237
  <div className="flex items-center gap-2 mt-1 flex-wrap">
238
238
  <span className="text-[11px] text-[var(--kyro-text-muted)]">
239
- {new Date(v.createdAt as string).toLocaleString("en-US", {
239
+ {new Date((v.createdAt || (v as any).created_at) as string).toLocaleString("en-US", {
240
240
  month: "short",
241
241
  day: "numeric",
242
242
  hour: "2-digit",
@@ -107,17 +107,6 @@ export const BlockEditModal: React.FC<BlockEditModalProps> = ({
107
107
  accentClass={theme.border}
108
108
  >
109
109
  <div className="space-y-4">
110
- <div>
111
- <label className="text-[10px] font-medium text-[var(--kyro-text-muted)] mb-1 block">
112
- Block Name
113
- </label>
114
- <input
115
- value={(blockData?.name as string) || ""}
116
- onChange={(e) => updateBlock(block.id, { name: e.target.value })}
117
- placeholder={blockSchema?.label || (block.type as string)}
118
- className="w-full bg-[var(--kyro-bg-primary)] border border-[var(--kyro-border)] rounded-lg px-3 py-2 text-sm text-[var(--kyro-text-primary)] outline-none focus:border-[var(--kyro-primary)] transition-colors"
119
- />
120
- </div>
121
110
 
122
111
  {renderFields()}
123
112
 
@@ -257,7 +257,7 @@ const SortableBlockComponent = ({
257
257
  onKeyDown={(e) => { if (e.key === "Enter") commitName(); if (e.key === "Escape") { setNameDraft((block.name as string) || ""); setEditingName(false); } }}
258
258
  onClick={(e) => e.stopPropagation()}
259
259
  placeholder={getBlockLabel(block.type as string)}
260
- className="w-[150px] sm:w-[250px] md:w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-primary)] rounded px-2 py-0.5 text-xs font-semibold text-[var(--kyro-text-primary)] outline-none"
260
+ className="w-full max-w-[150px] sm:max-w-[250px] md:max-w-[400px] bg-[var(--kyro-surface-accent)] border border-[var(--kyro-primary)] rounded px-2 py-0.5 text-xs font-semibold text-[var(--kyro-text-primary)] outline-none"
261
261
  />
262
262
  ) : (
263
263
  <div
@@ -65,7 +65,7 @@ export function RelationshipField({
65
65
  ? field.relationTo
66
66
  : [field.relationTo];
67
67
  const isPolymorphic = relationTo.length > 1;
68
- const [activeRelation, setActiveRelation] = useState(relationTo[0]);
68
+ const [activeRelation, setActiveRelation] = useState(relationTo[0] || "");
69
69
 
70
70
  const extractIds = useCallback((): string[] => {
71
71
  if (!value) return [];
@@ -8,9 +8,7 @@ import { CodeBlock } from "../../blocks/CodeBlock";
8
8
  import { FileBlock } from "../../blocks/FileBlock";
9
9
  import { AccordionBlock } from "../../blocks/AccordionBlock";
10
10
  import { RichTextBlock } from "../../blocks/RichTextBlock";
11
-
12
11
  import { HeroBlock } from "../../blocks/HeroBlock";
13
- import { HeadingSubheadingBlock } from "../../blocks/HeadingSubheadingBlock";
14
12
  import { CardBlock } from "../../blocks/CardBlock";
15
13
  import { ArrayBlock } from "../../blocks/ArrayBlock";
16
14
  import { RelationshipBlock } from "../../blocks/RelationshipBlock";
@@ -53,7 +51,6 @@ export const BLOCK_COMPONENTS: Record<string, React.ComponentType<{ block: Recor
53
51
  richtext: RichTextBlock,
54
52
 
55
53
  hero: HeroBlock,
56
- "heading-subheading": HeadingSubheadingBlock,
57
54
  card: CardBlock,
58
55
  array: ArrayBlock,
59
56
  relationship: RelationshipBlock,
@@ -61,19 +58,19 @@ export const BLOCK_COMPONENTS: Record<string, React.ComponentType<{ block: Recor
61
58
 
62
59
  // Block Theme mapping for consistent coloring across UI
63
60
  export const blockTheme: Record<string, { text: string; border: string; borderLeft: string }> = {
64
- "feature-split": { text: "text-indigo-500", border: "border-indigo-500", borderLeft: "border-l-indigo-500" },
65
- "feature-grid": { text: "text-blue-500", border: "border-blue-500", borderLeft: "border-l-blue-500" },
66
- "cta-banner": { text: "text-amber-500", border: "border-amber-500", borderLeft: "border-l-amber-500" },
61
+ featureSplit: { text: "text-indigo-500", border: "border-indigo-500", borderLeft: "border-l-indigo-500" },
62
+ featureGrid: { text: "text-blue-500", border: "border-blue-500", borderLeft: "border-l-blue-500" },
63
+ ctaBanner: { text: "text-amber-500", border: "border-amber-500", borderLeft: "border-l-amber-500" },
67
64
  testimonials: { text: "text-emerald-500", border: "border-emerald-500", borderLeft: "border-l-emerald-500" },
68
65
  faq: { text: "text-orange-500", border: "border-orange-500", borderLeft: "border-l-orange-500" },
69
66
  stats: { text: "text-rose-500", border: "border-rose-500", borderLeft: "border-l-rose-500" },
70
- "logo-cloud": { text: "text-cyan-500", border: "border-cyan-500", borderLeft: "border-l-cyan-500" },
67
+ logoCloud: { text: "text-cyan-500", border: "border-cyan-500", borderLeft: "border-l-cyan-500" },
71
68
  pricing: { text: "text-green-500", border: "border-green-500", borderLeft: "border-l-green-500" },
72
69
  team: { text: "text-violet-500", border: "border-violet-500", borderLeft: "border-l-violet-500" },
73
- "recent-feed": { text: "text-sky-500", border: "border-sky-500", borderLeft: "border-l-sky-500" },
74
- "process-steps": { text: "text-fuchsia-500", border: "border-fuchsia-500", borderLeft: "border-l-fuchsia-500" },
75
- "form-embed": { text: "text-pink-500", border: "border-pink-500", borderLeft: "border-l-pink-500" },
76
- "video-showcase": { text: "text-red-500", border: "border-red-500", borderLeft: "border-l-red-500" },
70
+ recentFeed: { text: "text-sky-500", border: "border-sky-500", borderLeft: "border-l-sky-500" },
71
+ processSteps: { text: "text-fuchsia-500", border: "border-fuchsia-500", borderLeft: "border-l-fuchsia-500" },
72
+ formEmbed: { text: "text-pink-500", border: "border-pink-500", borderLeft: "border-l-pink-500" },
73
+ videoShowcase: { text: "text-red-500", border: "border-red-500", borderLeft: "border-l-red-500" },
77
74
  hero: { text: "text-yellow-500", border: "border-yellow-500", borderLeft: "border-l-yellow-500" },
78
75
  card: { text: "text-teal-500", border: "border-teal-500", borderLeft: "border-l-teal-500" },
79
76
  default: { text: "text-zinc-400", border: "border-zinc-400", borderLeft: "border-l-zinc-400" },
@@ -92,25 +89,24 @@ export const blockIcons: Record<string, React.ReactNode> = {
92
89
  richtext: <AlignLeft className={`w-4 h-4 ${blockTheme.default.text}`} />,
93
90
 
94
91
  hero: <Star className={`w-4 h-4 ${blockTheme.hero.text}`} />,
95
- "heading-subheading": <Heading1 className={`w-4 h-4 ${blockTheme.default.text}`} />,
96
92
  card: <Box className={`w-4 h-4 ${blockTheme.card.text}`} />,
97
93
  array: <ListOrdered className={`w-4 h-4 ${blockTheme.default.text}`} />,
98
94
  relationship: <Link2 className={`w-4 h-4 ${blockTheme.default.text}`} />,
99
95
 
100
96
  // New Block Icons
101
- "feature-split": <Columns3 className={`w-4 h-4 ${blockTheme["feature-split"].text}`} />,
102
- "feature-grid": <Blocks className={`w-4 h-4 ${blockTheme["feature-grid"].text}`} />,
103
- "cta-banner": <Sparkles className={`w-4 h-4 ${blockTheme["cta-banner"].text}`} />,
97
+ featureSplit: <Columns3 className={`w-4 h-4 ${blockTheme.featureSplit.text}`} />,
98
+ featureGrid: <Blocks className={`w-4 h-4 ${blockTheme.featureGrid.text}`} />,
99
+ ctaBanner: <Sparkles className={`w-4 h-4 ${blockTheme.ctaBanner.text}`} />,
104
100
  testimonials: <Users className={`w-4 h-4 ${blockTheme.testimonials.text}`} />,
105
101
  faq: <HelpCircle className={`w-4 h-4 ${blockTheme.faq.text}`} />,
106
102
  stats: <Activity className={`w-4 h-4 ${blockTheme.stats.text}`} />,
107
- "logo-cloud": <Image className={`w-4 h-4 ${blockTheme["logo-cloud"].text}`} />,
103
+ logoCloud: <Image className={`w-4 h-4 ${blockTheme.logoCloud.text}`} />,
108
104
  pricing: <Tag className={`w-4 h-4 ${blockTheme.pricing.text}`} />,
109
105
  team: <Users className={`w-4 h-4 ${blockTheme.team.text}`} />,
110
- "recent-feed": <Database className={`w-4 h-4 ${blockTheme["recent-feed"].text}`} />,
111
- "process-steps": <Clock className={`w-4 h-4 ${blockTheme["process-steps"].text}`} />,
112
- "form-embed": <Mail className={`w-4 h-4 ${blockTheme["form-embed"].text}`} />,
113
- "video-showcase": <Video className={`w-4 h-4 ${blockTheme["video-showcase"].text}`} />,
106
+ recentFeed: <Database className={`w-4 h-4 ${blockTheme.recentFeed.text}`} />,
107
+ processSteps: <Clock className={`w-4 h-4 ${blockTheme.processSteps.text}`} />,
108
+ formEmbed: <Mail className={`w-4 h-4 ${blockTheme.formEmbed.text}`} />,
109
+ videoShowcase: <Video className={`w-4 h-4 ${blockTheme.videoShowcase.text}`} />,
114
110
  };
115
111
 
116
112
  // Internal utility to check if block has a specific custom react component
@@ -121,9 +117,9 @@ export function getBlockComponent(type: string) {
121
117
  // Determines if block is a native layout requiring generic rendering
122
118
  export function isGenericSemanticBlock(type: string) {
123
119
  return [
124
- "feature-split", "feature-grid", "cta-banner",
125
- "testimonials", "faq", "stats", "logo-cloud",
126
- "pricing", "team", "recent-feed", "process-steps", "form-embed", "video-showcase"
120
+ "featureSplit", "featureGrid", "ctaBanner",
121
+ "testimonials", "faq", "stats", "logoCloud",
122
+ "pricing", "team", "recentFeed", "processSteps", "formEmbed", "videoShowcase"
127
123
  ].includes(type);
128
124
  }
129
125
 
@@ -145,32 +141,31 @@ export function getBlockLabel(type: string): string {
145
141
 
146
142
  // Core Semantic Blocks
147
143
  hero: "Hero Section",
148
- "heading-subheading": "Heading + Subheading",
149
144
  card: "Card Block",
150
- "feature-split": "Feature Split",
151
- "feature-grid": "Feature Grid",
152
- "cta-banner": "CTA Banner",
145
+ featureSplit: "Feature Split",
146
+ featureGrid: "Feature Grid",
147
+ ctaBanner: "CTA Banner",
153
148
  testimonials: "Testimonials Stack",
154
149
  faq: "FAQ Section",
155
150
  stats: "Stats & Metrics",
156
- "logo-cloud": "Logo Cloud",
151
+ logoCloud: "Logo Cloud",
157
152
 
158
153
  // Brand New Blocks
159
154
  pricing: "Pricing Grid / Plan",
160
155
  team: "Team Profiles Showcase",
161
- "recent-feed": "Dynamic Content Feed",
162
- "process-steps": "Process Timeline / Steps",
163
- "form-embed": "Lead Intake Form",
164
- "video-showcase": "Cinematic Video Showcase",
156
+ recentFeed: "Dynamic Content Feed",
157
+ processSteps: "Process Timeline / Steps",
158
+ formEmbed: "Lead Intake Form",
159
+ videoShowcase: "Cinematic Video Showcase",
165
160
 
166
161
  // Inline Content Elements
167
- "heading-element": "Heading",
168
- "text-element": "Text",
169
- "image-element": "Image",
170
- "richtext-element": "Rich Text",
171
- "button-element": "Button",
172
- "video-element": "Video",
173
- "list-element": "List",
162
+ headingElement: "Heading",
163
+ textElement: "Text",
164
+ imageElement: "Image",
165
+ richtextElement: "Rich Text",
166
+ buttonElement: "Button",
167
+ videoElement: "Video",
168
+ listElement: "List",
174
169
  };
175
170
  return labelMap[type] || type;
176
171
  }
package/src/lib/config.ts CHANGED
@@ -101,6 +101,33 @@ function updateFieldByPath(
101
101
  if (field.fields && Array.isArray(field.fields)) {
102
102
  return updateFieldByPath(field.fields, remainingPath, updates);
103
103
  }
104
+
105
+ // For tabs fields, traverse into all tabs
106
+ if (field.type === "tabs" && field.tabs && Array.isArray(field.tabs)) {
107
+ for (const tab of field.tabs) {
108
+ if (tab.fields && Array.isArray(tab.fields)) {
109
+ if (updateFieldByPath(tab.fields, remainingPath, updates)) {
110
+ return true;
111
+ }
112
+ }
113
+ }
114
+ return false;
115
+ }
116
+
117
+ // For blocks fields, the next path segment is the block slug
118
+ if (field.type === "blocks" && field.blocks && Array.isArray(field.blocks)) {
119
+ const blockSlug = remainingPath.split(".")[0];
120
+ const restOfPath = remainingPath.split(".").slice(1).join(".");
121
+ if (!restOfPath) return false;
122
+
123
+ for (const block of field.blocks) {
124
+ if (block.slug === blockSlug && block.fields && Array.isArray(block.fields)) {
125
+ return updateFieldByPath(block.fields, restOfPath, updates);
126
+ }
127
+ }
128
+ return false;
129
+ }
130
+
104
131
  // For array fields, look in the nested fields
105
132
  if (field.type === "array" && field.fields && Array.isArray(field.fields)) {
106
133
  return updateFieldByPath(field.fields, remainingPath, updates);
@@ -112,6 +139,27 @@ function updateFieldByPath(
112
139
  return true;
113
140
  }
114
141
  }
142
+
143
+ // If it's a structural field (like tabs, row, or collapsible) that we should pass through implicitly,
144
+ // we must traverse it even if it has a name, because these fields do not create nested data objects.
145
+ const isFlatStructuralField =
146
+ !field.name ||
147
+ field.type === "tabs" ||
148
+ field.type === "row" ||
149
+ field.type === "collapsible";
150
+
151
+ if (isFlatStructuralField) {
152
+ if (field.fields && Array.isArray(field.fields)) {
153
+ if (updateFieldByPath(field.fields, path, updates)) return true;
154
+ }
155
+ if (field.type === "tabs" && field.tabs && Array.isArray(field.tabs)) {
156
+ for (const tab of field.tabs) {
157
+ if (tab.fields && Array.isArray(tab.fields)) {
158
+ if (updateFieldByPath(tab.fields, path, updates)) return true;
159
+ }
160
+ }
161
+ }
162
+ }
115
163
  }
116
164
  return false;
117
165
  }