gazetta 0.0.7 → 0.1.0

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 (91) hide show
  1. package/admin-dist/assets/index-DKYjjDqE.js +2451 -0
  2. package/admin-dist/assets/index-yguWG9_J.css +1 -0
  3. package/admin-dist/index.html +3 -2
  4. package/dist/admin-api/index.d.ts +6 -0
  5. package/dist/admin-api/index.d.ts.map +1 -1
  6. package/dist/admin-api/index.js +8 -3
  7. package/dist/admin-api/index.js.map +1 -1
  8. package/dist/admin-api/routes/fields.d.ts +4 -0
  9. package/dist/admin-api/routes/fields.d.ts.map +1 -0
  10. package/dist/admin-api/routes/fields.js +21 -0
  11. package/dist/admin-api/routes/fields.js.map +1 -0
  12. package/dist/admin-api/routes/pages.d.ts.map +1 -1
  13. package/dist/admin-api/routes/pages.js +3 -8
  14. package/dist/admin-api/routes/pages.js.map +1 -1
  15. package/dist/admin-api/routes/preview.d.ts +1 -1
  16. package/dist/admin-api/routes/preview.d.ts.map +1 -1
  17. package/dist/admin-api/routes/preview.js +30 -9
  18. package/dist/admin-api/routes/preview.js.map +1 -1
  19. package/dist/admin-api/routes/publish.d.ts +1 -1
  20. package/dist/admin-api/routes/publish.d.ts.map +1 -1
  21. package/dist/admin-api/routes/publish.js +44 -33
  22. package/dist/admin-api/routes/publish.js.map +1 -1
  23. package/dist/admin-api/routes/templates.d.ts +1 -1
  24. package/dist/admin-api/routes/templates.d.ts.map +1 -1
  25. package/dist/admin-api/routes/templates.js +32 -8
  26. package/dist/admin-api/routes/templates.js.map +1 -1
  27. package/dist/app.js +1 -1
  28. package/dist/app.js.map +1 -1
  29. package/dist/cli/index.js +650 -175
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/editor/mount.d.ts +15 -0
  32. package/dist/editor/mount.d.ts.map +1 -1
  33. package/dist/editor/mount.js +684 -29
  34. package/dist/editor/mount.js.map +1 -1
  35. package/dist/formats.d.ts +31 -0
  36. package/dist/formats.d.ts.map +1 -1
  37. package/dist/formats.js +14 -0
  38. package/dist/formats.js.map +1 -1
  39. package/dist/index.d.ts +10 -6
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +8 -5
  42. package/dist/index.js.map +1 -1
  43. package/dist/manifest.d.ts.map +1 -1
  44. package/dist/manifest.js +10 -10
  45. package/dist/manifest.js.map +1 -1
  46. package/dist/providers/r2.d.ts +8 -0
  47. package/dist/providers/r2.d.ts.map +1 -0
  48. package/dist/providers/r2.js +83 -0
  49. package/dist/providers/r2.js.map +1 -0
  50. package/dist/publish-rendered.d.ts +7 -3
  51. package/dist/publish-rendered.d.ts.map +1 -1
  52. package/dist/publish-rendered.js +27 -34
  53. package/dist/publish-rendered.js.map +1 -1
  54. package/dist/renderer.d.ts +2 -1
  55. package/dist/renderer.d.ts.map +1 -1
  56. package/dist/renderer.js +25 -15
  57. package/dist/renderer.js.map +1 -1
  58. package/dist/resolver.d.ts +1 -0
  59. package/dist/resolver.d.ts.map +1 -1
  60. package/dist/resolver.js +23 -3
  61. package/dist/resolver.js.map +1 -1
  62. package/dist/scope.d.ts +10 -4
  63. package/dist/scope.d.ts.map +1 -1
  64. package/dist/scope.js +19 -8
  65. package/dist/scope.js.map +1 -1
  66. package/dist/serve.d.ts +14 -0
  67. package/dist/serve.d.ts.map +1 -0
  68. package/dist/serve.js +135 -0
  69. package/dist/serve.js.map +1 -0
  70. package/dist/site-loader.d.ts +11 -1
  71. package/dist/site-loader.d.ts.map +1 -1
  72. package/dist/site-loader.js +19 -7
  73. package/dist/site-loader.js.map +1 -1
  74. package/dist/targets.d.ts +1 -0
  75. package/dist/targets.d.ts.map +1 -1
  76. package/dist/targets.js +42 -1
  77. package/dist/targets.js.map +1 -1
  78. package/dist/template-loader.d.ts +3 -2
  79. package/dist/template-loader.d.ts.map +1 -1
  80. package/dist/template-loader.js +32 -2
  81. package/dist/template-loader.js.map +1 -1
  82. package/dist/types.d.ts +35 -16
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/types.js +4 -1
  85. package/dist/types.js.map +1 -1
  86. package/dist/workers/cloudflare-r2.d.ts.map +1 -1
  87. package/dist/workers/cloudflare-r2.js +14 -1
  88. package/dist/workers/cloudflare-r2.js.map +1 -1
  89. package/package.json +16 -7
  90. package/admin-dist/assets/index-CVC2_Byr.js +0 -1941
  91. package/admin-dist/assets/index-W1ylqX_Y.css +0 -1
@@ -3,56 +3,711 @@ import React from 'react';
3
3
  import { createRoot } from 'react-dom/client';
4
4
  import Form from '@rjsf/core';
5
5
  import validator from '@rjsf/validator-ajv8';
6
+ import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
7
+ import { useEditor, EditorContent } from '@tiptap/react';
8
+ import { BubbleMenu } from '@tiptap/react/menus';
9
+ import StarterKit from '@tiptap/starter-kit';
10
+ import Link from '@tiptap/extension-link';
11
+ import Placeholder from '@tiptap/extension-placeholder';
6
12
  const roots = new WeakMap();
