@svgrid/enterprise 2.3.2 → 2.5.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 (165) hide show
  1. package/dist/cdn/svgrid-enterprise.svelte-external.js +5917 -5545
  2. package/dist/designer/assets/index-oYok52zd.css +1 -0
  3. package/dist/designer/assets/{index-DVrv9dH6.js → index-qL7ir9C7.js} +89 -89
  4. package/dist/designer/assets/src-96d31O63.js +2873 -0
  5. package/dist/designer/assets/src-D-O2F805.css +1 -0
  6. package/dist/designer/index.html +6 -6
  7. package/dist/node/studio.js +681 -84
  8. package/package.json +3 -3
  9. package/src/SvGridEditPanel.dom.test.ts +174 -0
  10. package/src/SvGridEditPanel.svelte +209 -46
  11. package/src/edit-panel.test.ts +145 -36
  12. package/src/edit-panel.ts +167 -16
  13. package/src/expressions/evaluate.ts +1 -1
  14. package/src/expressions/parse.ts +3 -3
  15. package/src/index.ts +8 -0
  16. package/src/license-core.ts +77 -5
  17. package/src/license-expiry.test.ts +91 -0
  18. package/src/license.ts +31 -2
  19. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/_svelte_metadata.json +1 -0
  20. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/_metadata.json +178 -0
  21. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-ZrPsKxob.js +3875 -0
  22. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-ZrPsKxob.js.map +1 -0
  23. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-ACqvWGE2.js +21 -0
  24. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-ACqvWGE2.js.map +1 -0
  25. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-D3w5jt92.js +1938 -0
  26. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-D3w5jt92.js.map +1 -0
  27. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-CC2V3H0y.js +205 -0
  28. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-CC2V3H0y.js.map +1 -0
  29. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-BXP37tAa.js +18 -0
  30. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-BXP37tAa.js.map +1 -0
  31. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-DwubnU5b.js +264 -0
  32. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-DwubnU5b.js.map +1 -0
  33. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-hD7f6PUz.js +697 -0
  34. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-hD7f6PUz.js.map +1 -0
  35. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/package.json +3 -0
  36. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js +134 -0
  37. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js.map +1 -0
  38. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js +55 -0
  39. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js.map +1 -0
  40. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_attachments.js +2 -0
  41. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js +46843 -0
  42. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js.map +1 -0
  43. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js +237 -0
  44. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js.map +1 -0
  45. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_events.js +2 -0
  46. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js +4 -0
  47. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js.map +1 -0
  48. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_client.js +7 -0
  49. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js +4 -0
  50. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js.map +1 -0
  51. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js +5 -0
  52. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js.map +1 -0
  53. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js +5 -0
  54. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js.map +1 -0
  55. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js +5 -0
  56. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js.map +1 -0
  57. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_server.js +6 -0
  58. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js +83 -0
  59. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js.map +1 -0
  60. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js +559 -0
  61. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js.map +1 -0
  62. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js +24 -0
  63. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js.map +1 -0
  64. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js +122 -0
  65. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js.map +1 -0
  66. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_server.js +2 -0
  67. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js +89 -0
  68. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js.map +1 -0
  69. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js +241 -0
  70. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js.map +1 -0
  71. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DBx1SCN6.js +4938 -0
  72. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DBx1SCN6.js.map +1 -0
  73. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-gjfpSTFS.js +495 -0
  74. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-gjfpSTFS.js.map +1 -0
  75. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-DKoOKg8S.js +190 -0
  76. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-DKoOKg8S.js.map +1 -0
  77. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-C0MiR7eR.js +8 -0
  78. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-C0MiR7eR.js.map +1 -0
  79. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-B6Y7P0Q-.js +138 -0
  80. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-B6Y7P0Q-.js.map +1 -0
  81. package/src/schema.ts +63 -0
  82. package/src/studio/emit-project.test.ts +191 -0
  83. package/src/studio/emit-project.ts +237 -27
  84. package/src/studio/emit-schema.ts +4 -1
  85. package/src/studio/index.ts +2 -0
  86. package/src/studio/init-flow.test.ts +150 -0
  87. package/src/studio/init-flow.ts +169 -22
  88. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/_svelte_metadata.json +1 -0
  89. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/_metadata.json +178 -0
  90. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-BOv1rBKp.js +3875 -0
  91. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-BOv1rBKp.js.map +1 -0
  92. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-B9vgIdCd.js +21 -0
  93. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-B9vgIdCd.js.map +1 -0
  94. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-T1qbW_qD.js +1938 -0
  95. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-T1qbW_qD.js.map +1 -0
  96. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-Bo11SfVK.js +205 -0
  97. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-Bo11SfVK.js.map +1 -0
  98. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-X7D4vfrZ.js +18 -0
  99. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-X7D4vfrZ.js.map +1 -0
  100. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-oyUje5i1.js +264 -0
  101. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-oyUje5i1.js.map +1 -0
  102. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-DeSQgSFm.js +697 -0
  103. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-DeSQgSFm.js.map +1 -0
  104. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/package.json +3 -0
  105. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js +134 -0
  106. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js.map +1 -0
  107. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js +55 -0
  108. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js.map +1 -0
  109. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_attachments.js +2 -0
  110. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js +46843 -0
  111. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js.map +1 -0
  112. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js +237 -0
  113. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js.map +1 -0
  114. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_events.js +2 -0
  115. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js +4 -0
  116. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js.map +1 -0
  117. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_client.js +7 -0
  118. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js +4 -0
  119. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js.map +1 -0
  120. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js +5 -0
  121. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js.map +1 -0
  122. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js +5 -0
  123. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js.map +1 -0
  124. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js +5 -0
  125. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js.map +1 -0
  126. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_server.js +6 -0
  127. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js +83 -0
  128. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js.map +1 -0
  129. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js +559 -0
  130. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js.map +1 -0
  131. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js +24 -0
  132. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js.map +1 -0
  133. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js +122 -0
  134. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js.map +1 -0
  135. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_server.js +2 -0
  136. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js +89 -0
  137. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js.map +1 -0
  138. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js +241 -0
  139. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js.map +1 -0
  140. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-BbWyXNk1.js +4938 -0
  141. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-BbWyXNk1.js.map +1 -0
  142. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DI_fC7vt.js +495 -0
  143. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DI_fC7vt.js.map +1 -0
  144. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-Dyr3lQv9.js +190 -0
  145. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-Dyr3lQv9.js.map +1 -0
  146. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-_9FHH-LF.js +8 -0
  147. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-_9FHH-LF.js.map +1 -0
  148. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-CUbSPnyP.js +138 -0
  149. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-CUbSPnyP.js.map +1 -0
  150. package/src/studio/project.test.ts +30 -0
  151. package/src/studio/project.ts +72 -8
  152. package/src/studio/samples/crm.ts +11 -2
  153. package/src/studio/samples/insurance.ts +10 -4
  154. package/src/studio/samples/samples.test.ts +36 -0
  155. package/src/studio/samples/shared.ts +14 -0
  156. package/src/studio/samples/support.ts +12 -5
  157. package/src/studio/screen-suites.test.ts +43 -0
  158. package/src/studio/screen-suites.ts +5 -2
  159. package/src/studio/user-error.ts +31 -0
  160. package/src/upgrade-prompt.ts +23 -10
  161. package/src/version.test.ts +17 -0
  162. package/src/version.ts +13 -0
  163. package/dist/designer/assets/index-DjPp1mn7.css +0 -1
  164. package/dist/designer/assets/src-BRKu53tV.js +0 -2842
  165. package/dist/designer/assets/src-DseB1VRN.css +0 -1
