@pixelated-tech/components 3.4.3 → 3.5.1

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 (241) hide show
  1. package/README.md +12 -191
  2. package/dist/components/admin/componentusage/componentAnalysis.js +12 -4
  3. package/dist/components/admin/componentusage/componentDiscovery.js +20 -6
  4. package/dist/components/admin/site-health/site-health-accessibility.js +5 -1
  5. package/dist/components/admin/site-health/site-health-axe-core.js +4 -0
  6. package/dist/components/admin/site-health/site-health-cloudwatch.integration.js +0 -5
  7. package/dist/components/admin/site-health/site-health-cloudwatch.js +7 -1
  8. package/dist/components/admin/site-health/site-health-core-web-vitals.integration.js +3 -3
  9. package/dist/components/admin/site-health/site-health-dependency-vulnerabilities.js +4 -0
  10. package/dist/components/admin/site-health/site-health-github.js +8 -2
  11. package/dist/components/admin/site-health/site-health-google-analytics.js +6 -0
  12. package/dist/components/admin/site-health/site-health-google-search-console.js +6 -0
  13. package/dist/components/admin/site-health/site-health-on-site-seo.integration.js +128 -55
  14. package/dist/components/admin/site-health/site-health-on-site-seo.js +4 -0
  15. package/dist/components/admin/site-health/site-health-overview.js +11 -4
  16. package/dist/components/admin/site-health/site-health-performance.js +4 -0
  17. package/dist/components/admin/site-health/site-health-security.js +7 -3
  18. package/dist/components/admin/site-health/site-health-seo.js +5 -1
  19. package/dist/components/admin/site-health/site-health-template.js +20 -9
  20. package/dist/components/admin/site-health/site-health-uptime.js +4 -0
  21. package/dist/components/callout/callout.js +0 -10
  22. package/dist/components/carousel/carousel.js +15 -4
  23. package/dist/components/carousel/tiles.js +1 -1
  24. package/dist/components/cms/contentful.items.components.js +3 -4
  25. package/dist/components/cms/flickr.js +1 -1
  26. package/dist/components/cms/google.reviews.components.js +3 -3
  27. package/dist/components/cms/instagram.components.js +15 -5
  28. package/dist/components/cms/smartimage.js +2 -2
  29. package/dist/components/cms/wordpress.components.js +32 -6
  30. package/dist/components/cms/yelp.js +5 -0
  31. package/dist/components/config/config.server.js +7 -1
  32. package/dist/components/general/accordion.js +4 -3
  33. package/dist/components/general/css.js +0 -1
  34. package/dist/components/general/image.js +0 -1
  35. package/dist/components/general/loading.js +2 -1
  36. package/dist/components/general/microinteractions.js +0 -1
  37. package/dist/components/general/modal.css +2 -4
  38. package/dist/components/general/modal.js +72 -30
  39. package/dist/components/general/sidepanel.js +16 -0
  40. package/dist/components/general/tab.js +1 -0
  41. package/dist/components/menu/menu-accordion.css +1 -1
  42. package/dist/components/menu/menu-accordion.js +15 -4
  43. package/dist/components/menu/menu-expando.js +21 -19
  44. package/dist/components/menu/menu-simple.js +14 -14
  45. package/dist/components/nerdjoke/nerdjoke.js +1 -1
  46. package/dist/components/seo/faq-accordion.css +125 -0
  47. package/dist/components/seo/faq-accordion.js +56 -0
  48. package/dist/components/seo/googlesearch.js +0 -1
  49. package/dist/components/seo/schema-blogposting.js +6 -1
  50. package/dist/components/seo/schema-faq.js +6 -0
  51. package/dist/components/seo/schema-recipe.js +34 -1
  52. package/dist/components/seo/schema-services.js +20 -2
  53. package/dist/components/shoppingcart/ebay.components.js +3 -3
  54. package/dist/components/shoppingcart/shoppingcart.components.js +76 -28
  55. package/dist/components/shoppingcart/shoppingcart.functions.js +4 -4
  56. package/dist/components/sitebuilder/config/CompoundFontSelector.js +13 -4
  57. package/dist/components/sitebuilder/config/ConfigBuilder.css +194 -5
  58. package/dist/components/sitebuilder/config/ConfigBuilder.js +183 -17
  59. package/dist/components/sitebuilder/config/FontSelector.js +13 -2
  60. package/dist/components/sitebuilder/config/routes-form.json +67 -0
  61. package/dist/components/sitebuilder/config/siteinfo-form.json +28 -14
  62. package/dist/components/sitebuilder/config/visualdesignform.json +4 -4
  63. package/dist/components/sitebuilder/form/formbuilder.js +1 -0
  64. package/dist/components/sitebuilder/form/formcomponents.js +2 -3
  65. package/dist/components/sitebuilder/form/formengine.js +6 -5
  66. package/dist/components/sitebuilder/form/formvalidator.js +5 -0
  67. package/dist/components/sitebuilder/page/components/PageBuilderUI.js +5 -1
  68. package/dist/components/structured/buzzwordbingo.css +0 -1
  69. package/dist/components/structured/recipe.js +1 -1
  70. package/dist/components/structured/socialcard.js +2 -2
  71. package/dist/components/utilities/functions.js +82 -1
  72. package/dist/components/utilities/gemini-api.client.js +76 -0
  73. package/dist/components/utilities/gemini-api.server.js +185 -0
  74. package/dist/data/routes.json +5 -5
  75. package/dist/index.adminclient.js +30 -0
  76. package/dist/index.adminserver.js +19 -0
  77. package/dist/index.js +11 -18
  78. package/dist/index.server.js +16 -28
  79. package/dist/types/components/admin/componentusage/componentAnalysis.d.ts.map +1 -1
  80. package/dist/types/components/admin/componentusage/componentDiscovery.d.ts +1 -1
  81. package/dist/types/components/admin/componentusage/componentDiscovery.d.ts.map +1 -1
  82. package/dist/types/components/admin/site-health/site-health-accessibility.d.ts +7 -4
  83. package/dist/types/components/admin/site-health/site-health-accessibility.d.ts.map +1 -1
  84. package/dist/types/components/admin/site-health/site-health-axe-core.d.ts +7 -4
  85. package/dist/types/components/admin/site-health/site-health-axe-core.d.ts.map +1 -1
  86. package/dist/types/components/admin/site-health/site-health-cloudwatch.d.ts +9 -6
  87. package/dist/types/components/admin/site-health/site-health-cloudwatch.d.ts.map +1 -1
  88. package/dist/types/components/admin/site-health/site-health-cloudwatch.integration.d.ts.map +1 -1
  89. package/dist/types/components/admin/site-health/site-health-dependency-vulnerabilities.d.ts +7 -4
  90. package/dist/types/components/admin/site-health/site-health-dependency-vulnerabilities.d.ts.map +1 -1
  91. package/dist/types/components/admin/site-health/site-health-github.d.ts +9 -6
  92. package/dist/types/components/admin/site-health/site-health-github.d.ts.map +1 -1
  93. package/dist/types/components/admin/site-health/site-health-google-analytics.d.ts +9 -6
  94. package/dist/types/components/admin/site-health/site-health-google-analytics.d.ts.map +1 -1
  95. package/dist/types/components/admin/site-health/site-health-google-search-console.d.ts +9 -6
  96. package/dist/types/components/admin/site-health/site-health-google-search-console.d.ts.map +1 -1
  97. package/dist/types/components/admin/site-health/site-health-on-site-seo.d.ts +8 -3
  98. package/dist/types/components/admin/site-health/site-health-on-site-seo.d.ts.map +1 -1
  99. package/dist/types/components/admin/site-health/site-health-on-site-seo.integration.d.ts.map +1 -1
  100. package/dist/types/components/admin/site-health/site-health-overview.d.ts +7 -4
  101. package/dist/types/components/admin/site-health/site-health-overview.d.ts.map +1 -1
  102. package/dist/types/components/admin/site-health/site-health-performance.d.ts +7 -4
  103. package/dist/types/components/admin/site-health/site-health-performance.d.ts.map +1 -1
  104. package/dist/types/components/admin/site-health/site-health-security.d.ts +7 -4
  105. package/dist/types/components/admin/site-health/site-health-security.d.ts.map +1 -1
  106. package/dist/types/components/admin/site-health/site-health-seo.d.ts +7 -4
  107. package/dist/types/components/admin/site-health/site-health-seo.d.ts.map +1 -1
  108. package/dist/types/components/admin/site-health/site-health-template.d.ts +13 -10
  109. package/dist/types/components/admin/site-health/site-health-template.d.ts.map +1 -1
  110. package/dist/types/components/admin/site-health/site-health-types.d.ts +0 -21
  111. package/dist/types/components/admin/site-health/site-health-types.d.ts.map +1 -1
  112. package/dist/types/components/admin/site-health/site-health-uptime.d.ts +7 -4
  113. package/dist/types/components/admin/site-health/site-health-uptime.d.ts.map +1 -1
  114. package/dist/types/components/callout/callout.d.ts +3 -3
  115. package/dist/types/components/callout/callout.d.ts.map +1 -1
  116. package/dist/types/components/carousel/carousel.d.ts +16 -7
  117. package/dist/types/components/carousel/carousel.d.ts.map +1 -1
  118. package/dist/types/components/carousel/tiles.d.ts +3 -6
  119. package/dist/types/components/carousel/tiles.d.ts.map +1 -1
  120. package/dist/types/components/cms/flickr.d.ts +3 -6
  121. package/dist/types/components/cms/flickr.d.ts.map +1 -1
  122. package/dist/types/components/cms/google.reviews.components.d.ts +1 -7
  123. package/dist/types/components/cms/google.reviews.components.d.ts.map +1 -1
  124. package/dist/types/components/cms/hubspot.components.d.ts +1 -2
  125. package/dist/types/components/cms/hubspot.components.d.ts.map +1 -1
  126. package/dist/types/components/cms/instagram.components.d.ts +14 -9
  127. package/dist/types/components/cms/instagram.components.d.ts.map +1 -1
  128. package/dist/types/components/cms/smartimage.d.ts +2 -28
  129. package/dist/types/components/cms/smartimage.d.ts.map +1 -1
  130. package/dist/types/components/cms/wordpress.components.d.ts +33 -14
  131. package/dist/types/components/cms/wordpress.components.d.ts.map +1 -1
  132. package/dist/types/components/cms/yelp.d.ts +9 -4
  133. package/dist/types/components/cms/yelp.d.ts.map +1 -1
  134. package/dist/types/components/config/config.server.d.ts +9 -6
  135. package/dist/types/components/config/config.server.d.ts.map +1 -1
  136. package/dist/types/components/general/accordion.d.ts +3 -2
  137. package/dist/types/components/general/accordion.d.ts.map +1 -1
  138. package/dist/types/components/general/loading.d.ts +5 -1
  139. package/dist/types/components/general/loading.d.ts.map +1 -1
  140. package/dist/types/components/general/microinteractions.d.ts +1 -3
  141. package/dist/types/components/general/microinteractions.d.ts.map +1 -1
  142. package/dist/types/components/general/modal.d.ts +11 -5
  143. package/dist/types/components/general/modal.d.ts.map +1 -1
  144. package/dist/types/components/general/semantic.d.ts +3 -3
  145. package/dist/types/components/general/sidepanel.d.ts +20 -13
  146. package/dist/types/components/general/sidepanel.d.ts.map +1 -1
  147. package/dist/types/components/general/tab.d.ts +1 -2
  148. package/dist/types/components/general/tab.d.ts.map +1 -1
  149. package/dist/types/components/menu/menu-accordion.d.ts +22 -9
  150. package/dist/types/components/menu/menu-accordion.d.ts.map +1 -1
  151. package/dist/types/components/menu/menu-expando.d.ts +14 -5
  152. package/dist/types/components/menu/menu-expando.d.ts.map +1 -1
  153. package/dist/types/components/menu/menu-simple.d.ts +4 -5
  154. package/dist/types/components/menu/menu-simple.d.ts.map +1 -1
  155. package/dist/types/components/nerdjoke/nerdjoke.d.ts +1 -1
  156. package/dist/types/components/nerdjoke/nerdjoke.d.ts.map +1 -1
  157. package/dist/types/components/seo/faq-accordion.d.ts +18 -0
  158. package/dist/types/components/seo/faq-accordion.d.ts.map +1 -0
  159. package/dist/types/components/seo/googleanalytics.d.ts.map +1 -1
  160. package/dist/types/components/seo/metadata.components.d.ts +2 -2
  161. package/dist/types/components/seo/metadata.components.d.ts.map +1 -1
  162. package/dist/types/components/seo/schema-blogposting.d.ts +7 -4
  163. package/dist/types/components/seo/schema-blogposting.d.ts.map +1 -1
  164. package/dist/types/components/seo/schema-faq.d.ts +6 -0
  165. package/dist/types/components/seo/schema-faq.d.ts.map +1 -0
  166. package/dist/types/components/seo/schema-recipe.d.ts +29 -30
  167. package/dist/types/components/seo/schema-recipe.d.ts.map +1 -1
  168. package/dist/types/components/seo/schema-services.d.ts +19 -9
  169. package/dist/types/components/seo/schema-services.d.ts.map +1 -1
  170. package/dist/types/components/shoppingcart/paypal.d.ts +1 -1
  171. package/dist/types/components/shoppingcart/paypal.d.ts.map +1 -1
  172. package/dist/types/components/shoppingcart/shoppingcart.components.d.ts +77 -28
  173. package/dist/types/components/shoppingcart/shoppingcart.components.d.ts.map +1 -1
  174. package/dist/types/components/shoppingcart/shoppingcart.functions.d.ts +4 -23
  175. package/dist/types/components/shoppingcart/shoppingcart.functions.d.ts.map +1 -1
  176. package/dist/types/components/sitebuilder/config/CompoundFontSelector.d.ts +10 -11
  177. package/dist/types/components/sitebuilder/config/CompoundFontSelector.d.ts.map +1 -1
  178. package/dist/types/components/sitebuilder/config/ConfigBuilder.d.ts +41 -174
  179. package/dist/types/components/sitebuilder/config/ConfigBuilder.d.ts.map +1 -1
  180. package/dist/types/components/sitebuilder/config/FontSelector.d.ts +12 -13
  181. package/dist/types/components/sitebuilder/config/FontSelector.d.ts.map +1 -1
  182. package/dist/types/components/sitebuilder/form/formbuilder.d.ts +7 -3
  183. package/dist/types/components/sitebuilder/form/formbuilder.d.ts.map +1 -1
  184. package/dist/types/components/sitebuilder/form/formcomponents.d.ts +1 -1
  185. package/dist/types/components/sitebuilder/form/formcomponents.d.ts.map +1 -1
  186. package/dist/types/components/sitebuilder/form/formengine.d.ts +1 -2
  187. package/dist/types/components/sitebuilder/form/formengine.d.ts.map +1 -1
  188. package/dist/types/components/sitebuilder/form/formextractor.d.ts +5 -4
  189. package/dist/types/components/sitebuilder/form/formextractor.d.ts.map +1 -1
  190. package/dist/types/components/sitebuilder/form/formtypes.d.ts +3 -3
  191. package/dist/types/components/sitebuilder/form/formtypes.d.ts.map +1 -1
  192. package/dist/types/components/sitebuilder/form/formvalidator.d.ts +8 -3
  193. package/dist/types/components/sitebuilder/form/formvalidator.d.ts.map +1 -1
  194. package/dist/types/components/sitebuilder/page/components/ComponentPropertiesForm.d.ts +2 -3
  195. package/dist/types/components/sitebuilder/page/components/ComponentPropertiesForm.d.ts.map +1 -1
  196. package/dist/types/components/sitebuilder/page/components/ComponentSelector.d.ts +2 -3
  197. package/dist/types/components/sitebuilder/page/components/ComponentSelector.d.ts.map +1 -1
  198. package/dist/types/components/sitebuilder/page/components/ComponentTree.d.ts +2 -3
  199. package/dist/types/components/sitebuilder/page/components/ComponentTree.d.ts.map +1 -1
  200. package/dist/types/components/sitebuilder/page/components/PageBuilderUI.d.ts +8 -7
  201. package/dist/types/components/sitebuilder/page/components/PageBuilderUI.d.ts.map +1 -1
  202. package/dist/types/components/sitebuilder/page/components/PageEngine.d.ts.map +1 -1
  203. package/dist/types/components/sitebuilder/page/components/SaveLoadSection.d.ts +2 -3
  204. package/dist/types/components/sitebuilder/page/components/SaveLoadSection.d.ts.map +1 -1
  205. package/dist/types/components/sitebuilder/page/lib/componentMap.d.ts +1 -1
  206. package/dist/types/components/structured/markdown.d.ts +1 -3
  207. package/dist/types/components/structured/markdown.d.ts.map +1 -1
  208. package/dist/types/components/structured/recipe.d.ts +5 -32
  209. package/dist/types/components/structured/recipe.d.ts.map +1 -1
  210. package/dist/types/components/structured/socialcard.d.ts +4 -0
  211. package/dist/types/components/structured/socialcard.d.ts.map +1 -1
  212. package/dist/types/components/structured/timeline.d.ts +1 -3
  213. package/dist/types/components/structured/timeline.d.ts.map +1 -1
  214. package/dist/types/components/utilities/functions.d.ts +20 -0
  215. package/dist/types/components/utilities/functions.d.ts.map +1 -1
  216. package/dist/types/components/utilities/gemini-api.client.d.ts +38 -0
  217. package/dist/types/components/utilities/gemini-api.client.d.ts.map +1 -0
  218. package/dist/types/components/utilities/gemini-api.server.d.ts +17 -0
  219. package/dist/types/components/utilities/gemini-api.server.d.ts.map +1 -0
  220. package/dist/types/index.adminclient.d.ts +27 -0
  221. package/dist/types/index.adminclient.d.ts.map +1 -0
  222. package/dist/types/index.adminserver.d.ts +20 -0
  223. package/dist/types/index.adminserver.d.ts.map +1 -0
  224. package/dist/types/index.d.ts +11 -18
  225. package/dist/types/index.server.d.ts +6 -28
  226. package/dist/types/stories/general/sidepanel.stories.d.ts.map +1 -1
  227. package/dist/types/stories/general/smartimage.stories.d.ts +74 -2
  228. package/dist/types/stories/general/smartimage.stories.d.ts.map +1 -1
  229. package/package.json +19 -9
  230. package/README.COMPONENTS.md +0 -2310
  231. package/dist/components/cms/pixelated.linkedin.js +0 -180
  232. package/dist/components/cms/pixelated.linkedin1.js +0 -84
  233. package/dist/components/cms/pixelated.linkedin2.js +0 -92
  234. package/dist/types/components/cms/pixelated.linkedin.d.ts +0 -2
  235. package/dist/types/components/cms/pixelated.linkedin.d.ts.map +0 -1
  236. package/dist/types/components/cms/pixelated.linkedin1.d.ts +0 -2
  237. package/dist/types/components/cms/pixelated.linkedin1.d.ts.map +0 -1
  238. package/dist/types/components/cms/pixelated.linkedin2.d.ts +0 -2
  239. package/dist/types/components/cms/pixelated.linkedin2.d.ts.map +0 -1
  240. package/dist/types/tests/pixelated.menu-expando.test.d.ts +0 -2
  241. package/dist/types/tests/pixelated.menu-expando.test.d.ts.map +0 -1