13
+ // ---------------------------------------------------------------------------
14
+ // Styles
15
+ // ---------------------------------------------------------------------------
7
16
  const STYLES = `
8
- .gz-editor { font-family: system-ui, -apple-system, sans-serif; font-size: 0.875rem; }
17
+ /* Theme CSS variables set by the host (EditorPanel.vue) */
18
+ /* Base */
19
+ .gz-editor {
20
+ font-family: system-ui, -apple-system, sans-serif; font-size: 0.875rem;
21
+ color: var(--gz-text, #e0e0e0);
22
+ transition: color 0.2s, background-color 0.2s;
23
+ }
24
+ .gz-editor * { box-sizing: border-box; }
9
25
  .gz-editor .form-group { margin-bottom: 1rem; }
10
- .gz-editor label { display: block; font-weight: 600; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.03em; color: #a0a0a0; margin-bottom: 0.25rem; }
11
- .gz-editor .field-description { font-size: 0.75rem; color: #888; margin-bottom: 0.375rem; }
26
+ .gz-editor label {
27
+ display: block; font-weight: 600; font-size: 0.6875rem; text-transform: uppercase;
28
+ letter-spacing: 0.05em; color: var(--gz-text-label, #8888a0); margin-bottom: 0.375rem;
29
+ }
30
+ .gz-editor .field-description { font-size: 0.75rem; color: var(--gz-text-hint, #666680); margin-bottom: 0.375rem; line-height: 1.4; }
31
+
32
+ /* Inputs */
12
33
  .gz-editor input[type="text"], .gz-editor input[type="number"], .gz-editor input[type="url"],
13
- .gz-editor input[type="email"], .gz-editor input[type="password"], .gz-editor input[type="color"],
34
+ .gz-editor input[type="email"], .gz-editor input[type="password"],
14
35
  .gz-editor textarea, .gz-editor select {
15
- width: 100%; padding: 0.5rem 0.625rem; font-size: 0.875rem; font-family: inherit;
16
- background: #1e1e2e; color: #e0e0e0; border: 1px solid #3a3a4a; border-radius: 6px;
17
- outline: none; transition: border-color 0.15s;
36
+ width: 100%; padding: 0.5rem 0.75rem; font-size: 0.875rem; font-family: inherit;
37
+ background: var(--gz-bg-input, #161622); color: var(--gz-text, #e0e0e0); border: 1px solid var(--gz-border, #2a2a3a); border-radius: 6px;
38
+ outline: none; transition: border-color 0.15s, box-shadow 0.15s, background-color 0.2s, color 0.2s;
18
39
  }
19
40
  .gz-editor input:focus, .gz-editor textarea:focus, .gz-editor select:focus {
20
- border-color: #667eea;
41
+ border-color: var(--gz-accent, #667eea); box-shadow: 0 0 0 2px color-mix(in srgb, var(--gz-accent, #667eea) 15%, transparent);
42
+ }
43
+ .gz-editor textarea { min-height: 5rem; resize: vertical; line-height: 1.5; }
44
+ .gz-editor select {
45
+ appearance: none; cursor: pointer;
46
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
47
+ background-repeat: no-repeat; background-position: right 0.75rem center; padding-right: 2rem;
21
48
  }
22
- .gz-editor textarea { min-height: 5rem; resize: vertical; }
49
+
50
+ /* Hide rjsf chrome */
23
51
  .gz-editor .btn { display: none; }
24
- .gz-editor .error-detail, .gz-editor .text-danger { color: #f87171; font-size: 0.75rem; }
25
- .gz-editor .array-item { padding: 0.5rem; border: 1px solid #2a2a3a; border-radius: 6px; margin-bottom: 0.5rem; }
26
- .gz-editor .array-item-toolbox { display: flex; gap: 0.25rem; margin-top: 0.375rem; }
27
- .gz-editor .array-item-toolbox .btn { display: inline-flex; padding: 0.25rem 0.5rem; font-size: 0.75rem; background: #2a2a3a; color: #ccc; border: none; border-radius: 4px; cursor: pointer; }
28
- .gz-editor .array-item-toolbox .btn:hover { background: #3a3a4a; }
29
- .gz-editor .markdown-widget textarea { min-height: 12rem; font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 0.8125rem; line-height: 1.6; }
30
- .gz-editor .markdown-widget .markdown-hint { font-size: 0.6875rem; color: #52525b; margin-top: 0.25rem; }
52
+ .gz-editor fieldset { border: none; padding: 0; margin: 0; }
53
+ .gz-editor legend { display: none; }
54
+
55
+ /* Errors */
56
+ .gz-editor .error-detail, .gz-editor .text-danger { color: var(--gz-error, #f87171); font-size: 0.75rem; margin-top: 0.25rem; }
57
+ .gz-editor .has-error input, .gz-editor .has-error textarea, .gz-editor .has-error select { border-color: color-mix(in srgb, var(--gz-error, #f87171) 50%, transparent); }
58
+
59
+ /* Required marker */
60
+ .gz-editor label .required { color: var(--gz-error, #f87171); margin-left: 0.125rem; }
61
+
62
+ /* ---- Toggle ---- */
63
+ .gz-editor .gz-toggle { display: flex; align-items: center; gap: 0.625rem; cursor: pointer; user-select: none; padding: 0.25rem 0; }
64
+ .gz-editor .gz-toggle-track {
65
+ position: relative; width: 36px; height: 20px; border-radius: 10px;
66
+ background: var(--gz-border, #2a2a3a); transition: background 0.2s; flex-shrink: 0;
67
+ }
68
+ .gz-editor .gz-toggle-track.on { background: var(--gz-accent, #667eea); }
69
+ .gz-editor .gz-toggle-thumb {
70
+ position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%;
71
+ background: #fff; transition: transform 0.2s ease;
72
+ }
73
+ .gz-editor .gz-toggle-track.on .gz-toggle-thumb { transform: translateX(16px); }
74
+ .gz-editor .gz-toggle-label { font-size: 0.8125rem; color: var(--gz-text-secondary, #ccc); font-weight: 400; text-transform: none; letter-spacing: 0; }
75
+
76
+ /* ---- Color ---- */
77
+ .gz-editor .gz-color-widget { display: flex; align-items: center; gap: 0.5rem; }
78
+ .gz-editor .gz-color-widget input[type="color"] {
79
+ width: 36px; height: 36px; border: 1px solid var(--gz-border, #2a2a3a); border-radius: 6px;
80
+ padding: 2px; cursor: pointer; background: transparent;
81
+ }
82
+ .gz-editor .gz-color-widget input[type="color"]::-webkit-color-swatch-wrapper { padding: 2px; }
83
+ .gz-editor .gz-color-widget input[type="color"]::-webkit-color-swatch { border-radius: 3px; border: none; }
84
+ .gz-editor .gz-color-widget input[type="text"] { flex: 1; }
85
+
86
+ /* ---- Tags ---- */
87
+ .gz-editor .gz-tags {
88
+ display: flex; flex-wrap: wrap; gap: 0.375rem; padding: 0.375rem 0.5rem;
89
+ background: var(--gz-bg-input, #161622); border: 1px solid var(--gz-border, #2a2a3a); border-radius: 6px;
90
+ min-height: 2.5rem; align-items: center; cursor: text; transition: border-color 0.15s, box-shadow 0.15s;
91
+ }
92
+ .gz-editor .gz-tags:focus-within { border-color: var(--gz-accent, #667eea); box-shadow: 0 0 0 2px color-mix(in srgb, var(--gz-accent, #667eea) 15%, transparent); }
93
+ .gz-editor .gz-tag {
94
+ display: inline-flex; align-items: center; gap: 0.25rem;
95
+ padding: 0.1875rem 0.5rem 0.1875rem 0.625rem;
96
+ background: var(--gz-bg-chip, #252538); border: 1px solid var(--gz-border, #2a2a3a); border-radius: 4px;
97
+ font-size: 0.8125rem; color: var(--gz-text-secondary, #ccc); transition: background 0.1s;
98
+ }
99
+ .gz-editor .gz-tag:hover { background: var(--gz-bg-chip, #252538); filter: brightness(1.1); }
100
+ .gz-editor .gz-tag-remove {
101
+ background: none; border: none; color: var(--gz-text-hint, #666); cursor: pointer; font-size: 1rem;
102
+ padding: 0; line-height: 1; display: flex; align-items: center;
103
+ }
104
+ .gz-editor .gz-tag-remove:hover { color: var(--gz-error, #f87171); }
105
+ .gz-editor .gz-tags-input {
106
+ border: none !important; background: transparent !important; color: var(--gz-text, #e0e0e0); font-size: 0.8125rem;
107
+ outline: none !important; min-width: 80px; flex: 1; padding: 0.125rem 0;
108
+ box-shadow: none !important;
109
+ }
110
+ .gz-editor .gz-tags-empty { color: var(--gz-text-hint, #444); font-size: 0.75rem; padding: 0.25rem 0; }
111
+
112
+ /* ---- Image ---- */
113
+ .gz-editor .gz-image-preview {
114
+ margin-top: 0.5rem; border-radius: 6px; overflow: hidden;
115
+ background: var(--gz-bg-input, #161622); border: 1px dashed var(--gz-border, #2a2a3a); transition: border-color 0.15s;
116
+ }
117
+ .gz-editor .gz-image-preview.has-image { border-style: solid; }
118
+ .gz-editor .gz-image-preview img { display: block; max-width: 100%; max-height: 200px; object-fit: contain; margin: 0 auto; }
119
+ .gz-editor .gz-image-preview-empty {
120
+ padding: 2rem; text-align: center; color: var(--gz-text-hint, #444); font-size: 0.75rem;
121
+ }
122
+
123
+ /* ---- Slug ---- */
124
+ .gz-editor .gz-slug-widget input {
125
+ font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 0.8125rem; letter-spacing: 0.02em;
126
+ }
127
+ .gz-editor .gz-slug-hint { font-size: 0.6875rem; color: var(--gz-text-hint, #444); margin-top: 0.25rem; }
128
+
129
+ /* ---- Code ---- */
130
+ .gz-editor .gz-code-widget textarea {
131
+ font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 0.8125rem;
132
+ line-height: 1.6; min-height: 10rem; tab-size: 2;
133
+ }
134
+ .gz-editor .gz-code-hint { font-size: 0.6875rem; color: var(--gz-text-hint, #444); margin-top: 0.25rem; }
135
+
136
+ /* ---- JSON ---- */
137
+ .gz-editor .gz-json-widget textarea {
138
+ font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 0.8125rem;
139
+ line-height: 1.6; min-height: 8rem; tab-size: 2;
140
+ }
141
+ .gz-editor .gz-json-error { font-size: 0.6875rem; color: var(--gz-error, #f87171); margin-top: 0.25rem; }
142
+ .gz-editor .gz-json-valid { font-size: 0.6875rem; color: var(--gz-success, #4ade80); margin-top: 0.25rem; }
143
+
144
+ /* ---- Markdown ---- */
145
+ .gz-editor .gz-markdown textarea {
146
+ min-height: 12rem; font-family: 'JetBrains Mono', 'Fira Code', monospace;
147
+ font-size: 0.8125rem; line-height: 1.6;
148
+ }
149
+ .gz-editor .gz-markdown-hint { font-size: 0.6875rem; color: var(--gz-text-hint, #444); margin-top: 0.25rem; }
150
+
151
+ /* ---- Rich text (Tiptap) ---- */
152
+ .gz-editor .gz-richtext {
153
+ border: 1px solid var(--gz-border, #2a2a3a); border-radius: 6px; overflow: hidden;
154
+ background: var(--gz-bg-input, #161622); transition: border-color 0.15s, box-shadow 0.15s;
155
+ }
156
+ .gz-editor .gz-richtext:focus-within { border-color: var(--gz-accent, #667eea); box-shadow: 0 0 0 2px color-mix(in srgb, var(--gz-accent, #667eea) 15%, transparent); }
157
+ .gz-editor .gz-richtext-toolbar {
158
+ display: flex; flex-wrap: wrap; gap: 1px; padding: 0.25rem;
159
+ border-bottom: 1px solid var(--gz-border-subtle, #1e1e2e); background: var(--gz-bg-toolbar, #1a1a2a);
160
+ }
161
+ .gz-editor .gz-rt-btn {
162
+ background: none; border: 1px solid transparent; border-radius: 4px;
163
+ color: var(--gz-text-hint, #777); cursor: pointer; padding: 0.3125rem 0.5rem; font-size: 0.6875rem;
164
+ font-family: inherit; line-height: 1; font-weight: 600; transition: all 0.1s;
165
+ display: flex; align-items: center; justify-content: center; min-width: 28px;
166
+ }
167
+ .gz-editor .gz-rt-btn:hover { color: var(--gz-text-secondary, #ccc); background: var(--gz-bg-chip, #252538); }
168
+ .gz-editor .gz-rt-btn.active { color: var(--gz-accent, #667eea); background: var(--gz-bg-chip, #252538); border-color: color-mix(in srgb, var(--gz-accent, #667eea) 20%, transparent); }
169
+ .gz-editor .gz-rt-sep { width: 1px; background: var(--gz-bg-chip, #252538); margin: 0.125rem 0.25rem; align-self: stretch; }
170
+
171
+ /* Tiptap content area */
172
+ .gz-editor .gz-richtext .tiptap { outline: none; padding: 0.75rem; min-height: 10rem; color: var(--gz-text, #e0e0e0); font-size: 0.875rem; line-height: 1.7; }
173
+ .gz-editor .gz-richtext .tiptap p { margin: 0.25rem 0; }
174
+ .gz-editor .gz-richtext .tiptap h2 { font-size: 1.25rem; font-weight: 700; margin: 1rem 0 0.25rem; color: var(--gz-text, #e0e0e0); }
175
+ .gz-editor .gz-richtext .tiptap h3 { font-size: 1.1rem; font-weight: 600; margin: 0.75rem 0 0.25rem; color: var(--gz-text, #e0e0e0); }
176
+ .gz-editor .gz-richtext .tiptap ul, .gz-editor .gz-richtext .tiptap ol { padding-left: 1.25rem; margin: 0.25rem 0; }
177
+ .gz-editor .gz-richtext .tiptap li { margin: 0.125rem 0; }
178
+ .gz-editor .gz-richtext .tiptap blockquote {
179
+ border-left: 3px solid color-mix(in srgb, var(--gz-accent, #667eea) 25%, transparent); padding-left: 0.875rem; margin: 0.5rem 0;
180
+ color: var(--gz-text-secondary, #999); font-style: italic;
181
+ }
182
+ .gz-editor .gz-richtext .tiptap code {
183
+ background: var(--gz-bg-chip, #252538); padding: 0.125rem 0.375rem; border-radius: 3px;
184
+ font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 0.8125rem;
185
+ }
186
+ .gz-editor .gz-richtext .tiptap pre {
187
+ background: var(--gz-bg-code, #12121e); padding: 0.875rem; border-radius: 6px; overflow-x: auto; margin: 0.5rem 0;
188
+ }
189
+ .gz-editor .gz-richtext .tiptap pre code { background: none; padding: 0; }
190
+ .gz-editor .gz-richtext .tiptap a { color: var(--gz-accent, #667eea); text-decoration: underline; cursor: pointer; }
191
+ .gz-editor .gz-richtext .tiptap hr { border: none; border-top: 1px solid var(--gz-border, #2a2a3a); margin: 1rem 0; }
192
+ .gz-editor .gz-richtext .tiptap p.is-editor-empty:first-child::before {
193
+ content: attr(data-placeholder); float: left; color: var(--gz-text-hint, #444); pointer-events: none; height: 0;
194
+ }
195
+
196
+ /* Bubble menu */
197
+ .gz-editor .gz-bubble {
198
+ display: flex; gap: 1px; background: var(--gz-bg-toolbar, #1a1a2a); border: 1px solid var(--gz-border, #2a2a3a);
199
+ border-radius: 6px; padding: 0.1875rem; box-shadow: 0 4px 16px #0008;
200
+ }
201
+ .gz-editor .gz-bubble .gz-rt-btn { padding: 0.25rem 0.4375rem; font-size: 0.625rem; }
202
+
203
+ /* ---- Array ---- */
204
+ .gz-editor .gz-array { margin-bottom: 0.25rem; }
205
+ .gz-editor .gz-array-header {
206
+ display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; padding-bottom: 0.375rem;
207
+ border-bottom: 1px solid var(--gz-border-subtle, #1e1e2e);
208
+ }
209
+ .gz-editor .gz-array-title {
210
+ font-size: 0.6875rem; font-weight: 600; text-transform: uppercase;
211
+ letter-spacing: 0.05em; color: var(--gz-text-label, #8888a0);
212
+ }
213
+ .gz-editor .gz-array-count {
214
+ font-size: 0.625rem; color: var(--gz-text-hint, #555); background: var(--gz-border-subtle, #1e1e2e);
215
+ padding: 0.0625rem 0.4375rem; border-radius: 8px; font-weight: 600;
216
+ }
217
+ .gz-editor .gz-array-add {
218
+ margin-left: auto; background: none; border: 1px solid color-mix(in srgb, var(--gz-accent, #667eea) 20%, transparent);
219
+ color: var(--gz-accent, #667eea); cursor: pointer; font-size: 0.6875rem; font-weight: 600;
220
+ padding: 0.25rem 0.625rem; border-radius: 4px; transition: all 0.15s;
221
+ }
222
+ .gz-editor .gz-array-add:hover { background: color-mix(in srgb, var(--gz-accent, #667eea) 8%, transparent); border-color: color-mix(in srgb, var(--gz-accent, #667eea) 30%, transparent); }
223
+ .gz-editor .gz-array-items { display: flex; flex-direction: column; gap: 0.25rem; }
224
+
225
+ /* Array items — collapsible */
226
+ .gz-editor .gz-array-item {
227
+ background: var(--gz-bg-card, #1a1a28); border-radius: 6px; border: 1px solid var(--gz-border-subtle, #1e1e2e);
228
+ transition: border-color 0.15s;
229
+ }
230
+ .gz-editor .gz-array-item:hover { border-color: var(--gz-border, #2a2a3a); }
231
+ .gz-editor .gz-array-item.dragging { opacity: 0.9; box-shadow: 0 4px 20px #0006; border-color: color-mix(in srgb, var(--gz-accent, #667eea) 25%, transparent); }
232
+ .gz-editor .gz-array-item-header {
233
+ display: flex; align-items: center; gap: 0.375rem; padding: 0.5rem 0.625rem;
234
+ cursor: pointer; user-select: none; min-height: 36px;
235
+ }
236
+ .gz-editor .gz-array-item-handle {
237
+ color: var(--gz-text-hint, #444); font-size: 0.625rem; cursor: grab; flex-shrink: 0; padding: 0.25rem 0.125rem;
238
+ opacity: 0; transition: opacity 0.1s; display: flex; align-items: center; letter-spacing: 1px;
239
+ }
240
+ .gz-editor .gz-array-item:hover .gz-array-item-handle { opacity: 1; }
241
+ .gz-editor .gz-array-item-handle:active { cursor: grabbing; color: var(--gz-accent, #667eea); }
242
+ .gz-editor .gz-array-item-chevron {
243
+ color: var(--gz-text-hint, #444); font-size: 0.5rem; transition: transform 0.15s ease; flex-shrink: 0; width: 12px; text-align: center;
244
+ }
245
+ .gz-editor .gz-array-item-chevron.open { transform: rotate(90deg); }
246
+ .gz-editor .gz-array-item-summary {
247
+ flex: 1; font-size: 0.8125rem; color: var(--gz-text-secondary, #999); white-space: nowrap;
248
+ overflow: hidden; text-overflow: ellipsis; min-width: 0;
249
+ }
250
+ .gz-editor .gz-array-item-summary.empty { color: var(--gz-text-hint, #444); font-style: italic; }
251
+ .gz-editor .gz-array-item-idx {
252
+ font-size: 0.625rem; color: var(--gz-text-hint, #444); font-weight: 600; flex-shrink: 0;
253
+ min-width: 1rem; text-align: center;
254
+ }
255
+ .gz-editor .gz-array-item-actions { display: flex; gap: 1px; flex-shrink: 0; opacity: 0; transition: opacity 0.1s; }
256
+ .gz-editor .gz-array-item:hover .gz-array-item-actions { opacity: 1; }
257
+ .gz-editor .gz-array-item-body {
258
+ padding: 0 0.75rem 0.75rem; overflow: hidden;
259
+ transition: max-height 0.2s ease, opacity 0.15s ease, padding 0.2s ease;
260
+ }
261
+ .gz-editor .gz-array-item-body.collapsed { max-height: 0; opacity: 0; padding-top: 0; padding-bottom: 0; }
262
+ .gz-editor .gz-array-item-body.expanded { max-height: 2000px; opacity: 1; }
263
+ .gz-editor .gz-array-item-body .form-group:last-child { margin-bottom: 0; }
264
+ .gz-editor .gz-array-item-body fieldset > .form-group:last-child { margin-bottom: 0; }
265
+
266
+ /* Icon buttons */
267
+ .gz-editor .gz-btn-icon {
268
+ width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;
269
+ background: transparent; border: none; border-radius: 3px; color: var(--gz-text-hint, #555); cursor: pointer;
270
+ font-size: 0.6875rem; padding: 0; transition: all 0.1s;
271
+ }
272
+ .gz-editor .gz-btn-icon:hover { background: var(--gz-bg-chip, #252538); color: var(--gz-text-secondary, #ccc); }
273
+ .gz-editor .gz-btn-icon:disabled { opacity: 0.25; cursor: default; }
274
+ .gz-editor .gz-btn-icon:disabled:hover { background: transparent; color: var(--gz-text-hint, #555); }
275
+ .gz-editor .gz-btn-icon.gz-btn-remove:hover { background: color-mix(in srgb, var(--gz-error, #f87171) 15%, transparent); color: var(--gz-error, #f87171); }
276
+
277
+ /* Array empty state */
278
+ .gz-editor .gz-array-empty {
279
+ padding: 1.5rem; text-align: center; border: 1px dashed var(--gz-border-subtle, #1e1e2e); border-radius: 6px;
280
+ color: var(--gz-text-hint, #444); font-size: 0.8125rem;
281
+ }
282
+
283
+ /* Nested objects in arrays */
284
+ .gz-editor .gz-object-inline { }
285
+ .gz-editor .gz-object-inline > .form-group { margin-bottom: 0.625rem; }
31
286
  `;
