vibespot 0.4.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.
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Field editor sidebar — edit module field values with live preview.
3
+ */
4
+
5
+ const editorEl = document.getElementById("field-editor");
6
+ const editorTitle = document.getElementById("field-editor-title");
7
+ const editorContent = document.getElementById("field-editor-content");
8
+ const editorClose = document.getElementById("field-editor-close");
9
+
10
+ let currentEditModule = null;
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Open / close
14
+ // ---------------------------------------------------------------------------
15
+
16
+ async function openFieldEditor(moduleName) {
17
+ currentEditModule = moduleName;
18
+ editorTitle.textContent = moduleName;
19
+ editorEl.classList.add("open");
20
+
21
+ // Fetch module data
22
+ try {
23
+ const res = await fetch("/api/modules");
24
+ const data = await res.json();
25
+ const mod = data.modules.find((m) => m.moduleName === moduleName);
26
+ if (!mod) {
27
+ editorContent.innerHTML = "<p>Module not found</p>";
28
+ return;
29
+ }
30
+
31
+ const fields = JSON.parse(mod.fieldsJson);
32
+ renderFieldForm(fields, moduleName);
33
+ } catch (err) {
34
+ editorContent.innerHTML = `<p>Error: ${err.message}</p>`;
35
+ }
36
+ }
37
+
38
+ function closeFieldEditor() {
39
+ editorEl.classList.remove("open");
40
+ currentEditModule = null;
41
+ }
42
+
43
+ editorClose.addEventListener("click", closeFieldEditor);
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Render field form
47
+ // ---------------------------------------------------------------------------
48
+
49
+ function renderFieldForm(fields, moduleName, prefix = "") {
50
+ editorContent.innerHTML = "";
51
+
52
+ for (const field of fields) {
53
+ const fullPath = prefix ? `${prefix}.${field.name}` : field.name;
54
+
55
+ // Skip meta fields
56
+ if (field.name === "id") continue;
57
+
58
+ if (field.type === "group" && field.children) {
59
+ // Render group header
60
+ const group = document.createElement("div");
61
+ group.className = "field-group";
62
+
63
+ if (field.tab === "STYLE") {
64
+ const tabLabel = document.createElement("div");
65
+ tabLabel.className = "field-group__tab-label";
66
+ tabLabel.textContent = "Style";
67
+ group.appendChild(tabLabel);
68
+ }
69
+
70
+ const label = document.createElement("label");
71
+ label.className = "field-group__label";
72
+ label.textContent = field.label || field.name;
73
+ group.appendChild(label);
74
+
75
+ // Render children
76
+ for (const child of field.children) {
77
+ const childEl = createFieldInput(child, moduleName, `${fullPath}.${child.name}`);
78
+ if (childEl) group.appendChild(childEl);
79
+ }
80
+
81
+ editorContent.appendChild(group);
82
+ } else {
83
+ const el = createFieldInput(field, moduleName, fullPath);
84
+ if (el) editorContent.appendChild(el);
85
+ }
86
+ }
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Create field inputs by type
91
+ // ---------------------------------------------------------------------------
92
+
93
+ function createFieldInput(field, moduleName, fullPath) {
94
+ const group = document.createElement("div");
95
+ group.className = "field-group";
96
+
97
+ const label = document.createElement("label");
98
+ label.className = "field-group__label";
99
+ label.textContent = field.label || field.name;
100
+ group.appendChild(label);
101
+
102
+ switch (field.type) {
103
+ case "text": {
104
+ const input = document.createElement("input");
105
+ input.type = "text";
106
+ input.className = "field-input";
107
+ input.value = field.default || "";
108
+ input.addEventListener("change", () => {
109
+ updateField(moduleName, fullPath, input.value);
110
+ });
111
+ group.appendChild(input);
112
+ break;
113
+ }
114
+
115
+ case "richtext": {
116
+ const textarea = document.createElement("textarea");
117
+ textarea.className = "field-input";
118
+ textarea.rows = 3;
119
+ textarea.value = field.default || "";
120
+ textarea.addEventListener("change", () => {
121
+ updateField(moduleName, fullPath, textarea.value);
122
+ });
123
+ group.appendChild(textarea);
124
+ break;
125
+ }
126
+
127
+ case "color": {
128
+ const wrapper = document.createElement("div");
129
+ wrapper.className = "field-color";
130
+
131
+ const picker = document.createElement("input");
132
+ picker.type = "color";
133
+ picker.className = "field-color__picker";
134
+ picker.value = field.default?.color || "#000000";
135
+
136
+ const hex = document.createElement("input");
137
+ hex.type = "text";
138
+ hex.className = "field-input field-color__hex";
139
+ hex.value = field.default?.color || "#000000";
140
+
141
+ picker.addEventListener("input", () => {
142
+ hex.value = picker.value;
143
+ updateField(moduleName, fullPath, {
144
+ color: picker.value,
145
+ opacity: field.default?.opacity ?? 100,
146
+ });
147
+ });
148
+
149
+ hex.addEventListener("change", () => {
150
+ picker.value = hex.value;
151
+ updateField(moduleName, fullPath, {
152
+ color: hex.value,
153
+ opacity: field.default?.opacity ?? 100,
154
+ });
155
+ });
156
+
157
+ wrapper.appendChild(picker);
158
+ wrapper.appendChild(hex);
159
+ group.appendChild(wrapper);
160
+ break;
161
+ }
162
+
163
+ case "image": {
164
+ const input = document.createElement("input");
165
+ input.type = "text";
166
+ input.className = "field-input";
167
+ input.placeholder = "Image URL";
168
+ input.value = field.default?.src || "";
169
+ input.addEventListener("change", () => {
170
+ updateField(moduleName, fullPath, {
171
+ src: input.value,
172
+ alt: field.default?.alt || "",
173
+ });
174
+ });
175
+ group.appendChild(input);
176
+ break;
177
+ }
178
+
179
+ case "link": {
180
+ const input = document.createElement("input");
181
+ input.type = "text";
182
+ input.className = "field-input";
183
+ input.placeholder = "URL";
184
+ input.value = field.default?.url?.href || "";
185
+ input.addEventListener("change", () => {
186
+ updateField(moduleName, fullPath, {
187
+ url: { href: input.value, type: "EXTERNAL" },
188
+ open_in_new_tab: field.default?.open_in_new_tab ?? false,
189
+ no_follow: field.default?.no_follow ?? false,
190
+ });
191
+ });
192
+ group.appendChild(input);
193
+ break;
194
+ }
195
+
196
+ case "number": {
197
+ const input = document.createElement("input");
198
+ input.type = "number";
199
+ input.className = "field-input";
200
+ input.value = field.default ?? 0;
201
+ input.addEventListener("change", () => {
202
+ updateField(moduleName, fullPath, Number(input.value));
203
+ });
204
+ group.appendChild(input);
205
+ break;
206
+ }
207
+
208
+ case "boolean": {
209
+ const wrapper = document.createElement("label");
210
+ wrapper.style.display = "flex";
211
+ wrapper.style.alignItems = "center";
212
+ wrapper.style.gap = "8px";
213
+ wrapper.style.cursor = "pointer";
214
+
215
+ const checkbox = document.createElement("input");
216
+ checkbox.type = "checkbox";
217
+ checkbox.checked = field.default ?? false;
218
+ checkbox.addEventListener("change", () => {
219
+ updateField(moduleName, fullPath, checkbox.checked);
220
+ });
221
+
222
+ const span = document.createElement("span");
223
+ span.textContent = field.label || field.name;
224
+ span.style.fontSize = "13px";
225
+
226
+ wrapper.appendChild(checkbox);
227
+ wrapper.appendChild(span);
228
+ group.innerHTML = "";
229
+ group.appendChild(wrapper);
230
+ break;
231
+ }
232
+
233
+ case "choice": {
234
+ const select = document.createElement("select");
235
+ select.className = "field-input";
236
+
237
+ const choices = field.choices || [];
238
+ for (const choice of choices) {
239
+ const option = document.createElement("option");
240
+ if (Array.isArray(choice)) {
241
+ option.value = choice[0];
242
+ option.textContent = choice[1];
243
+ } else {
244
+ option.value = choice;
245
+ option.textContent = choice;
246
+ }
247
+ select.appendChild(option);
248
+ }
249
+ select.value = field.default || "";
250
+ select.addEventListener("change", () => {
251
+ updateField(moduleName, fullPath, select.value);
252
+ });
253
+ group.appendChild(select);
254
+ break;
255
+ }
256
+
257
+ default:
258
+ // Unknown field type — show as text input
259
+ if (typeof field.default === "string" || typeof field.default === "number") {
260
+ const input = document.createElement("input");
261
+ input.type = "text";
262
+ input.className = "field-input";
263
+ input.value = field.default ?? "";
264
+ input.addEventListener("change", () => {
265
+ updateField(moduleName, fullPath, input.value);
266
+ });
267
+ group.appendChild(input);
268
+ } else {
269
+ return null;
270
+ }
271
+ }
272
+
273
+ return group;
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // Update field and refresh preview
278
+ // ---------------------------------------------------------------------------
279
+
280
+ let updateTimer = null;
281
+
282
+ function updateField(moduleName, fieldPath, value) {
283
+ // Debounce updates
284
+ clearTimeout(updateTimer);
285
+ updateTimer = setTimeout(() => {
286
+ fetch("/api/field", {
287
+ method: "POST",
288
+ headers: { "Content-Type": "application/json" },
289
+ body: JSON.stringify({ moduleName, fieldPath, value }),
290
+ }).then(() => refreshPreview());
291
+ }, 300);
292
+ }