@@ -162,8 +162,6 @@
162
162
  }
163
163
 
164
164
  .route-item {
165
- display: grid;
166
- grid-template-columns: 2fr 2fr 2fr 2fr auto;
167
165
  gap: 10px;
168
166
  align-items: center;
169
167
  padding: 10px;
@@ -172,11 +170,18 @@
172
170
  margin-bottom: 10px;
173
171
  }
174
172
 
175
- .route-item input {
173
+ .route-item input,
174
+ .route-item select,
175
+ .route-item textarea {
176
176
  padding: 6px 8px;
177
177
  border: 1px solid #ddd;
178
178
  border-radius: 3px;
179
- font-size: 14px;
179
+ font-size: var(--font-size5);
180
+ }
181
+
182
+ .route-item label {
183
+ font-size: var(--font-size5);
184
+ font-weight: bold;
180
185
  }
181
186
 
182
187
  .route-item button {
@@ -274,4 +279,188 @@
274
279
  font-size: 14px;
275
280
  margin-top: 8px;
276
281
  font-weight: 500;
277
- }
282
+ }
283
+ /* AI Recommendations Modal */
284
+ .ai-recommendations-modal {
285
+ max-width: 600px;
286
+ }
287
+
288
+ .ai-recommendations-modal h3 {
289
+ margin-top: 0;
290
+ color: #333;
291
+ }
292
+
293
+ .ai-loading {
294
+ text-align: center;
295
+ padding: 20px;
296
+ }
297
+
298
+ .loading-spinner {
299
+ border: 4px solid #f3f3f3;
300
+ border-top: 4px solid #007acc;
301
+ border-radius: 50%;
302
+ width: 30px;
303
+ height: 30px;
304
+ animation: spin 1s linear infinite;
305
+ margin: 10px auto;
306
+ }
307
+
308
+ @keyframes spin {
309
+ 0% { transform: rotate(0deg); }
310
+ 100% { transform: rotate(360deg); }
311
+ }
312
+
313
+ .ai-error {
314
+ color: #dc3545;
315
+ padding: 10px;
316
+ background-color: #f8d7da;
317
+ border-radius: 4px;
318
+ margin: 10px 0;
319
+ }
320
+
321
+ .ai-recommendations {
322
+ margin: 20px 0;
323
+ }
324
+
325
+ .recommendation-item {
326
+ margin-bottom: 20px;
327
+ padding: 15px;
328
+ background-color: #f8f9fa;
329
+ border-radius: 6px;
330
+ border-left: 4px solid #007acc;
331
+ }
332
+
333
+ .recommendation-item label {
334
+ display: block;
335
+ font-weight: bold;
336
+ margin-bottom: 8px;
337
+ cursor: pointer;
338
+ }
339
+
340
+ .recommendation-item input[type="checkbox"] {
341
+ margin-right: 8px;
342
+ accent-color: #007acc;
343
+ }
344
+
345
+ .recommendation-item label {
346
+ display: flex;
347
+ align-items: center;
348
+ cursor: pointer;
349
+ padding: 5px;
350
+ border-radius: 4px;
351
+ transition: background-color 0.2s;
352
+ }
353
+
354
+ .recommendation-item label:hover {
355
+ background-color: #f0f8ff;
356
+ }
357
+
358
+ .recommendation-content {
359
+ margin-left: 20px;
360
+ }
361
+
362
+ .current-value {
363
+ color: #666;
364
+ font-size: 0.9em;
365
+ margin-bottom: 5px;
366
+ }
367
+
368
+ .suggested-value {
369
+ color: #007acc;
370
+ font-weight: 500;
371
+ line-height: 1.4;
372
+ }
373
+
374
+ .modal-actions {
375
+ display: flex;
376
+ gap: 10px;
377
+ justify-content: flex-end;
378
+ margin-top: 20px;
379
+ padding-top: 15px;
380
+ border-top: 1px solid #dee2e6;
381
+ }
382
+
383
+ .modal-actions button {
384
+ padding: 8px 16px;
385
+ border: 1px solid #dee2e6;
386
+ border-radius: 4px;
387
+ background-color: #fff;
388
+ cursor: pointer;
389
+ font-size: 14px;
390
+ }
391
+
392
+ .modal-actions button:hover {
393
+ background-color: #f8f9fa;
394
+ }
395
+
396
+ .accept-button {
397
+ background-color: #28a745 !important;
398
+ color: white !important;
399
+ border-color: #28a745 !important;
400
+ }
401
+
402
+ .accept-button:hover {
403
+ background-color: #218838 !important;
404
+ }
405
+
406
+ .accept-button:disabled {
407
+ background-color: #6c757d !important;
408
+ border-color: #6c757d !important;
409
+ cursor: not-allowed;
410
+ color: white !important;
411
+ }
412
+
413
+ /* Route management buttons */
414
+ .route-buttons {
415
+ display: flex;
416
+ gap: 8px;
417
+ margin-top: 10px;
418
+ }
419
+
420
+ .route-button {
421
+ min-height: 32px;
422
+ display: inline-flex;
423
+ align-items: center;
424
+ gap: 4px;
425
+ padding: 6px 12px;
426
+ border: 1px solid #ced4da;
427
+ border-radius: 4px;
428
+ background-color: #fff;
429
+ color: #495057;
430
+ font-size: 14px;
431
+ cursor: pointer;
432
+ transition: all 0.2s ease;
433
+ }
434
+
435
+ .route-button:hover {
436
+ background-color: #f8f9fa;
437
+ border-color: #adb5bd;
438
+ }
439
+
440
+ .route-button.ai-recommend {
441
+ background-color: #4285f4;
442
+ color: #fff;
443
+ border-color: #4285f4;
444
+ }
445
+
446
+ .route-button.ai-recommend:hover {
447
+ background-color: #3367d6;
448
+ border-color: #3367d6;
449
+ }
450
+
451
+ .route-button.remove {
452
+ background-color: #dc3545;
453
+ color: #fff;
454
+ border-color: #dc3545;
455
+ }
456
+
457
+ .route-button.remove:hover {
458
+ background-color: #c82333;
459
+ border-color: #c82333;
460
+ }
461
+
462
+ .ai-icon {
463
+ font-size: 14px;
464
+ font-weight: bold;
465
+ color: #fff;
466
+ }
@@ -2,18 +2,25 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useEffect } from 'react';
4
4
  import PropTypes from 'prop-types';