32
- /** Markdown widget — textarea with monospace font and hint */
287
+ // ---------------------------------------------------------------------------
288
+ // Helpers
289
+ // ---------------------------------------------------------------------------
290
+ const LONG_TEXT_NAMES = new Set(['body', 'description', 'text', 'content', 'bio', 'summary', 'message', 'notes', 'output']);
291
+ const URL_NAMES = new Set(['href', 'url', 'link', 'src']);
292
+ const COLOR_NAMES = new Set(['background', 'color']);
293
+ function buildUiSchema(jsonSchema) {
294
+ const ui = { 'ui:submitButtonOptions': { norender: true } };
295
+ const properties = jsonSchema.properties;
296
+ if (!properties)
297
+ return ui;
298
+ for (const [name, prop] of Object.entries(properties)) {
299
+ const format = prop.format;
300
+ const type = prop.type;
301
+ const customField = prop.field;
302
+ // Custom field — highest priority
303
+ if (customField) {
304
+ ui[name] = { 'ui:widget': `custom-field:${customField}` };
305
+ continue;
306
+ }
307
+ if (format === 'markdown' || format === 'richtext' || format === 'image' || format === 'link' || format === 'slug' || format === 'code' || format === 'json') {
308
+ ui[name] = { 'ui:widget': format };
309
+ continue;
310
+ }
311
+ if (format === 'color') {
312
+ ui[name] = { 'ui:widget': 'color' };
313
+ continue;
314
+ }
315
+ if (type === 'string' && !format) {
316
+ if (LONG_TEXT_NAMES.has(name)) {
317
+ ui[name] = { 'ui:widget': 'textarea', 'ui:options': { rows: 5 } };
318
+ }
319
+ else if (URL_NAMES.has(name)) {
320
+ ui[name] = { 'ui:options': { inputType: 'url' } };
321
+ }
322
+ else if (COLOR_NAMES.has(name)) {
323
+ ui[name] = { 'ui:widget': 'color' };
324
+ }
325
+ }
326
+ if (type === 'boolean') {
327
+ ui[name] = { 'ui:widget': 'toggle' };
328
+ }
329
+ if (type === 'array') {
330
+ const items = prop.items;
331
+ if (items?.type === 'string') {
332
+ ui[name] = { 'ui:widget': 'tags' };
333
+ }
334
+ }
335
+ }
336
+ return ui;
337
+ }
338
+ /** Extract a one-line summary from an array item's formData for the collapsed header */
339
+ function summarizeItem(data) {
340
+ if (!data || typeof data !== 'object')
341
+ return String(data ?? '');
342
+ const obj = data;
343
+ // Try common field names that make good summaries
344
+ for (const key of ['title', 'name', 'label', 'heading', 'text', 'quote', 'command']) {
345
+ if (typeof obj[key] === 'string' && obj[key])
346
+ return obj[key];
347
+ }
348
+ // Fall back to first non-empty string value
349
+ for (const val of Object.values(obj)) {
350
+ if (typeof val === 'string' && val.trim())
351
+ return val.trim();
352
+ }
353
+ return '';
354
+ }
355
+ // ---------------------------------------------------------------------------
356
+ // Widgets
357
+ // ---------------------------------------------------------------------------
33
358
  function MarkdownWidget(props) {
34
- return (_jsxs("div", { className: "markdown-widget", children: [_jsx("textarea", { value: props.value ?? '', onChange: (e) => props.onChange(e.target.value), placeholder: props.placeholder, rows: props.schema.rows ?? 12 }), _jsx("div", { className: "markdown-hint", children: "Markdown supported" })] }));
359
+ return (_jsxs("div", { className: "gz-markdown", children: [_jsx("textarea", { value: props.value ?? '', onChange: (e) => props.onChange(e.target.value), placeholder: "Write markdown here...", rows: 12 }), _jsx("div", { className: "gz-markdown-hint", children: "Markdown supported" })] }));
360
+ }
361
+ function ToggleWidget(props) {
362
+ const on = !!props.value;
363
+ return (_jsxs("div", { className: "gz-toggle", onClick: () => !props.disabled && !props.readonly && props.onChange(!on), role: "switch", "aria-checked": on, children: [_jsx("div", { className: `gz-toggle-track${on ? ' on' : ''}`, children: _jsx("div", { className: "gz-toggle-thumb" }) }), props.label && _jsx("span", { className: "gz-toggle-label", children: props.label })] }));
364
+ }
365
+ function ColorWidget(props) {
366
+ const val = props.value ?? '';
367
+ return (_jsxs("div", { className: "gz-color-widget", children: [_jsx("input", { type: "color", value: val || '#667eea', onChange: (e) => props.onChange(e.target.value) }), _jsx("input", { type: "text", value: val, onChange: (e) => props.onChange(e.target.value), placeholder: "#667eea" })] }));
35
368
  }