@@ -17,6 +17,8 @@ import { blockColumns, blockStyleCss, blockClassName, sanitizeClassName, compone
17
17
  import { uiComponentSpec, gridApiSettableProps, STANDARD_UI_EVENTS } from './ui-components.js'
18
18
  import { resolveThemeTokens, resolveThemeTokensFor, isDarkTheme } from './themes.js'
19
19
  import type { EntityField, EntitySchema } from '../schema.js'
20
+ import { hasFieldConditions } from '../edit-panel.js'
21
+ import { SVGRID_VERSION } from '../version.js'
20
22
  import { emitEntityModules, homeFile, layoutFile, lookupVar, namesFor, prepareEntities, relationDisplayFields, sqlDdlFiles, type NavItem } from './emit-schema.js'
21
23
 
22
24
  const has = (blocks: Block[], kind: Block['config']['kind']) => blocks.some((b) => b.config.kind === kind)
@@ -2249,7 +2251,10 @@ function screenChildEntities(blocks: Block[], resolve: (name: string) => EntityS
2249
2251
  /** `+page.server.ts` for a read-only SSR screen (data-viz / detail / master-
2250
2252
  * detail): a `load` that returns the full dataset (+ each child collection) -
2251
2253
  * the page renders real SSR HTML from `data.*`. No actions (read-only). */
2252
- function ssrReadServerFile(schema: EntitySchema, screen: Screen, sourceKind: 'memory' | 'sql', accessEnabled: boolean, screenIds: string[], resolve: (name: string) => EntitySchema | undefined): GeneratedFile {
2254
+ /** Sources a screen can render on the server. See `ssrScreenShape`. */
2255
+ type SsrSourceKind = 'memory' | 'sql' | 'rest'
2256
+
2257
+ function ssrReadServerFile(schema: EntitySchema, screen: Screen, sourceKind: SsrSourceKind, accessEnabled: boolean, screenIds: string[], resolve: (name: string) => EntitySchema | undefined): GeneratedFile {
2253
2258
  const n = namesFor(schema)
2254
2259
  const isSql = sourceKind === 'sql'
2255
2260
  const children = screenChildEntities(screen.blocks, resolve)
@@ -2264,8 +2269,8 @@ function ssrReadServerFile(schema: EntitySchema, screen: Screen, sourceKind: 'me
2264
2269
  contents: `import type { PageServerLoad } from './$types'
2265
2270
  import type { ServerRequest } from '@svgrid/grid'
2266
2271
  ${isSql ? "import { createKitDataSource } from '@svgrid/enterprise'\n" : ''}${rbac ? "import { error } from '@sveltejs/kit'\nimport { authorizeAction, getServerRole } from '$lib/access'\n" : ''}${memImports.length ? `import { ${memImports.join(', ')} } from '$lib/data'\n` : ''}
2267
- // The app shell is a client SPA (+layout.ts ssr=false); this screen opts back
2268
- // INTO server rendering - real SSR HTML, not just a server-side load.
2272
+ // This screen renders on the server: real SSR HTML, not just a server-side
2273
+ // load. Stated explicitly so it holds whichever way the root layout is set.
2269
2274
  export const ssr = true
2270
2275
  ${rbac ? `\nconst SCREEN_IDS = ${JSON.stringify(screenIds)}\n` : ''}
2271
2276
  const PAGE: ServerRequest = { startRow: 0, endRow: 1000, pageIndex: 0, pageSize: 1000, sortModel: [], filterModel: {} }
@@ -2325,26 +2330,61 @@ function ssrQueryHelperFile(): GeneratedFile {
2325
2330
  }
2326
2331
 
2327
2332
  const htmlEsc = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
2333
+ /** Escape a value going inside a double-quoted HTML attribute. */
2334
+ const attrEsc = (s: string) => htmlEsc(s).replace(/"/g, '&quot;')
2335
+
2336
+ /**
2337
+ * The field's own constraints as native HTML attributes, so a server-rendered
2338
+ * form validates in the browser too. The server still re-checks every one of
2339
+ * them: these only save a round-trip, they never decide anything.
2340
+ */
2341
+ function attrs(f: EntityField): string {
2342
+ const out: string[] = []
2343
+ if (f.input?.placeholder) out.push(`placeholder="${attrEsc(f.input.placeholder)}"`)
2344
+ if (f.type === 'number') {
2345
+ if (f.min != null) out.push(`min="${f.min}"`)
2346
+ if (f.max != null) out.push(`max="${f.max}"`)
2347
+ if (f.input?.step != null) out.push(`step="${f.input.step}"`)
2348
+ } else {
2349
+ if (f.minLength != null) out.push(`minlength="${f.minLength}"`)
2350
+ if (f.maxLength != null) out.push(`maxlength="${f.maxLength}"`)
2351
+ // `format` already picks the input type, which brings its own checking.
2352
+ if (f.pattern && !f.format) out.push(`pattern="${attrEsc(f.pattern)}"`)
2353
+ }
2354
+ return out.length ? ' ' + out.join(' ') : ''
2355
+ }
2328
2356
 
2329
2357
  /** The `+page.server.ts` + SSR `+page.svelte` for a single-grid CRUD screen. */
2330
- function emitSsrGridScreen(schema: EntitySchema, screen: Screen, sourceKind: 'memory' | 'sql', accessEnabled: boolean, screenIds: string[], byName: Map<string, EntitySchema>): GeneratedFile[] {
2358
+ function emitSsrGridScreen(schema: EntitySchema, screen: Screen, sourceKind: SsrSourceKind, accessEnabled: boolean, screenIds: string[], byName: Map<string, EntitySchema>): GeneratedFile[] {
2331
2359
  const n = namesFor(schema)
2332
2360
  const isSql = sourceKind === 'sql'
2361
+ // REST reads like the memory path - the source is imported from $lib/data and
2362
+ // called directly - but the remote API owns the ids, so a create posts the
2363
+ // values alone, as sql does.
2364
+ const isRest = sourceKind === 'rest'
2333
2365
  const relSchemaOf = (f: EntityField) => byName.get(f.relation!.entity)!
2334
2366
  const relIdOf = (rs: EntitySchema) => rs.idField ?? rs.fields.find((x) => x.primaryKey)?.field ?? 'id'
2335
- const grid = screen.blocks[0]!.config as GridConfig
2367
+ const grid = screen.blocks.find((b) => b.config.kind === 'grid')!.config as GridConfig
2368
+ const facetPanel = screen.blocks.find((b) => b.config.kind === 'filter')?.config as FilterPanelConfig | undefined
2336
2369
  const pageSize = grid.pageSize && grid.pageSize > 0 ? grid.pageSize : 25
2337
2370
  const wantsFilter = grid.filterable !== false
2338
2371
  const idField = schema.idField ?? schema.fields.find((f) => f.primaryKey)?.field ?? 'id'
2339
2372
  const isFormHidden = (f: EntityField) => f.hidden === true || (typeof f.hidden === 'object' && !!f.hidden.form)
2340
- const formFields = schema.fields.filter(
2373
+ const editable = schema.fields.filter(
2341
2374
  (f) => f.field !== idField && !f.primaryKey && !f.readonly && !f.computed && !f.formula && !isFormHidden(f),
2342
2375
  )
2376
+ // The grid block's form settings win over the schema's, matching how the edit
2377
+ // panel resolves them - so a layout arranged in the designer renders the same
2378
+ // here instead of the server-rendered form quietly ignoring it.
2379
+ const formFields = grid.formFields?.length
2380
+ ? (grid.formFields.map((name) => editable.find((f) => f.field === name)).filter(Boolean) as EntityField[])
2381
+ : editable
2343
2382
  const normType = (t: string): 'text' | 'number' | 'boolean' => (t === 'number' ? 'number' : t === 'boolean' ? 'boolean' : 'text')
2344
2383
  const fieldTypesLit = `{ ${formFields.map((f) => `${jsStr(f.field)}: ${jsStr(normType(f.type))}`).join(', ')} }`
2345
2384
 
2346
- // Relation fields render as a native <select> whose options are prefetched in load
2347
- // (memory path only for now; sql relation fields stay a text FK - follow-up).
2385
+ // Relation fields render as a native <select> whose options are prefetched in
2386
+ // load - from the in-process source on memory, or the related entity's /api
2387
+ // route on sql (see relSource below).
2348
2388
  const isRel = (f: EntityField) => f.type === 'relation' && !!f.relation && byName.has(f.relation!.entity)
2349
2389
  const relFields = formFields.filter(isRel)
2350
2390
  // The $lib/data import only needs the related sources on the memory path; on sql
@@ -2371,14 +2411,25 @@ function emitSsrGridScreen(schema: EntitySchema, screen: Screen, sourceKind: 'me
2371
2411
  // once, in that route's createKitHandlers - not duplicated here.
2372
2412
  const src = isSql ? 'source(fetch)' : n.sourceVar
2373
2413
  const fetchArg = isSql ? ', fetch' : ''
2374
- const enterpriseImports = isSql ? 'validateAll, createKitDataSource' : 'validateAll'
2375
- const sourceImport = isSql ? '' : `import { ${[...new Set([n.sourceVar, ...relSourceVars]), 'nextId'].join(', ')} } from '$lib/data'\n`
2414
+ // A schema with value-driven fields posts through `stripHiddenValues`, so a
2415
+ // field the form was hiding is never written from a submitted FormData - the
2416
+ // same fields the client panel would have sent.
2417
+ const conditional = hasFieldConditions(schema)
2418
+ const readValues = (fd: string) => (conditional ? `stripHiddenValues(${n.schemaVar}, formToValues(${fd}))` : `formToValues(${fd})`)
2419
+ const enterpriseImports = [
2420
+ 'validateAll',
2421
+ ...(conditional ? ['stripHiddenValues'] : []),
2422
+ ...(isSql ? ['createKitDataSource'] : []),
2423
+ ].join(', ')
2424
+ const sourceImport = isSql
2425
+ ? ''
2426
+ : `import { ${[...new Set([n.sourceVar, ...relSourceVars]), ...(isRest ? [] : ['nextId'])].join(', ')} } from '$lib/data'\n`
2376
2427
  const schemaImport = `import { ${n.schemaVar}${isSql ? `, type ${n.type}` : ''} } from '$lib/schemas'`
2377
- const idConst = isSql ? '' : `\nconst ID_FIELD = ${jsStr(idField)}`
2428
+ const idConst = isSql || isRest ? '' : `\nconst ID_FIELD = ${jsStr(idField)}`
2378
2429
  const srcHelper = isSql
2379
2430
  ? `\n// Same-origin client over the connected /api/${n.route} route (that route runs\n// validation, RBAC, triggers + audit via createKitHandlers).\nconst source = (fetch: typeof globalThis.fetch) => createKitDataSource<${n.type}>({ endpoint: ${jsStr('/api/' + n.route)}, fetch })\n`
2380
2431
  : ''
2381
- const createCall = isSql
2432
+ const createCall = isSql || isRest
2382
2433
  ? `await ${src}.createRow(values)`
2383
2434
  : `await ${n.sourceVar}.createRow({ [ID_FIELD]: nextId(${jsStr(n.idPrefix)}), ...values })`
2384
2435
 
@@ -2395,8 +2446,8 @@ import { ${enterpriseImports} } from '@svgrid/enterprise'
2395
2446
  ${rbac ? "import { authorizeAction, getServerRole } from '$lib/access'\n" : ''}${sourceImport}${schemaImport}
2396
2447
  import { planFromSearchParams } from '$lib/server/query'
2397
2448
 
2398
- // The app shell is a client SPA (+layout.ts ssr=false); this screen opts back
2399
- // INTO server rendering - real SSR HTML, not just a server-side load.
2449
+ // This screen renders on the server: real SSR HTML, not just a server-side
2450
+ // load. Stated explicitly so it holds whichever way the root layout is set.
2400
2451
  export const ssr = true
2401
2452
  ${idConst}${rbac ? `\nconst SCREEN_IDS = ${JSON.stringify(screenIds)}` : ''}
2402
2453
  const FIELD_TYPES: Record<string, 'text' | 'number' | 'boolean'> = ${fieldTypesLit}
@@ -2422,7 +2473,7 @@ ${relPrefetch ? relPrefetch + '\n' : ''} return { rows, total: rowCount, page:
2422
2473
 
2423
2474
  export const actions: Actions = {
2424
2475
  create: async ({ request${fetchArg}${localsArg} }) => {
2425
- ${writeGuard('create')} const values = formToValues(await request.formData())
2476
+ ${writeGuard('create')} const values = ${readValues('await request.formData()')}
2426
2477
  const errors = await validateAll(${n.schemaVar}, values)
2427
2478
  if (Object.keys(errors).length) return fail(422, { errors, values })
2428
2479
  ${createCall}
@@ -2431,7 +2482,7 @@ ${writeGuard('create')} const values = formToValues(await request.formData())
2431
2482
  update: async ({ request${fetchArg}${localsArg} }) => {
2432
2483
  const fd = await request.formData()
2433
2484
  ${writeGuard('update')} const id = String(fd.get('__id') ?? '')
2434
- const values = formToValues(fd)
2485
+ const values = ${readValues('fd')}
2435
2486
  const errors = await validateAll(${n.schemaVar}, values)
2436
2487
  if (Object.keys(errors).length) return fail(422, { errors, values })
2437
2488
  await ${src}.updateRow(id, values)
@@ -2445,6 +2496,78 @@ ${writeGuard('delete')} const fd = await request.formData()
2445
2496
  }
2446
2497
  `
2447
2498
 
2499
+ // The facet panel becomes a plain GET form: each control is named for the
2500
+ // param `planFromSearchParams` reads, so submitting navigates to a filtered
2501
+ // URL and the server load does the rest. No JavaScript required, and the
2502
+ // resulting URL is shareable. A GET submit drops `page`, which is what we want
2503
+ // (filtering returns you to the first page); `sort` is carried in a hidden
2504
+ // input so it survives.
2505
+ // Which control types the panel actually renders, so the stylesheet below can
2506
+ // skip rules for controls this screen has none of (svelte-check flags an unused
2507
+ // selector, and a generated app should not ship warnings).
2508
+ const facetControls = { select: false, input: false }
2509
+ const facetForm = (() => {
2510
+ if (!facetPanel) return ''
2511
+ const fields = filterFieldsOf(schema, facetPanel)
2512
+ if (!fields.length) return ''
2513
+ // Parenthesised: `===` binds tighter than `??`, so an unwrapped
2514
+ // `get(x) ?? '' === ''` would compare the empty strings and hand `selected`
2515
+ // the raw param value - marking every option selected at once.
2516
+ const cur = (f: EntityField) => `(page.url.searchParams.get(${jsStr('f_' + f.field)}) ?? '')`
2517
+ // `selected` per option rather than `value` on the <select>: the value form
2518
+ // is applied client-side, so a server-rendered page would show "Any" while
2519
+ // the URL said otherwise.
2520
+ const optionList = (f: EntityField, opts: { value: string; label: string }[]) =>
2521
+ [`<option value="" selected={${cur(f)} === ''}>Any</option>`]
2522
+ .concat(opts.map((o) => `<option value=${jsStr(o.value)} selected={${cur(f)} === ${jsStr(o.value)}}>${htmlEsc(o.label)}</option>`))
2523
+ .join('\n ')
2524
+ const controls = fields
2525
+ .map((f) => {
2526
+ const key = jsStr('f_' + f.field)
2527
+ const opts = enumOpts(f)
2528
+ let control: string
2529
+ if (opts.length) {
2530
+ facetControls.select = true
2531
+ control = `<select name=${key}>\n ${optionList(f, opts)}\n </select>`
2532
+ } else if (f.type === 'boolean') {
2533
+ facetControls.select = true
2534
+ control = `<select name=${key}>\n ${optionList(f, [{ value: 'true', label: 'Yes' }, { value: 'false', label: 'No' }])}\n </select>`
2535
+ } else {
2536
+ facetControls.input = true
2537
+ control = `<input name=${key} value={${cur(f)}} placeholder="Any" />`
2538
+ }
2539
+ return ` <label class="sk-facet">\n <span>${htmlEsc(f.label ?? f.field)}</span>\n ${control}\n </label>`
2540
+ })
2541
+ .join('\n')
2542
+ return `
2543
+ <form method="GET" class="sk-facets">
2544
+ <input type="hidden" name="sort" value={page.url.searchParams.get('sort') ?? ''} />
2545
+ ${controls}
2546
+ <div class="sk-facet-actions">
2547
+ <button type="submit">Filter</button>
2548
+ <a href={page.url.pathname}>Clear</a>
2549
+ </div>
2550
+ </form>
2551
+ `
2552
+ })()
2553
+
2554
+ const facetCss = !facetForm
2555
+ ? ''
2556
+ : [
2557
+ ' .sk-facets { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px; margin-bottom: 14px; padding: 12px 14px; border: 1px solid var(--sg-border, #cbd5e1); border-radius: 10px; background: var(--sg-surface, #f8fafc); }',
2558
+ ' .sk-facet { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }',
2559
+ ' .sk-facet span { color: var(--sg-muted, #64748b); }',
2560
+ ...(facetControls.select && facetControls.input
2561
+ ? [' .sk-facet input, .sk-facet select { font: inherit; padding: 6px 9px; border-radius: 8px; border: 1px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); }']
2562
+ : facetControls.select
2563
+ ? [' .sk-facet select { font: inherit; padding: 6px 9px; border-radius: 8px; border: 1px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); }']
2564
+ : [' .sk-facet input { font: inherit; padding: 6px 9px; border-radius: 8px; border: 1px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); }']),
2565
+ ' .sk-facet-actions { display: flex; align-items: center; gap: 10px; margin-left: auto; }',
2566
+ ' .sk-facet-actions button { font: inherit; padding: 7px 14px; border-radius: 8px; border: 1px solid var(--sg-accent, #4f46e5); background: var(--sg-accent, #4f46e5); color: #fff; cursor: pointer; }',
2567
+ ' .sk-facet-actions a { font-size: 13px; color: var(--sg-muted, #64748b); }',
2568
+ '',
2569
+ ].join('\n')
2570
+
2448
2571
  const row = `(editing as Record<string, unknown>)`
2449
2572
  const fieldBlocks = formFields
2450
2573
  .map((f) => {
@@ -2461,17 +2584,57 @@ ${writeGuard('delete')} const fd = await request.formData()
2461
2584
  } else if (isRel(f)) {
2462
2585
  // Relation: options prefetched in load (data.<field>Options), current FK selected.
2463
2586
  input = `<select name=${key}${req}>\n <option value="">-</option>\n {#each data.${f.field}Options as o (o.value)}<option value={o.value} selected={!isCreate && String(${row}[${key}] ?? '') === o.value}>{o.label}</option>{/each}\n </select>`
2587
+ } else if (f.input?.editorType === 'textarea' || f.type === 'json') {
2588
+ input = `<textarea name=${key}${req}${attrs(f)}>{isCreate ? '' : (${row}[${key}] ?? '')}</textarea>`
2464
2589
  } else {
2465
- const t = normType(f.type) === 'number' ? 'number' : f.type === 'date' || f.type === 'dateString' ? 'date' : 'text'
2466
- input = `<input type="${t}" name=${key} value={isCreate ? '' : (${row}[${key}] ?? '')}${req} />`
2590
+ // The browser enforces the same constraints the server will, so an
2591
+ // obvious mistake is caught before a round-trip - and still caught after
2592
+ // one, because `validateAll` runs in the action regardless.
2593
+ const t = f.format === 'email' ? 'email'
2594
+ : f.format === 'url' ? 'url'
2595
+ : normType(f.type) === 'number' ? 'number'
2596
+ : f.type === 'date' || f.type === 'dateString' ? 'date'
2597
+ : 'text'
2598
+ input = `<input type="${t}" name=${key} value={isCreate ? '' : (${row}[${key}] ?? '')}${req}${attrs(f)} />`
2467
2599
  }
2468
- return ` <label class="sk-field">
2600
+ const hint = f.input?.help
2601
+ return ` <label class="sk-field${f.input?.span === 2 ? ' sk-field--wide' : ''}">
2469
2602
  <span>${htmlEsc(f.label ?? f.field)}${f.required ? ' *' : ''}</span>
2470
2603
  ${input}
2471
- {#if form?.errors?.[${key}]}<em class="sk-err">{form.errors[${key}]}</em>{/if}
2604
+ ${hint ? ` {#if !form?.errors?.[${key}]}<small class="sk-hint">${htmlEsc(hint)}</small>{/if}\n` : ''} {#if form?.errors?.[${key}]}<em class="sk-err">{form.errors[${key}]}</em>{/if}
2472
2605
  </label>`
2473
2606
  })
2474
- .join('\n')
2607
+
2608
+ // Group the inputs the way the schema's form layout says, so the server-
2609
+ // rendered form matches the one the edit panel draws instead of being a flat
2610
+ // fallback. Fields outside every section keep rendering, in a trailing group.
2611
+ const blockFor = (name: string) => {
2612
+ const i = formFields.findIndex((f) => f.field === name)
2613
+ return i === -1 ? null : fieldBlocks[i]!
2614
+ }
2615
+ const layout = {
2616
+ columns: grid.formColumns ?? schema.form?.columns,
2617
+ sections: grid.formSections?.length ? grid.formSections : schema.form?.sections,
2618
+ }
2619
+ const ssrSections = layout?.sections?.length
2620
+ ? (() => {
2621
+ const assigned = new Set<string>()
2622
+ const groups = layout.sections!.map((s) => {
2623
+ const items = s.fields.map((name) => { assigned.add(name); return blockFor(name) }).filter(Boolean) as string[]
2624
+ return { title: s.title, description: s.description, columns: s.columns, items }
2625
+ }).filter((g) => g.items.length)
2626
+ const rest = formFields.filter((f) => !assigned.has(f.field)).map((f) => blockFor(f.field)!).filter(Boolean)
2627
+ return rest.length ? [...groups, { title: undefined, description: undefined, columns: undefined, items: rest }] : groups
2628
+ })()
2629
+ : null
2630
+ const formCols = layout?.columns ?? 1
2631
+ const fieldsMarkup = ssrSections
2632
+ ? ssrSections
2633
+ .map((g) => ` <fieldset class="sk-group" style="--sk-cols: ${g.columns ?? formCols}">
2634
+ ${g.title ? ` <legend>${htmlEsc(g.title)}</legend>\n` : ''}${g.description ? ` <p class="sk-group__desc">${htmlEsc(g.description)}</p>\n` : ''}${g.items.join('\n')}
2635
+ </fieldset>`)
2636
+ .join('\n')
2637
+ : ` <div class="sk-group" style="--sk-cols: ${formCols}">\n${fieldBlocks.join('\n')}\n </div>`
2475
2638
 
2476
2639
  const page = `<script lang="ts">
2477
2640
  import { SvGrid, renderSnippet, type ColumnDef, type CellContext } from '@svgrid/grid'
@@ -2541,7 +2704,7 @@ ${wantsFilter ? `
2541
2704
  <h1>{TITLE}</h1>
2542
2705
  <button type="button" class="sk-btn sk-btn--primary" onclick={() => (editing = 'create')}>{NEW_LABEL}</button>
2543
2706
  </header>
2544
-
2707
+ ${facetForm}
2545
2708
  <SvGrid
2546
2709
  data={data.rows}
2547
2710
  {columns}
@@ -2560,7 +2723,7 @@ ${wantsFilter ? `
2560
2723
  <form method="POST" action={isCreate ? '?/create' : '?/update'} class="sk-form" use:enhance={onSubmit}>
2561
2724
  <h2>{isCreate ? NEW_LABEL : 'Edit ${htmlEsc(n.label)}'}</h2>
2562
2725
  {#if !isCreate}<input type="hidden" name="__id" value={${row}[ID_FIELD] as string} />{/if}
2563
- ${fieldBlocks}
2726
+ ${fieldsMarkup}
2564
2727
  <div class="sk-form__actions">
2565
2728
  <button type="button" class="sk-btn" onclick={() => (editing = null)}>Cancel</button>
2566
2729
  <button type="submit" class="sk-btn sk-btn--primary">Save</button>
@@ -2574,12 +2737,19 @@ ${fieldBlocks}
2574
2737
  .sk-head h1 { margin: 0; font-size: 20px; }
2575
2738
  .sk-btn { font: inherit; padding: 7px 14px; border-radius: 8px; border: 1px solid var(--sg-border, #cbd5e1); background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a); cursor: pointer; }
2576
2739
  .sk-btn--primary { background: var(--sg-accent, #4f46e5); border-color: var(--sg-accent, #4f46e5); color: #fff; }
2577
- .sk-rowact { display: flex; gap: 10px; }
2740
+ ${facetCss} .sk-rowact { display: flex; gap: 10px; }
2578
2741
  .sk-link { background: none; border: none; padding: 0; font: inherit; color: var(--sg-accent, #4f46e5); cursor: pointer; }
2579
2742
  .sk-danger { color: #dc2626; }
2580
2743
  .sk-overlay { position: fixed; inset: 0; background: rgba(15, 23, 42, 0.4); display: grid; place-items: center; z-index: 50; }
2581
2744
  .sk-form { width: min(480px, 92vw); max-height: 90vh; overflow: auto; background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a); border-radius: 12px; padding: 22px; display: flex; flex-direction: column; gap: 12px; box-shadow: 0 24px 60px -20px rgba(15, 23, 42, 0.5); }
2582
2745
  .sk-form h2 { margin: 0 0 4px; font-size: 16px; }
2746
+ .sk-group { display: grid; grid-template-columns: repeat(var(--sk-cols, 1), minmax(0, 1fr)); gap: 12px; border: 0; padding: 0; margin: 0; min-width: 0; }
2747
+ .sk-group + .sk-group { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--sg-border, #e2e8f0); }
2748
+ .sk-group legend { grid-column: 1 / -1; padding: 0; font-size: 12px; font-weight: 700; letter-spacing: 0.02em; text-transform: uppercase; color: var(--sg-muted, #64748b); }
2749
+ .sk-group__desc { grid-column: 1 / -1; margin: 0; font-size: 12px; color: var(--sg-muted, #64748b); }
2750
+ .sk-field--wide { grid-column: 1 / -1; }
2751
+ .sk-hint { color: var(--sg-muted, #64748b); font-size: 11.5px; }
2752
+ @media (max-width: 560px) { .sk-group { grid-template-columns: 1fr; } }
2583
2753
  .sk-field { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }
2584
2754
  .sk-field span { color: var(--sg-muted, #64748b); }
2585
2755
  .sk-field input, .sk-field select { font: inherit; padding: 7px 9px; border-radius: 8px; border: 1px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); }
@@ -2669,7 +2839,8 @@ export function emitStudioProject(project: StudioProject): GeneratedFile[] {
2669
2839
  // driven sort/filter/page; a read-only screen (data-viz / detail) gets a
2670
2840
  // load-only server file, and the SAME page markup renders from data.*.
2671
2841
  if (isSsrScreen(project, screen)) {
2672
- const srcKind = sources[screen.entity]?.kind === 'sql' ? 'sql' : 'memory'
2842
+ const kind = sources[screen.entity]?.kind
2843
+ const srcKind: SsrSourceKind = kind === 'sql' ? 'sql' : kind === 'rest' ? 'rest' : 'memory'
2673
2844
  if (ssrScreenShape(project, screen) === 'grid') {
2674
2845
  pages.push(...emitSsrGridScreen(schema, screen, srcKind, accessEnabled, screensByEntity.get(screen.entity) ?? [], byName))
2675
2846
  } else {
@@ -2682,6 +2853,20 @@ export function emitStudioProject(project: StudioProject): GeneratedFile[] {
2682
2853
  }
2683
2854
  pages.push(screenPage(schema, rawByName.get(screen.entity) ?? schema, screen, resolve, (name) => rawByName.get(name), accessEnabled, i18nEnabled, routeById, drillEnabled, false, sources[screen.entity]))
2684
2855
  }
2856
+ // Once the root layout leaves SSR on, every client-only screen has to say so
2857
+ // itself: these pages fetch their rows in the browser, so rendering them on the
2858
+ // server would run that fetch there and hand back an empty first paint.
2859
+ const anySsr = project.screens.some((s) => isSsrScreen(project, s))
2860
+ if (anySsr) {
2861
+ for (const screen of project.screens) {
2862
+ if (isSsrScreen(project, screen) || !screen.route) continue
2863
+ pages.push({
2864
+ path: `src/routes/${screen.route}/+page.ts`,
2865
+ description: `${screen.title} renders in the browser (client data source).`,
2866
+ contents: `// This screen loads its rows in the browser, so it opts out of the root\n// layout's server rendering.\nexport const ssr = false\n`,
2867
+ })
2868
+ }
2869
+ }
2685
2870
  // Any SSR screen needs the shared URL-params -> query helper (server-only).
2686
2871
  const ssrHelpers = project.screens.some((s) => isSsrScreen(project, s)) ? [ssrQueryHelperFile()] : []
2687
2872
  const actionRouteFiles = [...actionsById.values()].map(({ action, screenId }) => actionRouteFile(action, screenId, accessEnabled))
@@ -4516,6 +4701,26 @@ main().then(() => process.exit(0)).catch((err) => { console.error(err); process.
4516
4701
  const appSlug = (title: string): string =>
4517
4702
  (title || 'studio-app').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'studio-app'
4518
4703
 
4704
+ /**
4705
+ * The root `+layout.ts`.
4706
+ *
4707
+ * An app with server-rendered screens leaves SvelteKit's own default in place
4708
+ * (SSR on) and lets each client-only screen opt out in its own `+page.ts` - so
4709
+ * the app reads the way a hand-written SvelteKit app does, instead of announcing
4710
+ * itself as a SPA in the first file anyone opens. With nothing to server-render,
4711
+ * the whole app stays a SPA as before.
4712
+ */
4713
+ function rootLayoutModule(project: StudioProject): GeneratedFile {
4714
+ const anySsr = project.screens.some((s) => isSsrScreen(project, s))
4715
+ return {
4716
+ path: 'src/routes/+layout.ts',
4717
+ description: anySsr ? 'Root layout options (SSR on; client-only screens opt out per page).' : 'Client SPA (in-memory sources persist across navigation).',
4718
+ contents: anySsr
4719
+ ? `// Server rendering stays on (SvelteKit's default). Screens that have to run in\n// the browser - anything on an in-memory source, or a shape that can't render on\n// the server - opt out in their own +page.ts.\nexport const prerender = false\n`
4720
+ : `// In-memory sources are module singletons, so render as a client SPA. Move an\n// entity to SQL / Supabase and its /api route still runs server-side.\nexport const ssr = false\nexport const prerender = false\n`,
4721
+ }
4722
+ }
4723
+
4519
4724
  /** The static SvelteKit + Vite scaffolding around the generated screens. */
4520
4725
  const SCAFFOLD_STATIC: ReadonlyArray<GeneratedFile> = [
4521
4726
  // watch.ignored: `studio dev` runs this app's Vite next to the designer, which
@@ -4525,7 +4730,6 @@ const SCAFFOLD_STATIC: ReadonlyArray<GeneratedFile> = [
4525
4730
  { path: 'tsconfig.json', description: 'TypeScript config.', contents: `{\n "extends": "./.svelte-kit/tsconfig.json",\n "compilerOptions": {\n "allowJs": true,\n "checkJs": true,\n "esModuleInterop": true,\n "forceConsistentCasingInFileNames": true,\n "resolveJsonModule": true,\n "skipLibCheck": true,\n "sourceMap": true,\n "strict": true,\n "moduleResolution": "bundler"\n }\n}\n` },
4526
4731
  { path: 'src/app.html', description: 'HTML shell.', contents: `<!doctype html>\n<html lang="en">\n <head>\n <meta charset="utf-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1" />\n %sveltekit.head%\n </head>\n <body data-sveltekit-preload-data="hover">\n <div style="display: contents">%sveltekit.body%</div>\n </body>\n</html>\n` },
4527
4732
  { path: 'src/app.d.ts', description: 'SvelteKit app types.', contents: `declare global {\n namespace App {}\n}\n\nexport {}\n` },
4528
- { path: 'src/routes/+layout.ts', description: 'Client SPA (in-memory sources persist across navigation).', contents: `// In-memory sources are module singletons, so render as a client SPA. Move an\n// entity to SQL / Supabase and its /api route still runs server-side.\nexport const ssr = false\nexport const prerender = false\n` },
4529
4733
  // engine-strict=false: a mismatched Node `engines` range only warns, never hard-fails
4530
4734
  // `npm install` - important for sandboxes (StackBlitz WebContainer) whose Node version
4531
4735
  // may not satisfy every transitive dep. (npm 7+ installs peers by default, so no
@@ -4883,7 +5087,12 @@ function svelteConfig(plan: DeployPlan): string {
4883
5087
  /** The runtime npm deps the generated source imports, keyed by package -> range.
4884
5088
  * One source of truth for package.json AND the fragment-mode dependency report. */
4885
5089
  export function runtimeDeps(project: StudioProject, allSource: string): Record<string, string> {
4886
- const dependencies: Record<string, string> = { '@svgrid/grid': 'latest', '@svgrid/enterprise': 'latest' }
5090
+ // Pin to the generator's own version, not `latest`: the emitted code uses the
5091
+ // APIs of the SvGrid it was written by, so the app must install a runtime at
5092
+ // least that new. `latest` also made a scaffold non-reproducible - an app could
5093
+ // resolve a different runtime tomorrow, or break on an unrelated publish.
5094
+ const svgrid = `^${SVGRID_VERSION}`
5095
+ const dependencies: Record<string, string> = { '@svgrid/grid': svgrid, '@svgrid/enterprise': svgrid }
4887
5096
  if (allSource.includes("from '@supabase/supabase-js'")) dependencies['@supabase/supabase-js'] = '^2.45.0'
4888
5097
  if (allSource.includes("import pg from 'pg'")) dependencies['pg'] = '^8.11.0'
4889
5098
  if (allSource.includes("from 'mysql2/promise'")) dependencies['mysql2'] = '^3.9.0'
@@ -5088,6 +5297,7 @@ export function emitStudioAppBundle(project: StudioProject): GeneratedFile[] {
5088
5297
  { path: 'src/lib/schemas.test.ts', description: 'Smoke tests: every entity renders + round-trips through its data source.', contents: smokeTestFile(project) },
5089
5298
  ...plan.files,
5090
5299
  ...cronScheduleFiles(project, plan),
5300
+ rootLayoutModule(project),
5091
5301
  ...SCAFFOLD_STATIC,
5092
5302
  ...(envExample(allSource) ? [{ path: '.env.example', description: 'Environment variables the app reads (copy to .env and fill in).', contents: envExample(allSource)! }] : []),
5093
5303
  ...(envDotFile(allSource) ? [{ path: '.env', description: 'Local env (git-ignored): a real random SESSION_SECRET is pre-filled so sessions are secure out of the box; fill in the rest.', contents: envDotFile(allSource)! }] : []),
@@ -88,7 +88,10 @@ export const lookupVar = (schema: EntitySchema, field: string) => `${camel(schem
88
88
  function rowType(schema: EntitySchema, derived: Set<string>): string {
89
89
  // Runtime-filled columns are optional in the row type: relation labels (`derived`)
90
90
  // and computed / no-code-formula fields (not present on seed rows / inserts).
91
- const optional = (f: EntityField) => derived.has(f.field) || !!f.formula || !!f.computed
91
+ // So is a field the form only asks for under a condition - a row that never met
92
+ // the condition simply does not carry it (a won deal has no "lost reason").
93
+ const optional = (f: EntityField) =>
94
+ derived.has(f.field) || !!f.formula || !!f.computed || !!f.when?.visible
92
95
  const lines = schema.fields.map((f) => ` ${f.field}${optional(f) ? '?' : ''}: ${tsType(f)}`)
93
96
  return `export type ${pascal(schema.name)} = {\n${lines.join('\n')}\n}`
94
97
  }
@@ -54,6 +54,7 @@ export {
54
54
  type InitFlags,
55
55
  type InitResult,
56
56
  } from './init-flow.js'
57
+ export { UserError, isUserError } from './user-error.js'
57
58
  export {
58
59
  crudSuiteScreens,
59
60
  addCrudSuite,
@@ -171,6 +172,7 @@ export {
171
172
  clickSlot,
172
173
  ssrScreenShape,
173
174
  ssrEligible,
175
+ withSsrDefaults,
174
176
  setScreenRenderMode,
175
177
  eventSlot,
176
178
  rowSelectSlot,
@@ -1,8 +1,19 @@
1
1
  import { describe, it, expect, vi, afterEach } from 'vitest'
2
2
  import { runStudioInit, parseSelection, type DbGateway, type PromptIO } from './init-flow.js'
3
+ import { isUserError } from './user-error.js'
3
4
  import type { StudioIO } from './cli.js'
4
5
  import { parseProject, validateProject, type GridConfig } from './project.js'
5
6
 
7
+ /** Run the flow and hand back whatever it threw. */
8
+ async function thrownBy(run: Promise<unknown>): Promise<unknown> {
9
+ try {
10
+ await run
11
+ throw new Error('expected the flow to throw')
12
+ } catch (err) {
13
+ return err
14
+ }
15
+ }
16
+
6
17
  /** A scripted terminal: answers are consumed in order, defaults fill the rest. */
7
18
  function scriptedPrompts(answers: string[]) {
8
19
  const asked: string[] = []
@@ -352,6 +363,145 @@ describe('runStudioInit', () => {
352
363
  expect(result.project.theme?.mode).toBe('dark')
353
364
  })
354
365
 
366
+ describe('mistakes are reported as mistakes, not crashes', () => {
367
+ // The CLI writes a bug report and prints a "report this" link for anything
368
+ // that isn't a UserError, so a typo must never come back as an Error.
369
+ it('flags a bad dataset id, an empty table pick and a missing driver as user errors', async () => {
370
+ const fs = memoryIo()
371
+ const badDataset = await thrownBy(
372
+ runStudioInit({ yes: true, dataset: 'nope' }, scriptedPrompts([]).io, null, fs.io),
373
+ )
374
+ expect(isUserError(badDataset)).toBe(true)
375
+ expect((badDataset as { hint?: string }).hint).toContain('customers-orders')
376
+
377
+ const noTables = await thrownBy(
378
+ runStudioInit({ db: 'postgres', url: 'postgres://x' }, scriptedPrompts(['none']).io, fakeDb(), fs.io),
379
+ )
380
+ expect(isUserError(noTables)).toBe(true)
381
+
382
+ const db: DbGateway = { ...fakeDb(), ensureDriver: async () => ({ ok: false, message: 'pg is not installed' }) }
383
+ const noDriver = await thrownBy(
384
+ runStudioInit({ db: 'postgres', url: 'postgres://x' }, scriptedPrompts(['all']).io, db, fs.io),
385
+ )
386
+ expect(isUserError(noDriver)).toBe(true)
387
+ })
388
+
389
+ it('blames the connection string when the database cannot be reached', async () => {
390
+ const fs = memoryIo()
391
+ const db: DbGateway = {
392
+ ensureDriver: async () => ({ ok: true }),
393
+ connect: async () => { throw new Error('getaddrinfo ENOTFOUND base') },
394
+ }
395
+ const err = await thrownBy(
396
+ runStudioInit({ db: 'postgres', url: 'not-a-real-conn' }, scriptedPrompts([]).io, db, fs.io),
397
+ )
398
+
399
+ expect(isUserError(err)).toBe(true)
400
+ // The driver's own wording survives - it is the diagnostic - but the hint
401
+ // points at the thing the user can actually fix.
402
+ expect((err as Error).message).toContain('ENOTFOUND')
403
+ expect((err as { hint?: string }).hint).toContain('postgres://')
404
+ })
405
+
406
+ it('explains a REST endpoint that is not an absolute URL instead of throwing "Invalid URL"', async () => {
407
+ const prompts = scriptedPrompts(['5', './rows.json'])
408
+ const fs = memoryIo()
409
+ const rows = JSON.stringify([{ id: 1, name: 'Widget' }])
410
+ const err = await thrownBy(runStudioInit({}, prompts.io, null, fs.io, async () => rows))
411
+
412
+ expect(isUserError(err)).toBe(true)
413
+ expect((err as Error).message).not.toMatch(/Invalid URL/)
414
+ expect((err as Error).message).toContain('not a full URL')
415
+ })
416
+
417
+ it('explains a REST endpoint that does not return JSON', async () => {
418
+ const prompts = scriptedPrompts(['5', 'https://api.example.com/widgets'])
419
+ const fs = memoryIo()
420
+ const err = await thrownBy(runStudioInit({}, prompts.io, null, fs.io, async () => '<html>nope</html>'))
421
+
422
+ expect(isUserError(err)).toBe(true)
423
+ expect((err as Error).message).toContain('did not return JSON')
424
+ })
425
+ })
426
+
427
+ describe('target folder guard', () => {
428
+ /** A folder that already holds somebody else's SvelteKit app. */
429
+ const existingApp = () => {
430
+ const fs = memoryIo()
431
+ fs.files.set('package.json', '{ "name": "their-app" }')
432
+ fs.files.set('svelte.config.js', 'export default {}')
433
+ return fs
434
+ }
435
+
436
+ it('asks before overwriting an app that is already there', async () => {
437
+ const fs = existingApp()
438
+ const prompts = scriptedPrompts(['1', '1', 'y', '1', '1', 'n', 'Mine', '.', 'y'])
439
+ await runStudioInit({}, prompts.io, null, fs.io)
440
+
441
+ expect(prompts.asked).toContain('Overwrite? (y/N)')
442
+ expect(fs.files.has('studio.config.json')).toBe(true)
443
+ })
444
+
445
+ it('writes nothing when the answer is no', async () => {
446
+ const fs = existingApp()
447
+ const prompts = scriptedPrompts(['1', '1', 'y', '1', '1', 'n', 'Mine', '.', 'n'])
448
+ const err = await thrownBy(runStudioInit({}, prompts.io, null, fs.io))
449
+
450
+ expect(isUserError(err)).toBe(true)
451
+ expect(fs.files.get('package.json')).toBe('{ "name": "their-app" }')
452
+ expect(fs.files.has('studio.config.json')).toBe(false)
453
+ })
454
+
455
+ it('refuses rather than silently clobbering when nothing can be asked', async () => {
456
+ const fs = existingApp()
457
+ const err = await thrownBy(runStudioInit({ yes: true }, scriptedPrompts([]).io, null, fs.io))
458
+
459
+ expect(isUserError(err)).toBe(true)
460
+ expect((err as { hint?: string }).hint).toContain('--force')
461
+ expect(fs.files.get('package.json')).toBe('{ "name": "their-app" }')
462
+ })
463
+
464
+ it('goes ahead with --force', async () => {
465
+ const fs = existingApp()
466
+ await runStudioInit({ yes: true, force: true }, scriptedPrompts([]).io, null, fs.io)
467
+ expect(fs.files.get('package.json')).not.toBe('{ "name": "their-app" }')
468
+ })
469
+
470
+ it('treats a folder Studio generated as a regenerate, not a clobber', async () => {
471
+ const fs = memoryIo()
472
+ await runStudioInit({ yes: true, out: 'app' }, scriptedPrompts([]).io, null, fs.io)
473
+ // studio.config.json is there now, so a second run asks nothing.
474
+ const second = scriptedPrompts([])
475
+ await runStudioInit({ yes: true, out: 'app' }, second.io, null, fs.io)
476
+ expect(second.asked).toEqual([])
477
+ })
478
+
479
+ it('ignores files Studio itself creates mid-run', async () => {
480
+ // Picking a database installs a driver into the target folder first, which
481
+ // writes a package.json. The guard must not then trip over it.
482
+ const fs = memoryIo()
483
+ const db: DbGateway = {
484
+ ...fakeDb(),
485
+ ensureDriver: async () => {
486
+ await fs.io.writeFile('package.json', '{ "dependencies": { "pg": "^8" } }')
487
+ return { ok: true }
488
+ },
489
+ }
490
+ const prompts = scriptedPrompts([])
491
+ const result = await runStudioInit({ db: 'postgres', url: 'postgres://x', yes: true }, prompts.io, db, fs.io)
492
+
493
+ expect(prompts.asked).toEqual([])
494
+ expect(result.written.some((p) => p.endsWith('studio.config.json'))).toBe(true)
495
+ })
496
+
497
+ it('leaves an empty folder alone', async () => {
498
+ const fs = memoryIo()
499
+ const prompts = scriptedPrompts([])
500
+ await runStudioInit({ yes: true }, prompts.io, null, fs.io)
501
+ expect(prompts.asked).toEqual([])
502
+ })
503
+ })
504
+
355
505
  it('keeps user-owned files that already exist', async () => {
356
506
  const fs = memoryIo()
357
507
  await runStudioInit({ yes: true }, scriptedPrompts([]).io, null, fs.io)