5
+ import { Modal } from '../../general/modal';
5
6
  import { Tab } from '../../general/tab';
6
7
  import { Accordion } from '../../general/accordion';
8
+ import { createGeminiApiService } from '../../utilities/gemini-api.client';
7
9
  import { FormEngine } from '../form/formengine';
10
+ import { FormValidationProvider } from '../form/formvalidator';
11
+ import * as FC from '../form/formcomponents';
8
12
  import siteInfoForm from './siteinfo-form.json';
9
13
  import visualDesignForm from './visualdesignform.json';
14
+ import routesForm from './routes-form.json';
10
15
  import defaultConfigData from '../../../data/routes.json';
11
16
  import './ConfigBuilder.css';
12
17
  const RoutePropTypes = {
18
+ name: PropTypes.string,
13
19
  path: PropTypes.string.isRequired,
14
- component: PropTypes.string.isRequired,
15
20
  title: PropTypes.string,
16
21
  description: PropTypes.string,
22
+ keywords: PropTypes.arrayOf(PropTypes.string),
23
+ hidden: PropTypes.bool,
17
24
  };
18
25
  const SiteInfoPropTypes = {
19
26
  name: PropTypes.string.isRequired,
@@ -78,7 +85,7 @@ const SiteConfigPropTypes = {
78
85
  routes: PropTypes.arrayOf(PropTypes.shape(RoutePropTypes).isRequired).isRequired,
79
86
  visualdesign: PropTypes.shape(VisualDesignPropTypes).isRequired,
80
87
  };
81
- const ConfigBuilderPropTypes = {
88
+ ConfigBuilder.propTypes = {
82
89
  initialConfig: PropTypes.shape(SiteConfigPropTypes),
83
90
  onSave: PropTypes.func,
84
91
  };
@@ -98,18 +105,38 @@ export function ConfigBuilder(props) {
98
105
  });
99
106
  const [socialLinks, setSocialLinks] = useState(initialConfig?.siteInfo?.sameAs || ['']);
100
107
  const [isFormValid, setIsFormValid] = useState(false);
108
+ // AI Recommendations state
109
+ const [aiModalOpen, setAiModalOpen] = useState(false);
110
+ const [currentRouteIndex, setCurrentRouteIndex] = useState(null);
111
+ const [aiRecommendations, setAiRecommendations] = useState(null);
112
+ const [aiLoading, setAiLoading] = useState(false);
113
+ const [acceptTitle, setAcceptTitle] = useState(false);
114
+ const [acceptKeywords, setAcceptKeywords] = useState(false);
115
+ const [acceptDescription, setAcceptDescription] = useState(false);
101
116
  // Validate form whenever config changes
102
117
  useEffect(() => {
103
118
  const siteInfo = config.siteInfo || {};
104
- const isValid = (siteInfo.name || '').trim() !== '' &&
105
- (siteInfo.author || '').trim() !== '' &&
106
- (siteInfo.description || '').trim() !== '' &&
107
- (siteInfo.url || '').trim() !== '' &&
108
- (siteInfo.email || '').trim() !== '' &&
119
+ const isValid = String(siteInfo.name || '').trim() !== '' &&
120
+ String(siteInfo.author || '').trim() !== '' &&
121
+ String(siteInfo.description || '').trim() !== '' &&
122
+ String(siteInfo.url || '').trim() !== '' &&
123
+ String(siteInfo.email || '').trim() !== '' &&
109
124
  // Basic email validation
110
125
  /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(siteInfo.email || '');
111
126
  setIsFormValid(isValid);
112
127
  }, [config]);
128
+ // Handle AI modal visibility - now handled by Modal component isOpen prop
129
+ // useEffect(() => {
130
+ // console.log('AI modal effect running, aiModalOpen:', aiModalOpen);
131
+ // if (aiModalOpen) {
132
+ // const modal = document.getElementById('myModalai-recommendations');
133
+ // console.log('Modal element found:', modal);
134
+ // if (modal) {
135
+ // modal.style.display = 'block';
136
+ // console.log('Set modal display to block');
137
+ // }
138
+ // }
139
+ // }, [aiModalOpen]);
113
140
  const handleFileUpload = (event) => {
114
141
  const file = event.target.files?.[0];
115
142
  if (!file)
@@ -121,7 +148,19 @@ export function ConfigBuilder(props) {
121
148
  const parsedConfig = JSON.parse(jsonContent);
122
149
  // Validate the structure
123
150
  if (parsedConfig.siteInfo && parsedConfig.routes) {
124
- setConfig(parsedConfig);
151
+ // Ensure keywords are arrays for all routes
152
+ const normalizedRoutes = parsedConfig.routes.map((route) => ({
153
+ ...route,
154
+ keywords: Array.isArray(route.keywords)
155
+ ? route.keywords
156
+ : (typeof route.keywords === 'string'
157
+ ? route.keywords.split(',').map((k) => k.trim()).filter((k) => k.length > 0)
158
+ : [])
159
+ }));
160
+ setConfig({
161
+ ...parsedConfig,
162
+ routes: normalizedRoutes
163
+ });
125
164
  setSocialLinks(parsedConfig.siteInfo.sameAs || ['']);
126
165
  }
127
166
  else {
@@ -138,9 +177,18 @@ export function ConfigBuilder(props) {
138
177
  };
139
178
  useEffect(() => {
140
179
  if (initialConfig) {
180
+ // Ensure keywords are arrays for all routes
181
+ const normalizedRoutes = (initialConfig.routes || []).map((route) => ({
182
+ ...route,
183
+ keywords: Array.isArray(route.keywords)
184
+ ? route.keywords
185
+ : (typeof route.keywords === 'string'
186
+ ? route.keywords.split(',').map((k) => k.trim()).filter((k) => k.length > 0)
187
+ : [])
188
+ }));
141
189
  setConfig((prev) => ({
142
190
  siteInfo: { ...prev.siteInfo, ...initialConfig.siteInfo },
143
- routes: initialConfig.routes || [],
191
+ routes: normalizedRoutes,
144
192
  visualdesign: initialConfig.visualdesign || prev.visualdesign || {}
145
193
  }));
146
194
  setSocialLinks(initialConfig.siteInfo?.sameAs || ['']);
@@ -155,11 +203,18 @@ export function ConfigBuilder(props) {
155
203
  value: config.siteInfo[field.props.name] || '',
156
204
  defaultValue: config.siteInfo[field.props.name] || field.props.defaultValue || '',
157
205
  onChange: (value) => {
206
+ // Handle both direct values and event objects
207
+ let actualValue = value;
208
+ if (value && typeof value === 'object' && value.target) {
209
+ // It's an event object, extract the value
210
+ const target = value.target;
211
+ actualValue = target.type === 'checkbox' ? (target.checked ? target.value : '') : target.value;
212
+ }
158
213
  setConfig((prev) => ({
159
214
  ...prev,
160
215
  siteInfo: {
161
216
  ...prev.siteInfo,
162
- [field.props.name]: value
217
+ [field.props.name]: actualValue
163
218
  }
164
219
  }));
165
220
  }
@@ -174,7 +229,8 @@ export function ConfigBuilder(props) {
174
229
  ...field.props,
175
230
  value: (config.visualdesign && config.visualdesign[field.props.name]) ? (config.visualdesign[field.props.name].value ?? config.visualdesign[field.props.name]) : '',
176
231
  defaultValue: (config.visualdesign && config.visualdesign[field.props.name]) ? (config.visualdesign[field.props.name].value ?? config.visualdesign[field.props.name]) : field.props.defaultValue || '',
177
- onChange: (value) => {
232
+ onChange: (event) => {
233
+ const value = event.target.value;
178
234
  setConfig((prev) => ({
179
235
  ...prev,
180
236
  visualdesign: {
@@ -228,13 +284,18 @@ export function ConfigBuilder(props) {
228
284
  const addRoute = () => {
229
285
  setConfig(prev => ({
230
286
  ...prev,
231
- routes: [...prev.routes, { path: '', component: '', title: '', description: '' }]
287
+ routes: [...prev.routes, { name: '', path: '', title: '', description: '', keywords: [], hidden: false }]
232
288
  }));
233
289
  };
234
290
  const updateRoute = (index, field, value) => {
291
+ // Special handling for keywords field - convert comma-separated string to array
292
+ let processedValue = value;
293
+ if (field === 'keywords' && typeof value === 'string') {
294
+ processedValue = value.split(',').map((k) => k.trim()).filter((k) => k.length > 0);
295
+ }
235
296
  setConfig(prev => ({
236
297
  ...prev,
237
- routes: prev.routes.map((route, i) => i === index ? { ...route, [field]: value } : route)
298
+ routes: prev.routes.map((route, i) => i === index ? { ...route, [field]: processedValue } : route)
238
299
  }));
239
300
  };
240
301
  const removeRoute = (index) => {
@@ -250,6 +311,60 @@ export function ConfigBuilder(props) {
250
311
  }
251
312
  onSave?.(config);
252
313
  };
314
+ const handleAiRecommendations = async (routeIndex) => {
315
+ console.log('handleAiRecommendations called with routeIndex:', routeIndex);
316
+ setCurrentRouteIndex(routeIndex);
317
+ setAiLoading(true);
318
+ setAiModalOpen(true);
319
+ setAcceptTitle(false);
320
+ setAcceptKeywords(false);
321
+ setAcceptDescription(false);
322
+ try {
323
+ const geminiService = createGeminiApiService('dummy-key'); // API key handled server-side
324
+ const route = config.routes[routeIndex];
325
+ const result = await geminiService.generateRouteRecommendations({
326
+ route,
327
+ siteInfo: config.siteInfo,
328
+ baseUrl: config.siteInfo.url
329
+ });
330
+ if (result.success && result.data) {
331
+ setAiRecommendations(result.data);
332
+ }
333
+ else {
334
+ setAiRecommendations({ error: result.error || 'Failed to generate recommendations' });
335
+ }
336
+ }
337
+ catch (error) {
338
+ console.error('AI recommendation error:', error);
339
+ setAiRecommendations({ error: 'Failed to generate AI recommendations' });
340
+ }
341
+ finally {
342
+ setAiLoading(false);
343
+ }
344
+ };
345
+ const handleAcceptAiRecommendations = () => {
346
+ if (currentRouteIndex === null || !aiRecommendations)
347
+ return;
348
+ const updates = {};
349
+ if (acceptTitle && aiRecommendations.title) {
350
+ updates.title = aiRecommendations.title;
351
+ }
352
+ if (acceptKeywords && aiRecommendations.keywords) {
353
+ updates.keywords = aiRecommendations.keywords;
354
+ }
355
+ if (acceptDescription && aiRecommendations.description) {
356
+ updates.description = aiRecommendations.description;
357
+ }
358
+ if (Object.keys(updates).length > 0) {
359
+ setConfig(prev => ({
360
+ ...prev,
361
+ routes: prev.routes.map((route, i) => i === currentRouteIndex ? { ...route, ...updates } : route)
362
+ }));
363
+ }
364
+ setAiModalOpen(false);
365
+ setAiRecommendations(null);
366
+ setCurrentRouteIndex(null);
367
+ };
253
368
  return (_jsxs("div", { className: "config-builder", children: [_jsx("h2", { children: "Config Builder" }), _jsxs("div", { className: "file-upload-section", children: [_jsx("label", { htmlFor: "config-file-upload", className: "file-upload-label", children: "Load Configuration File:" }), _jsx("input", { id: "config-file-upload", type: "file", accept: ".json", onChange: handleFileUpload, className: "file-upload-input" })] }), _jsx(Tab, { tabs: [
254
369
  {
255
370
  id: 'siteinfo',
@@ -259,7 +374,41 @@ export function ConfigBuilder(props) {
259
374
  {
260
375
  id: 'routes',
261
376
  label: 'Routes',
262
- content: (_jsxs("div", { className: "routes-section", children: [_jsxs("div", { className: "routes-list", children: [_jsxs("div", { className: "route-headers", children: [_jsx("span", { children: "Path" }), _jsx("span", { children: "Component" }), _jsx("span", { children: "Title" }), _jsx("span", { children: "Description" }), _jsx("span", { children: "Actions" })] }), config.routes.map((route, index) => (_jsxs("div", { className: "route-item", children: [_jsx("input", { type: "text", placeholder: "Path", value: route.path, onChange: (e) => updateRoute(index, 'path', e.target.value) }), _jsx("input", { type: "text", placeholder: "Component", value: route.component, onChange: (e) => updateRoute(index, 'component', e.target.value) }), _jsx("input", { type: "text", placeholder: "Title", value: route.title || '', onChange: (e) => updateRoute(index, 'title', e.target.value) }), _jsx("input", { type: "text", placeholder: "Description", value: route.description || '', onChange: (e) => updateRoute(index, 'description', e.target.value) }), _jsx("button", { onClick: () => removeRoute(index), children: "Remove" })] }, index)))] }), _jsx("button", { onClick: addRoute, children: "Add Route" })] }))
377
+ content: (_jsx(FormValidationProvider, { children: _jsxs("div", { className: "routes-section", children: [_jsx("div", { className: "routes-list", children: config.routes.map((route, index) => (_jsxs("div", { className: "route-item", children: [routesForm.fields.map((field) => {
378
+ const Component = FC[field.component];
379
+ if (!Component)
380
+ return null;
381
+ let fieldValue = route[field.props.name];
382
+ if (field.props.name === 'keywords' && Array.isArray(fieldValue)) {
383
+ fieldValue = fieldValue.join(', ');
384
+ }
385
+ const fieldProps = {
386
+ ...field.props,
387
+ id: `${field.props.id}-${index}`,
388
+ ...(field.component === 'FormTextarea'
389
+ ? { defaultValue: fieldValue || '' }
390
+ : field.props.type === 'checkbox'
391
+ ? { checked: fieldValue || false }
392
+ : { value: fieldValue || '' }),
393
+ onChange: (e) => {
394
+ let value;
395
+ if (field.props.type === 'checkbox') {
396
+ value = e.target.checked;
397
+ }
398
+ else if (field.props.name === 'keywords') {
399
+ value = e.target.value.split(',').map((s) => s.trim()).filter((s) => s);
400
+ }
401
+ else {
402
+ value = e.target.value;
403
+ }
404
+ updateRoute(index, field.props.name, value);
405
+ }
406
+ };
407
+ return _jsx(Component, { ...fieldProps }, fieldProps.id);
408
+ }), _jsxs("div", { className: "route-buttons", children: [_jsxs("button", { onClick: () => {
409
+ console.log('AI Recommend button clicked for route:', index);
410
+ handleAiRecommendations(index);
411
+ }, className: "route-button ai-recommend", children: [_jsx("span", { className: "ai-icon", children: "\u2728" }), " Recommend"] }), _jsx("button", { onClick: () => removeRoute(index), className: "route-button remove", children: "Remove" })] })] }, index))) }), _jsx("button", { onClick: addRoute, children: "Add Route" })] }) }))
263
412
  },
264
413
  {
265
414
  id: 'visualdesign',
@@ -269,8 +418,25 @@ export function ConfigBuilder(props) {
269
418
  ], orientation: "top" }), _jsx("button", { onClick: handleSave, disabled: !isFormValid, className: isFormValid ? 'save-button-valid' : 'save-button-invalid', children: "Save Config" }), !isFormValid && (_jsx("div", { className: "validation-message", children: "Please fill in all required fields (marked with *) before saving." })), _jsx(Accordion, { items: [
270
419
  {
271
420
  title: 'Configuration Preview',
272
- content: _jsx("pre", { children: JSON.stringify(config, null, 2) })
421
+ content: _jsx("pre", { children: (() => {
422
+ try {
423
+ return JSON.stringify(config, null, 2);
424
+ }
425
+ catch (e) {
426
+ // Simple fallback that doesn't try to analyze the object deeply
427
+ const errorMessage = e instanceof Error ? e.message : String(e);
428
+ return `Configuration contains non-serializable data (functions, circular references, or DOM elements).\n\nError: ${errorMessage}\n\nTo debug, check the config object in browser dev tools for functions or complex objects.`;
429
+ }
430
+ })() })
273
431
  }
274
- ] })] }));
432
+ ] }), _jsx(Modal, { modalID: "ai-recommendations", isOpen: aiModalOpen, handleCloseEvent: () => setAiModalOpen(false), modalContent: _jsxs("div", { className: "ai-recommendations-modal", children: [_jsx("h3", { children: "AI SEO Recommendations" }), currentRouteIndex !== null && (_jsxs("p", { children: [_jsx("strong", { children: "Route:" }), " ", config.routes[currentRouteIndex].name || config.routes[currentRouteIndex].path] })), aiLoading ? (_jsxs("div", { className: "ai-loading", children: [_jsx("p", { children: "Generating AI recommendations..." }), _jsx("div", { className: "loading-spinner" })] })) : aiRecommendations?.error ? (_jsx("div", { className: "ai-error", children: _jsxs("p", { children: ["Error: ", aiRecommendations.error] }) })) : aiRecommendations ? (_jsxs("div", { className: "ai-recommendations", children: [_jsxs("div", { className: "recommendation-item", children: [_jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: acceptTitle, onChange: (e) => setAcceptTitle(e.target.checked) }), _jsx("strong", { children: "Title:" })] }), _jsxs("div", { className: "recommendation-content", children: [_jsx("div", { className: "current-value", children: _jsxs("small", { children: ["Current: ", config.routes[currentRouteIndex]?.title || 'None'] }) }), _jsx("div", { className: "suggested-value", children: aiRecommendations.title })] })] }), _jsxs("div", { className: "recommendation-item", children: [_jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: acceptKeywords, onChange: (e) => setAcceptKeywords(e.target.checked) }), _jsx("strong", { children: "Keywords:" })] }), _jsxs("div", { className: "recommendation-content", children: [_jsx("div", { className: "current-value", children: _jsxs("small", { children: ["Current: ", (() => {
433
+ const keywords = config.routes[currentRouteIndex]?.keywords;
434
+ if (Array.isArray(keywords)) {
435
+ return keywords.join(', ') || 'None';
436
+ }
437
+ else if (typeof keywords === 'string') {
438
+ return keywords || 'None';
439
+ }
440
+ return 'None';
441
+ })()] }) }), _jsx("div", { className: "suggested-value", children: aiRecommendations.keywords?.join(', ') })] })] }), _jsxs("div", { className: "recommendation-item", children: [_jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: acceptDescription, onChange: (e) => setAcceptDescription(e.target.checked) }), _jsx("strong", { children: "Description:" })] }), _jsxs("div", { className: "recommendation-content", children: [_jsx("div", { className: "current-value", children: _jsxs("small", { children: ["Current: ", config.routes[currentRouteIndex]?.description || 'None'] }) }), _jsx("div", { className: "suggested-value", children: aiRecommendations.description })] })] })] })) : null, _jsxs("div", { className: "modal-actions", children: [_jsx("button", { onClick: () => setAiModalOpen(false), children: "Cancel" }), _jsx("button", { onClick: handleAcceptAiRecommendations, disabled: !acceptTitle && !acceptKeywords && !acceptDescription, className: "accept-button", children: "Accept Selected" })] })] }) })] }));
275
442
  }