36
- const customWidgets = {
369
+ function TagsWidget(props) {
370
+ const tags = Array.isArray(props.value) ? props.value : [];
371
+ const [input, setInput] = React.useState('');
372
+ const inputRef = React.useRef(null);
373
+ const addTag = (tag) => {
374
+ const trimmed = tag.trim();
375
+ if (trimmed && !tags.includes(trimmed))
376
+ props.onChange([...tags, trimmed]);
377
+ setInput('');
378
+ };
379
+ const removeTag = (index) => props.onChange(tags.filter((_, i) => i !== index));
380
+ const handleKeyDown = (e) => {
381
+ if (e.key === 'Enter' || e.key === ',') {
382
+ e.preventDefault();
383
+ addTag(input);
384
+ }
385
+ else if (e.key === 'Backspace' && !input && tags.length > 0) {
386
+ removeTag(tags.length - 1);
387
+ }
388
+ };
389
+ return (_jsxs("div", { className: "gz-tags", onClick: () => inputRef.current?.focus(), children: [tags.map((tag, i) => (_jsxs("span", { className: "gz-tag", children: [tag, _jsx("button", { type: "button", className: "gz-tag-remove", onClick: (e) => { e.stopPropagation(); removeTag(i); }, "aria-label": `Remove ${tag}`, children: "\u00D7" })] }, `${tag}-${i}`))), _jsx("input", { ref: inputRef, className: "gz-tags-input", value: input, onChange: (e) => setInput(e.target.value), onKeyDown: handleKeyDown, onBlur: () => { if (input)
390
+ addTag(input); }, placeholder: tags.length === 0 ? 'Type and press Enter...' : '' })] }));
391
+ }
392
+ function ImageWidget(props) {
393
+ const url = props.value ?? '';
394
+ const [broken, setBroken] = React.useState(false);
395
+ React.useEffect(() => { setBroken(false); }, [url]);
396
+ return (_jsxs("div", { children: [_jsx("input", { type: "text", value: url, onChange: (e) => props.onChange(e.target.value), placeholder: "https://example.com/image.png" }), _jsx("div", { className: `gz-image-preview${url && !broken ? ' has-image' : ''}`, children: url && !broken
397
+ ? _jsx("img", { src: url, alt: "Preview", onError: () => setBroken(true) })
398
+ : _jsx("div", { className: "gz-image-preview-empty", children: url ? 'Image failed to load' : 'Paste an image URL above' }) })] }));
399
+ }
400
+ function LinkWidget(props) {
401
+ return _jsx("input", { type: "url", value: props.value ?? '', onChange: (e) => props.onChange(e.target.value), placeholder: "https://..." });
402
+ }
403
+ function SlugWidget(props) {
404
+ const toSlug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
405
+ return (_jsxs("div", { className: "gz-slug-widget", children: [_jsx("input", { type: "text", value: props.value ?? '', onChange: (e) => props.onChange(toSlug(e.target.value)), placeholder: "my-page-slug" }), _jsx("div", { className: "gz-slug-hint", children: "URL-safe identifier \u2014 lowercase, hyphens only" })] }));
406
+ }
407
+ function CodeWidget(props) {
408
+ const language = props.schema.language;
409
+ return (_jsxs("div", { className: "gz-code-widget", children: [_jsx("textarea", { value: props.value ?? '', onChange: (e) => props.onChange(e.target.value), placeholder: props.placeholder, rows: 15, spellCheck: false }), language && _jsx("div", { className: "gz-code-hint", children: language })] }));
410
+ }
411
+ function JsonWidget(props) {
412
+ const [error, setError] = React.useState(null);
413
+ const [text, setText] = React.useState(() => {
414
+ if (props.value == null)
415
+ return '';
416
+ if (typeof props.value === 'string')
417
+ return props.value;
418
+ return JSON.stringify(props.value, null, 2);
419
+ });
420
+ const handleBlur = () => {
421
+ if (!text.trim()) {
422
+ setError(null);
423
+ return;
424
+ }
425
+ try {
426
+ JSON.parse(text);
427
+ setError(null);
428
+ }
429
+ catch {
430
+ setError('Invalid JSON');
431
+ }
432
+ };
433
+ const handleChange = (value) => {
434
+ setText(value);
435
+ try {
436
+ const parsed = JSON.parse(value);
437
+ setError(null);
438
+ props.onChange(parsed);
439
+ }
440
+ catch {
441
+ // Don't set error while typing — only on blur
442
+ }
443
+ };
444
+ return (_jsxs("div", { className: "gz-json-widget", children: [_jsx("textarea", { value: text, onChange: (e) => handleChange(e.target.value), onBlur: handleBlur, placeholder: '{ "key": "value" }', rows: 10, spellCheck: false }), error && _jsx("div", { className: "gz-json-error", children: error })] }));
445
+ }
446
+ // ---------------------------------------------------------------------------
447
+ // Rich Text Widget (Tiptap)
448
+ // ---------------------------------------------------------------------------
449
+ function RichTextWidget(props) {
450
+ const onChangeRef = React.useRef(props.onChange);
451
+ onChangeRef.current = props.onChange;
452
+ const editor = useEditor({
453
+ extensions: [
454
+ StarterKit.configure({
455
+ heading: { levels: [2, 3] },
456
+ codeBlock: { HTMLAttributes: { spellcheck: 'false' } },
457
+ }),
458
+ Link.configure({ openOnClick: false, HTMLAttributes: { rel: 'noopener noreferrer' } }),
459
+ Placeholder.configure({ placeholder: 'Start writing...' }),
460
+ ],
461
+ content: props.value || '',
462
+ onUpdate: ({ editor: e }) => onChangeRef.current(e.getHTML()),
463
+ immediatelyRender: true,
464
+ });
465
+ if (!editor)
466
+ return null;
467
+ const Btn = ({ active, onClick, children }) => (_jsx("button", { type: "button", className: `gz-rt-btn${active ? ' active' : ''}`, onMouseDown: (e) => { e.preventDefault(); onClick(); }, children: children }));
468
+ const addLink = () => {
469
+ const prev = editor.getAttributes('link').href;
470
+ const url = prompt('URL:', prev ?? 'https://');
471
+ if (url === null)
472
+ return;
473
+ if (url === '') {
474
+ editor.chain().focus().extendMarkRange('link').unsetLink().run();
475
+ return;
476
+ }
477
+ editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
478
+ };
479
+ return (_jsxs("div", { className: "gz-richtext", children: [_jsxs("div", { className: "gz-richtext-toolbar", children: [_jsx(Btn, { active: editor.isActive('bold'), onClick: () => editor.chain().focus().toggleBold().run(), children: "B" }), _jsx(Btn, { active: editor.isActive('italic'), onClick: () => editor.chain().focus().toggleItalic().run(), children: "I" }), _jsx(Btn, { active: editor.isActive('strike'), onClick: () => editor.chain().focus().toggleStrike().run(), children: "S" }), _jsx(Btn, { active: editor.isActive('code'), onClick: () => editor.chain().focus().toggleCode().run(), children: "</>" }), _jsx("div", { className: "gz-rt-sep" }), _jsx(Btn, { active: editor.isActive('heading', { level: 2 }), onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), children: "H2" }), _jsx(Btn, { active: editor.isActive('heading', { level: 3 }), onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(), children: "H3" }), _jsx("div", { className: "gz-rt-sep" }), _jsx(Btn, { active: editor.isActive('bulletList'), onClick: () => editor.chain().focus().toggleBulletList().run(), children: "\u2022" }), _jsx(Btn, { active: editor.isActive('orderedList'), onClick: () => editor.chain().focus().toggleOrderedList().run(), children: "1." }), _jsx(Btn, { active: editor.isActive('blockquote'), onClick: () => editor.chain().focus().toggleBlockquote().run(), children: "\u201C" }), _jsx("div", { className: "gz-rt-sep" }), _jsx(Btn, { active: editor.isActive('link'), onClick: addLink, children: "Link" }), _jsx(Btn, { onClick: () => editor.chain().focus().setHorizontalRule().run(), children: "\u2014" }), _jsx(Btn, { active: editor.isActive('codeBlock'), onClick: () => editor.chain().focus().toggleCodeBlock().run(), children: "Code" })] }), _jsx(BubbleMenu, { editor: editor, children: _jsxs("div", { className: "gz-bubble", children: [_jsx(Btn, { active: editor.isActive('bold'), onClick: () => editor.chain().focus().toggleBold().run(), children: "B" }), _jsx(Btn, { active: editor.isActive('italic'), onClick: () => editor.chain().focus().toggleItalic().run(), children: "I" }), _jsx(Btn, { active: editor.isActive('strike'), onClick: () => editor.chain().focus().toggleStrike().run(), children: "S" }), _jsx(Btn, { active: editor.isActive('code'), onClick: () => editor.chain().focus().toggleCode().run(), children: "</>" }), _jsx(Btn, { active: editor.isActive('link'), onClick: addLink, children: "Link" })] }) }), _jsx(EditorContent, { editor: editor })] }));
480
+ }
481
+ // ---------------------------------------------------------------------------
482
+ // Custom Templates
483
+ // ---------------------------------------------------------------------------
484
+ function GzArrayFieldTemplate(props) {
485
+ const ctx = props.registry.formContext;
486
+ const fieldPath = props.fieldPathId?.$id ?? '';
487
+ const handleDragEnd = (result) => {
488
+ if (!result.destination || result.source.index === result.destination.index)
489
+ return;
490
+ ctx?.reorderArray(fieldPath, result.source.index, result.destination.index);
491
+ };
492
+ if (props.items.length === 0) {
493
+ return (_jsxs("div", { className: "gz-array", children: [_jsxs("div", { className: "gz-array-header", children: [props.title && _jsx("span", { className: "gz-array-title", children: props.title }), _jsx("span", { className: "gz-array-count", children: "0" })] }), _jsxs("div", { className: "gz-array-empty", children: ["No items yet", props.canAdd && (_jsx("div", { style: { marginTop: '0.5rem' }, children: _jsx("button", { type: "button", className: "gz-array-add", onClick: props.onAddClick, children: "+ Add first item" }) }))] })] }));
494
+ }
495
+ return (_jsxs("div", { className: "gz-array", children: [_jsxs("div", { className: "gz-array-header", children: [props.title && _jsx("span", { className: "gz-array-title", children: props.title }), _jsx("span", { className: "gz-array-count", children: props.items.length }), props.canAdd && _jsx("button", { type: "button", className: "gz-array-add", onClick: props.onAddClick, children: "+ Add" })] }), _jsx(DragDropContext, { onDragEnd: handleDragEnd, children: _jsx(Droppable, { droppableId: fieldPath || 'array', children: (provided) => (_jsxs("div", { className: "gz-array-items", ref: provided.innerRef, ...provided.droppableProps, children: [props.items, provided.placeholder] })) }) })] }));
496
+ }
497
+ function GzArrayFieldItemTemplate(props) {
498
+ const [open, setOpen] = React.useState(true);
499
+ const summary = summarizeItem(props.formData);
500
+ const draggableId = `${props.buttonsProps.fieldPathId?.$id ?? 'item'}-${props.index}`;
501
+ return (_jsx(Draggable, { draggableId: draggableId, index: props.index, children: (provided, snapshot) => (_jsxs("div", { className: `gz-array-item${snapshot.isDragging ? ' dragging' : ''}`, ref: provided.innerRef, ...provided.draggableProps, children: [_jsxs("div", { className: "gz-array-item-header", onClick: () => setOpen(!open), children: [_jsx("span", { className: "gz-array-item-handle", ...provided.dragHandleProps, onClick: (e) => e.stopPropagation(), children: "\u2801\u2801" }), _jsx("span", { className: `gz-array-item-chevron${open ? ' open' : ''}`, children: "\u25B6" }), _jsx("span", { className: "gz-array-item-idx", children: props.index + 1 }), !open && _jsx("span", { className: `gz-array-item-summary${summary ? '' : ' empty'}`, children: summary || 'Empty item' }), _jsx("div", { className: "gz-array-item-actions", onClick: (e) => e.stopPropagation(), children: _jsx(GzArrayFieldItemButtonsTemplate, { ...props.buttonsProps }) })] }), _jsx("div", { className: `gz-array-item-body ${open ? 'expanded' : 'collapsed'}`, children: props.children })] })) }));
502
+ }
503
+ function GzArrayFieldItemButtonsTemplate(props) {
504
+ return (_jsxs(_Fragment, { children: [props.hasMoveUp && _jsx("button", { type: "button", className: "gz-btn-icon", onClick: props.onMoveUpItem, title: "Move up", children: "\u2191" }), props.hasMoveDown && _jsx("button", { type: "button", className: "gz-btn-icon", onClick: props.onMoveDownItem, title: "Move down", children: "\u2193" }), props.hasRemove && _jsx("button", { type: "button", className: "gz-btn-icon gz-btn-remove", onClick: props.onRemoveItem, title: "Remove", children: "\u00D7" })] }));
505
+ }
506
+ function GzObjectFieldTemplate(props) {
507
+ const isRoot = props.fieldPathId?.$id === 'root';
508
+ if (isRoot)
509
+ return _jsx(_Fragment, { children: props.properties.map((p) => p.content) });
510
+ return _jsx("div", { className: "gz-object-inline", children: props.properties.map((p) => p.content) });
511
+ }
512
+ function GzAddButton(props) {
513
+ return _jsx("button", { type: "button", className: "gz-array-add", onClick: props.onClick, disabled: props.disabled, children: "+ Add" });
514
+ }
515
+ const fieldWidgetCache = new Map();
516
+ /** Create an async widget that loads a FieldMount module and mounts it */
517
+ function getCustomFieldWidget(fieldName) {
518
+ const cached = fieldWidgetCache.get(fieldName);
519
+ if (cached)
520
+ return cached;
521
+ const CustomFieldWidget = (props) => {
522
+ const containerRef = React.useRef(null);
523
+ const mountRef = React.useRef(null);
524
+ const mountedRef = React.useRef(false);
525
+ const [loading, setLoading] = React.useState(true);
526
+ const [error, setError] = React.useState(null);
527
+ const ctx = props.registry?.formContext;
528
+ // Load the field module once
529
+ React.useEffect(() => {
530
+ if (!ctx?.fieldsBaseUrl) {
531
+ setError('No fields base URL configured');
532
+ setLoading(false);
533
+ return;
534
+ }
535
+ const base = `${ctx.fieldsBaseUrl}/${fieldName}`;
536
+ (async () => {
537
+ // Try extensions in order: .tsx/.ts (dev), .js (production)
538
+ for (const ext of ['.tsx', '.ts', '.js']) {
539
+ try {
540
+ const mod = await import(/* @vite-ignore */ `${base}${ext}`);
541
+ mountRef.current = (mod.default ?? mod);
542
+ setLoading(false);
543
+ return;
544
+ }
545
+ catch { /* try next */ }
546
+ }
547
+ setError(`Failed to load field "${fieldName}"`);
548
+ setLoading(false);
549
+ })();
550
+ }, [ctx?.fieldsBaseUrl]);
551
+ // Mount the FieldMount once loaded — unmount on cleanup
552
+ React.useEffect(() => {
553
+ if (loading || !containerRef.current || !mountRef.current)
554
+ return;
555
+ const el = containerRef.current;
556
+ const fm = mountRef.current;
557
+ fm.mount(el, {
558
+ value: props.value,
559
+ schema: props.schema,
560
+ theme: ctx?.theme ?? 'dark',
561
+ onChange: (v) => props.onChange(v),
562
+ });
563
+ mountedRef.current = true;
564
+ return () => { fm.unmount(el); mountedRef.current = false; };
565
+ }, [loading]);
566
+ // Update value without re-mounting — call mount again with new value
567
+ // FieldMount implementations should handle being called multiple times
568
+ React.useEffect(() => {
569
+ if (!mountedRef.current || !containerRef.current || !mountRef.current)
570
+ return;
571
+ mountRef.current.mount(containerRef.current, {
572
+ value: props.value,
573
+ schema: props.schema,
574
+ theme: ctx?.theme ?? 'dark',
575
+ onChange: (v) => props.onChange(v),
576
+ });
577
+ }, [props.value]);
578
+ if (loading)
579
+ return _jsxs("div", { style: { color: 'var(--gz-text-hint)', fontSize: '0.75rem', padding: '0.5rem 0' }, children: ["Loading ", fieldName, "..."] });
580
+ if (error)
581
+ return _jsx("div", { style: { color: 'var(--gz-error)', fontSize: '0.75rem' }, children: error });
582
+ return _jsx("div", { ref: containerRef });
583
+ };
584
+ fieldWidgetCache.set(fieldName, CustomFieldWidget);
585
+ return CustomFieldWidget;
586
+ }
587
+ // ---------------------------------------------------------------------------
588
+ // Registries
589
+ // ---------------------------------------------------------------------------
590
+ const builtinWidgets = {
37
591
  markdown: MarkdownWidget,
592
+ toggle: ToggleWidget,
593
+ color: ColorWidget,
594
+ tags: TagsWidget,
595
+ richtext: RichTextWidget,
596
+ image: ImageWidget,
597
+ link: LinkWidget,
598
+ slug: SlugWidget,
599
+ code: CodeWidget,
600
+ json: JsonWidget,
601
+ };
602
+ /** Build widgets object including any custom field widgets referenced in the schema */
603
+ function buildWidgets(jsonSchema) {
604
+ const widgets = { ...builtinWidgets };
605
+ const properties = jsonSchema.properties;
606
+ if (!properties)
607
+ return widgets;
608
+ for (const prop of Object.values(properties)) {
609
+ const fieldName = prop.field;
610
+ if (fieldName) {
611
+ widgets[`custom-field:${fieldName}`] = getCustomFieldWidget(fieldName);
612
+ }
613
+ }
614
+ return widgets;
615
+ }
616
+ const customTemplates = {
617
+ ArrayFieldTemplate: GzArrayFieldTemplate,
618
+ ArrayFieldItemTemplate: GzArrayFieldItemTemplate,
619
+ ArrayFieldItemButtonsTemplate: GzArrayFieldItemButtonsTemplate,
620
+ ObjectFieldTemplate: GzObjectFieldTemplate,
621
+ ButtonTemplates: { AddButton: GzAddButton },
38
622
  };