276
- ConfigBuilder.propTypes = ConfigBuilderPropTypes;
@@ -4,7 +4,18 @@ import PropTypes from 'prop-types';
4
4
  import { getFontOptions } from './google-fonts';
5
5
  import { WEB_SAFE_FONTS, GENERIC_FAMILIES } from './fonts';
6
6
  import './FontSelector.css';
7
- export function FontSelector({ id, name, label, fontType, required = false, placeholder, value = '', onChange }) {
7
+ FontSelector.propTypes = {
8
+ id: PropTypes.string.isRequired,
9
+ name: PropTypes.string.isRequired,
10
+ label: PropTypes.string.isRequired,
11
+ fontType: PropTypes.oneOf(['google', 'websafe', 'generic']).isRequired,
12
+ required: PropTypes.bool,
13
+ placeholder: PropTypes.string,
14
+ value: PropTypes.string,
15
+ onChange: PropTypes.func,
16
+ };
17
+ export function FontSelector(props) {
18
+ const { id, name, label, fontType, required = false, placeholder, value = '', onChange } = props;
8
19
  const [inputValue, setInputValue] = useState(value);
9
20
  const [googleFonts, setGoogleFonts] = useState([]);
10
21
  const [isLoading, setIsLoading] = useState(false);
@@ -67,7 +78,7 @@ export function FontSelector({ id, name, label, fontType, required = false, plac
67
78
  }
68
79
  return null;
69
80
  };
70
- return (_jsxs("div", { className: "font-selector-container", children: [_jsxs("label", { htmlFor: id, className: "font-selector-label", children: [label, required && _jsx("span", { className: "font-selector-required", children: "*" }), getTooltip() && (_jsx("span", { className: "font-selector-tooltip", title: getTooltip().replace(/\[([^\]]+)\]\([^)]+\)/, '$1'), children: "\uD83D\uDC41\uFE0F" }))] }), _jsxs("div", { className: "font-selector-input-container", children: [_jsx("input", { type: "text", id: id, name: name, value: inputValue, onChange: handleInputChange, onFocus: handleFocus, onBlur: handleBlur, placeholder: placeholder, required: required, autoComplete: "off", className: "font-selector-input" }), showDropdown && filteredOptions.length > 0 && (_jsx("div", { className: "font-selector-dropdown", children: isLoading ? (_jsx("div", { className: "font-selector-loading", children: "Loading fonts..." })) : (filteredOptions.map((option) => (_jsxs("div", { className: "font-selector-option", onClick: () => handleOptionSelect(option), onKeyDown: (e) => {
81
+ return (_jsxs("div", { className: "font-selector-container", children: [_jsxs("label", { htmlFor: id, className: "font-selector-label", children: [label, required && _jsx("span", { className: "font-selector-required", children: "*" }), getTooltip() && (_jsx("span", { className: "font-selector-tooltip", title: getTooltip().replace(/\[([^\]]+)\]\([^)]+\)/, '$1'), children: "\uD83D\uDC41\uFE0F" }))] }), _jsxs("div", { className: "font-selector-input-container", children: [_jsx("input", { type: "text", id: id, name: name, value: inputValue ?? '', onChange: handleInputChange, onFocus: handleFocus, onBlur: handleBlur, placeholder: placeholder ?? undefined, required: required ?? false, autoComplete: "off", className: "font-selector-input" }), showDropdown && filteredOptions.length > 0 && (_jsx("div", { className: "font-selector-dropdown", children: isLoading ? (_jsx("div", { className: "font-selector-loading", children: "Loading fonts..." })) : (filteredOptions.map((option) => (_jsxs("div", { className: "font-selector-option", onClick: () => handleOptionSelect(option), onKeyDown: (e) => {
71
82
  if (e.key === 'Enter' || e.key === ' ') {
72
83
  e.preventDefault();
73
84
  handleOptionSelect(option);
@@ -0,0 +1,67 @@
1
+ {
2
+ "fields": [
3
+ {
4
+ "component": "FormInput",
5
+ "props": {
6
+ "type": "text",
7
+ "id": "name",
8
+ "name": "name",
9
+ "label": "Route Name",
10
+ "required": true,
11
+ "placeholder": "e.g., Home Page",
12
+ "size": "40"
13
+ }
14
+ },
15
+ {
16
+ "component": "FormInput",
17
+ "props": {
18
+ "type": "text",
19
+ "id": "path",
20
+ "name": "path",
21
+ "label": "Path",
22
+ "required": true,
23
+ "placeholder": "e.g., /",
24
+ "size": "40"
25
+ }
26
+ },
27
+ {
28
+ "component": "FormInput",
29
+ "props": {
30
+ "type": "text",
31
+ "id": "title",
32
+ "name": "title",
33
+ "label": "Title",
34
+ "placeholder": "Page title for SEO",
35
+ "size": "40"
36
+ }
37
+ },
38
+ {
39
+ "component": "FormTextarea",
40
+ "props": {
41
+ "id": "description",
42
+ "name": "description",
43
+ "label": "Description",
44
+ "placeholder": "Meta description for SEO"
45
+ }
46
+ },
47
+ {
48
+ "component": "FormTextarea",
49
+ "props": {
50
+ "id": "keywords",
51
+ "name": "keywords",
52
+ "label": "Keywords",
53
+ "placeholder": "Comma-separated keywords for SEO",
54
+ "cols": "40"
55
+ }
56
+ },
57
+ {
58
+ "component": "FormInput",
59
+ "props": {
60
+ "type": "checkbox",
61
+ "id": "hidden",
62
+ "name": "hidden",
63
+ "label": "Hidden Route"
64
+ }
65
+ }
66
+ ]
67
+ }