623
+ /**
624
+ * The default @rjsf form editor as a React component.
625
+ * Custom editors can embed this: `<DefaultEditorForm schema={schema} content={content} onChange={onChange} />`
626
+ */
627
+ export function DefaultEditorForm({ schema: jsonSchema, content, onChange, fieldsBaseUrl, theme }) {
628
+ const uiSchema = React.useMemo(() => buildUiSchema(jsonSchema), [jsonSchema]);
629
+ const widgets = React.useMemo(() => buildWidgets(jsonSchema), [jsonSchema]);
630
+ const [formData, setFormData] = React.useState(content);
631
+ const formDataRef = React.useRef(formData);
632
+ formDataRef.current = formData;
633
+ const undoStack = React.useRef([]);
634
+ const redoStack = React.useRef([]);
635
+ const isUndoRedo = React.useRef(false);
636
+ const pushHistory = (prev) => {
637
+ if (isUndoRedo.current)
638
+ return;
639
+ undoStack.current.push(prev);
640
+ if (undoStack.current.length > 50)
641
+ undoStack.current.shift();
642
+ redoStack.current = [];
643
+ };
644
+ const applyFormData = (data) => {
645
+ setFormData(data);
646
+ onChange(data);
647
+ };
648
+ const handleChange = (e) => {
649
+ pushHistory(formDataRef.current);
650
+ setFormData(e.formData);
651
+ onChange(e.formData);
652
+ };
653
+ React.useEffect(() => {
654
+ const handler = (e) => {
655
+ if ((e.metaKey || e.ctrlKey) && e.key === 'z') {
656
+ e.preventDefault();
657
+ if (e.shiftKey) {
658
+ if (redoStack.current.length === 0)
659
+ return;
660
+ const next = redoStack.current.pop();
661
+ undoStack.current.push(formDataRef.current);
662
+ isUndoRedo.current = true;
663
+ applyFormData(next);
664
+ isUndoRedo.current = false;
665
+ }
666
+ else {
667
+ if (undoStack.current.length === 0)
668
+ return;
669
+ const prev = undoStack.current.pop();
670
+ redoStack.current.push(formDataRef.current);
671
+ isUndoRedo.current = true;
672
+ applyFormData(prev);
673
+ isUndoRedo.current = false;
674
+ }
675
+ }
676
+ };
677
+ document.addEventListener('keydown', handler);
678
+ return () => document.removeEventListener('keydown', handler);
679
+ }, []);
680
+ const reorderArray = React.useCallback((fieldPath, fromIndex, toIndex) => {
681
+ setFormData((prev) => {
682
+ pushHistory(prev);
683
+ const parts = fieldPath.replace(/^root_/, '').split('_');
684
+ const key = parts[0];
685
+ const arr = prev[key];
686
+ if (!Array.isArray(arr))
687
+ return prev;
688
+ const next = [...arr];
689
+ const [moved] = next.splice(fromIndex, 1);
690
+ next.splice(toIndex, 0, moved);
691
+ const updated = { ...prev, [key]: next };
692
+ onChange(updated);
693
+ return updated;
694
+ });
695
+ }, [onChange]);
696
+ const formContext = React.useMemo(() => ({ reorderArray, fieldsBaseUrl, theme }), [reorderArray, fieldsBaseUrl, theme]);
697
+ return (_jsxs(_Fragment, { children: [_jsx("style", { children: STYLES }), _jsx("div", { className: "gz-editor", children: _jsx(Form, { schema: jsonSchema, uiSchema: uiSchema, formData: formData, onChange: handleChange, validator: validator, widgets: widgets, templates: customTemplates, formContext: formContext, liveValidate: false, omitExtraData: true, noHtml5Validate: true }) })] }));
698
+ }
699
+ // ---------------------------------------------------------------------------
700
+ // Mount — wraps DefaultEditorForm for the mount/unmount lifecycle
701
+ // ---------------------------------------------------------------------------
39
702
  export function createEditorMount(jsonSchema) {
40
703
  return {
41
- mount(el, { content, onChange }) {
704
+ mount(el, { content, schema, theme, onChange, fieldsBaseUrl }) {
42
705
  const existing = roots.get(el);
43
706
  if (existing)
44
707
  existing.unmount();
45
708
  const root = createRoot(el);
46
709
  roots.set(el, root);
47
- function EditorForm() {
48
- const [formData, setFormData] = React.useState(content);
49
- const handleChange = (e) => {
50
- setFormData(e.formData);
51
- onChange(e.formData);
52
- };
53
- return (_jsxs(_Fragment, { children: [_jsx("style", { children: STYLES }), _jsx("div", { className: "gz-editor", children: _jsx(Form, { schema: jsonSchema, uiSchema: { 'ui:submitButtonOptions': { norender: true } }, formData: formData, onChange: handleChange, validator: validator, widgets: customWidgets, liveValidate: true, omitExtraData: true }) })] }));
54
- }
55
- root.render(_jsx(EditorForm, {}));
710
+ root.render(_jsx(DefaultEditorForm, { schema: schema ?? jsonSchema, content: content, theme: theme, fieldsBaseUrl: fieldsBaseUrl, onChange: onChange }));
56
711
  },
57
712
  unmount(el) {
58
713
  const root = roots.get(el);