@svgrid/enterprise 2.4.0 → 2.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 (161) hide show
  1. package/dist/cdn/svgrid-enterprise.svelte-external.js +5990 -5612
  2. package/dist/designer/assets/{index-okIcRP9P.js → index-OwdzSsmo.js} +3 -3
  3. package/dist/designer/assets/{src-BvsJ00AO.css → src-D-O2F805.css} +1 -1
  4. package/dist/designer/assets/{src-CIla2aVG.js → src-LRVqg7Sf.js} +259 -228
  5. package/dist/designer/index.html +3 -3
  6. package/dist/node/studio.js +681 -84
  7. package/package.json +3 -3
  8. package/src/SvGridEditPanel.dom.test.ts +174 -0
  9. package/src/SvGridEditPanel.svelte +209 -46
  10. package/src/edit-panel.test.ts +145 -36
  11. package/src/edit-panel.ts +167 -16
  12. package/src/expressions/evaluate.ts +1 -1
  13. package/src/expressions/parse.ts +3 -3
  14. package/src/index.ts +14 -0
  15. package/src/license-core.ts +77 -5
  16. package/src/license-expiry.test.ts +91 -0
  17. package/src/license.ts +31 -2
  18. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/_svelte_metadata.json +1 -0
  19. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/_metadata.json +178 -0
  20. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-ZrPsKxob.js +3875 -0
  21. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-ZrPsKxob.js.map +1 -0
  22. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-ACqvWGE2.js +21 -0
  23. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-ACqvWGE2.js.map +1 -0
  24. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-D3w5jt92.js +1938 -0
  25. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-D3w5jt92.js.map +1 -0
  26. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-CC2V3H0y.js +205 -0
  27. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-CC2V3H0y.js.map +1 -0
  28. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-BXP37tAa.js +18 -0
  29. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-BXP37tAa.js.map +1 -0
  30. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-DwubnU5b.js +264 -0
  31. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-DwubnU5b.js.map +1 -0
  32. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-hD7f6PUz.js +697 -0
  33. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-hD7f6PUz.js.map +1 -0
  34. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/package.json +3 -0
  35. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js +134 -0
  36. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js.map +1 -0
  37. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js +55 -0
  38. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js.map +1 -0
  39. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_attachments.js +2 -0
  40. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js +46843 -0
  41. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js.map +1 -0
  42. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js +237 -0
  43. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js.map +1 -0
  44. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_events.js +2 -0
  45. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js +4 -0
  46. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js.map +1 -0
  47. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_client.js +7 -0
  48. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js +4 -0
  49. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js.map +1 -0
  50. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js +5 -0
  51. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js.map +1 -0
  52. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js +5 -0
  53. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js.map +1 -0
  54. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js +5 -0
  55. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js.map +1 -0
  56. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_server.js +6 -0
  57. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js +83 -0
  58. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js.map +1 -0
  59. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js +559 -0
  60. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js.map +1 -0
  61. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js +24 -0
  62. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js.map +1 -0
  63. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js +122 -0
  64. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js.map +1 -0
  65. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_server.js +2 -0
  66. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js +89 -0
  67. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js.map +1 -0
  68. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js +241 -0
  69. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js.map +1 -0
  70. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DBx1SCN6.js +4938 -0
  71. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DBx1SCN6.js.map +1 -0
  72. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-gjfpSTFS.js +495 -0
  73. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-gjfpSTFS.js.map +1 -0
  74. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-DKoOKg8S.js +190 -0
  75. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-DKoOKg8S.js.map +1 -0
  76. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-C0MiR7eR.js +8 -0
  77. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-C0MiR7eR.js.map +1 -0
  78. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-B6Y7P0Q-.js +138 -0
  79. package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-B6Y7P0Q-.js.map +1 -0
  80. package/src/schema.ts +63 -0
  81. package/src/studio/emit-project.test.ts +191 -0
  82. package/src/studio/emit-project.ts +237 -27
  83. package/src/studio/emit-schema.ts +4 -1
  84. package/src/studio/index.ts +2 -0
  85. package/src/studio/init-flow.test.ts +150 -0
  86. package/src/studio/init-flow.ts +169 -22
  87. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/_svelte_metadata.json +1 -0
  88. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/_metadata.json +178 -0
  89. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-BOv1rBKp.js +3875 -0
  90. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-BOv1rBKp.js.map +1 -0
  91. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-B9vgIdCd.js +21 -0
  92. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-B9vgIdCd.js.map +1 -0
  93. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-T1qbW_qD.js +1938 -0
  94. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-T1qbW_qD.js.map +1 -0
  95. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-Bo11SfVK.js +205 -0
  96. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-Bo11SfVK.js.map +1 -0
  97. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-X7D4vfrZ.js +18 -0
  98. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-X7D4vfrZ.js.map +1 -0
  99. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-oyUje5i1.js +264 -0
  100. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-oyUje5i1.js.map +1 -0
  101. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-DeSQgSFm.js +697 -0
  102. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-DeSQgSFm.js.map +1 -0
  103. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/package.json +3 -0
  104. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js +134 -0
  105. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js.map +1 -0
  106. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js +55 -0
  107. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js.map +1 -0
  108. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_attachments.js +2 -0
  109. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js +46843 -0
  110. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js.map +1 -0
  111. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js +237 -0
  112. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js.map +1 -0
  113. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_events.js +2 -0
  114. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js +4 -0
  115. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js.map +1 -0
  116. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_client.js +7 -0
  117. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js +4 -0
  118. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js.map +1 -0
  119. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js +5 -0
  120. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js.map +1 -0
  121. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js +5 -0
  122. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js.map +1 -0
  123. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js +5 -0
  124. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js.map +1 -0
  125. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_server.js +6 -0
  126. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js +83 -0
  127. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js.map +1 -0
  128. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js +559 -0
  129. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js.map +1 -0
  130. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js +24 -0
  131. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js.map +1 -0
  132. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js +122 -0
  133. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js.map +1 -0
  134. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_server.js +2 -0
  135. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js +89 -0
  136. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js.map +1 -0
  137. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js +241 -0
  138. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js.map +1 -0
  139. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-BbWyXNk1.js +4938 -0
  140. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-BbWyXNk1.js.map +1 -0
  141. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DI_fC7vt.js +495 -0
  142. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DI_fC7vt.js.map +1 -0
  143. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-Dyr3lQv9.js +190 -0
  144. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-Dyr3lQv9.js.map +1 -0
  145. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-_9FHH-LF.js +8 -0
  146. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-_9FHH-LF.js.map +1 -0
  147. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-CUbSPnyP.js +138 -0
  148. package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-CUbSPnyP.js.map +1 -0
  149. package/src/studio/project.test.ts +30 -0
  150. package/src/studio/project.ts +72 -8
  151. package/src/studio/samples/crm.ts +11 -2
  152. package/src/studio/samples/insurance.ts +10 -4
  153. package/src/studio/samples/samples.test.ts +36 -0
  154. package/src/studio/samples/shared.ts +14 -0
  155. package/src/studio/samples/support.ts +12 -5
  156. package/src/studio/screen-suites.test.ts +43 -0
  157. package/src/studio/screen-suites.ts +5 -2
  158. package/src/studio/user-error.ts +31 -0
  159. package/src/upgrade-prompt.ts +23 -10
  160. package/src/version.test.ts +17 -0
  161. package/src/version.ts +13 -0
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "type": "commercial",
5
5
  "url": "https://svgrid.com/pricing"
6
6
  },
7
- "version": "2.4.0",
7
+ "version": "2.5.1",
8
8
  "description": "Enterprise feature pack for SvGrid: Excel (xlsx), PDF, CSV, TSV and HTML export, Excel/CSV/JSON import, pivot tables with a drag-and-drop designer, paginated print, Kanban board and scheduler views. Requires a paid license key.",
9
9
  "license": "SEE LICENSE IN LICENSE",
10
10
  "author": "jQWidgets <sales@jqwidgets.com>",
@@ -65,7 +65,7 @@
65
65
  "jszip": "^3.10.0",
66
66
  "pdfmake": "^0.2.0",
67
67
  "svelte": "^5.0.0",
68
- "@svgrid/grid": "^2.4.0"
68
+ "@svgrid/grid": "^2.5.0"
69
69
  },
70
70
  "peerDependenciesMeta": {
71
71
  "jszip": {
@@ -82,7 +82,7 @@
82
82
  "svelte": "^5.55.5",
83
83
  "typescript": "6.0.3",
84
84
  "vite": "^8.2.1",
85
- "@svgrid/grid": "^2.4.0"
85
+ "@svgrid/grid": "^2.5.0"
86
86
  },
87
87
  "repository": {
88
88
  "type": "git",
@@ -33,6 +33,11 @@ afterEach(() => {
33
33
  })
34
34
 
35
35
  function render(props: Record<string, unknown>): HTMLElement {
36
+ // Tear down a previous mount first: a test that renders more than once used to
37
+ // leak every panel but the last, leaving live components (and duplicate field
38
+ // ids) in the document for whatever ran next.
39
+ if (comp) { unmount(comp); comp = null }
40
+ if (host) { host.remove(); host = null }
36
41
  host = document.createElement('div')
37
42
  document.body.appendChild(host)
38
43
  comp = mount(SvGridEditPanel, { target: host, props })
@@ -143,4 +148,173 @@ describe('SvGridEditPanel (DOM)', () => {
143
148
  expect(payload.values).toMatchObject({ name: 'Ada', mrr: 1200, active: true, tier: 'pro' })
144
149
  expect(payload.values).not.toHaveProperty('id') // readonly PK is never submitted
145
150
  })
151
+
152
+ it('lays the form out from the schema, so a built form travels with the entity', () => {
153
+ const laidOut: EntitySchema = {
154
+ name: 'people',
155
+ idField: 'id',
156
+ fields: [
157
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
158
+ { field: 'name', type: 'text' },
159
+ { field: 'email', type: 'text' },
160
+ { field: 'street', type: 'text' },
161
+ { field: 'city', type: 'text' },
162
+ ],
163
+ form: {
164
+ columns: 2,
165
+ sections: [
166
+ { title: 'Who', description: 'How we reach them.', fields: ['name', 'email'] },
167
+ { title: 'Where', columns: 1, fields: ['street', 'city'] },
168
+ ],
169
+ },
170
+ }
171
+ const el = render({ schema: laidOut, presentation: 'inline', onSubmit: vi.fn() })
172
+
173
+ const titles = [...el.querySelectorAll('.sv-ep__section-title')].map((n) => n.textContent)
174
+ expect(titles).toEqual(['Who', 'Where'])
175
+ expect(el.querySelector('.sv-ep__section-desc')?.textContent).toBe('How we reach them.')
176
+
177
+ // Per-section column count wins over the form's.
178
+ const bodies = [...el.querySelectorAll<HTMLElement>('.sv-ep__body')]
179
+ expect(bodies[0]!.style.getPropertyValue('--sv-ep-cols')).toBe('2')
180
+ expect(bodies[1]!.style.getPropertyValue('--sv-ep-cols')).toBe('1')
181
+
182
+ // The unassigned id field is not dropped - it lands in a trailing group.
183
+ expect(el.querySelector('#sv-ef-id')).toBeTruthy()
184
+ })
185
+
186
+ it('waits until you leave a field before complaining, then clears as you fix it', async () => {
187
+ const required: EntitySchema = {
188
+ name: 'people',
189
+ idField: 'id',
190
+ fields: [
191
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
192
+ { field: 'email', type: 'text', required: true, format: 'email' },
193
+ ],
194
+ }
195
+ const el = render({ schema: required, presentation: 'inline', onSubmit: vi.fn() })
196
+ const input = el.querySelector<HTMLInputElement>('#sv-ef-email')!
197
+
198
+ // Typing something invalid says nothing yet - the user is mid-thought.
199
+ input.value = 'nope'
200
+ input.dispatchEvent(new Event('input', { bubbles: true }))
201
+ flushSync()
202
+ expect(el.querySelector('.sv-ep-field__err')).toBeNull()
203
+ expect(input.getAttribute('aria-invalid')).toBe('false')
204
+
205
+ // Leaving it is the moment to speak up.
206
+ input.dispatchEvent(new FocusEvent('focusout', { bubbles: true }))
207
+ await vi.waitFor(() => expect(el.querySelector('.sv-ep-field__err')?.textContent).toMatch(/valid email/))
208
+ expect(input.getAttribute('aria-invalid')).toBe('true')
209
+ // The message is wired to the input for screen readers.
210
+ expect(input.getAttribute('aria-describedby')).toBe('sv-ee-email')
211
+ expect(el.querySelector('#sv-ee-email')).toBeTruthy()
212
+
213
+ // Correcting it clears the message without another submit.
214
+ input.value = 'ada@example.com'
215
+ input.dispatchEvent(new Event('input', { bubbles: true }))
216
+ await vi.waitFor(() => expect(el.querySelector('.sv-ep-field__err')).toBeNull())
217
+ expect(input.getAttribute('aria-invalid')).toBe('false')
218
+ })
219
+
220
+ it('summarises a failed submit and focuses the first field that needs fixing', async () => {
221
+ const required: EntitySchema = {
222
+ name: 'people',
223
+ idField: 'id',
224
+ fields: [
225
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
226
+ { field: 'name', type: 'text', required: true },
227
+ { field: 'email', type: 'text', required: true },
228
+ ],
229
+ }
230
+ const onSubmit = vi.fn()
231
+ const el = render({ schema: required, presentation: 'inline', onSubmit })
232
+
233
+ el.querySelector<HTMLButtonElement>('button[type="submit"]')!.click()
234
+
235
+ await vi.waitFor(() => expect(el.querySelector('.sv-ep__summary')).toBeTruthy())
236
+ expect(onSubmit).not.toHaveBeenCalled()
237
+ const summary = el.querySelector('.sv-ep__summary')!
238
+ expect(summary.getAttribute('role')).toBe('alert')
239
+ expect(summary.textContent).toContain('Name')
240
+ expect(summary.textContent).toContain('Email')
241
+ // Focus lands on the first problem, so fixing can start immediately.
242
+ expect(document.activeElement?.id).toBe('sv-ef-name')
243
+ })
244
+
245
+ it('asks before throwing away edits, and closes straight away when there are none', () => {
246
+ const onCancel = vi.fn()
247
+ const el = render({
248
+ schema,
249
+ row: { id: '1', name: 'Ada', mrr: 1200, active: true, tier: 'pro' },
250
+ presentation: 'inline',
251
+ onSubmit: vi.fn(),
252
+ onCancel,
253
+ })
254
+ const cancel = () => [...el.querySelectorAll<HTMLButtonElement>('button')].find((b) => b.textContent?.trim() === 'Cancel')!
255
+
256
+ // Untouched: cancelling just closes.
257
+ cancel().click()
258
+ flushSync()
259
+ expect(onCancel).toHaveBeenCalledTimes(1)
260
+
261
+ // Now dirty the form; cancelling asks instead of discarding.
262
+ const name = el.querySelector<HTMLInputElement>('#sv-ef-name')!
263
+ name.value = 'Grace'
264
+ name.dispatchEvent(new Event('input', { bubbles: true }))
265
+ flushSync()
266
+ cancel().click()
267
+ flushSync()
268
+ expect(onCancel).toHaveBeenCalledTimes(1) // still not closed
269
+ expect(el.textContent).toContain('Discard your changes?')
270
+
271
+ // "Keep editing" puts you back in the form with the edit intact.
272
+ ;[...el.querySelectorAll<HTMLButtonElement>('button')].find((b) => b.textContent?.trim() === 'Keep editing')!.click()
273
+ flushSync()
274
+ expect(el.textContent).not.toContain('Discard your changes?')
275
+ expect(el.querySelector<HTMLInputElement>('#sv-ef-name')!.value).toBe('Grace')
276
+
277
+ // Confirming discards.
278
+ cancel().click()
279
+ flushSync()
280
+ ;[...el.querySelectorAll<HTMLButtonElement>('button')].find((b) => b.textContent?.trim() === 'Discard')!.click()
281
+ flushSync()
282
+ expect(onCancel).toHaveBeenCalledTimes(2)
283
+ })
284
+
285
+ it('reveals and locks fields live as the answers change', async () => {
286
+ // One answer drives two others: choosing "refund" asks for a justification
287
+ // and unlocks the approver, both without a remount.
288
+ const cmp = (column: string, op: string, value: unknown) => ({ kind: 'cmp', column, op, value })
289
+ const returns = {
290
+ name: 'returns',
291
+ idField: 'id',
292
+ fields: [
293
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
294
+ { field: 'mode', type: 'text' },
295
+ { field: 'justification', type: 'text', label: 'Justification', when: { visible: cmp('mode', 'equals', 'refund') } },
296
+ { field: 'approver', type: 'text', when: { disabled: cmp('mode', 'notEquals', 'refund') } },
297
+ ],
298
+ } as unknown as EntitySchema<Record<string, unknown>>
299
+
300
+ const el = render({
301
+ schema: returns,
302
+ row: { id: '1', mode: 'exchange', justification: '', approver: '' },
303
+ presentation: 'inline',
304
+ onSubmit: vi.fn(),
305
+ })
306
+
307
+ // An exchange: no justification asked for, and the approver is locked.
308
+ expect(el.textContent).not.toContain('Justification')
309
+ expect(el.querySelector('#sv-ef-approver')!.hasAttribute('disabled')).toBe(true)
310
+
311
+ // Switch to a refund; both fields react to the new value.
312
+ const mode = el.querySelector<HTMLInputElement>('#sv-ef-mode')!
313
+ mode.value = 'refund'
314
+ mode.dispatchEvent(new Event('input', { bubbles: true }))
315
+ flushSync()
316
+
317
+ expect(el.textContent).toContain('Justification')
318
+ expect(el.querySelector('#sv-ef-approver')!.hasAttribute('disabled')).toBe(false)
319
+ })
146
320
  })
@@ -25,7 +25,7 @@
25
25
  import { fade, fly } from 'svelte/transition'
26
26
  import { cubicOut } from 'svelte/easing'
27
27
  import { SvGridDropdown, SvNumberInput, SvColorInput, SvPasswordInput, SvSlider, SvSwitchButton, SvPhoneInput, SvCountryInput, SvMaskedInput, SvDateTimePicker, SvTagsInput, type CellEditorOption } from '@svgrid/grid'
28
- import { schemaToFormFields, type EntitySchema, type FormFieldDescriptor } from './schema'
28
+ import { schemaToFormFields, type EntitySchema, type FormFieldDescriptor, type FormSection } from './schema'
29
29
  import SvFileInput from './SvFileInput.svelte'
30
30
  import {
31
31
  buildInitialValues,
@@ -40,7 +40,11 @@
40
40
  fromNumberValue,
41
41
  toSliderValue,
42
42
  toTags,
43
+ fieldState,
44
+ sectionVisible,
45
+ validateOne,
43
46
  type EditMode,
47
+ type FieldState,
44
48
  } from './edit-panel'
45
49
  import SvLookupInput from './SvLookupInput.svelte'
46
50
  import type { RelationLookup } from './sources/relation-lookup'
@@ -76,13 +80,15 @@
76
80
  * localStorage under this key, so it reopens where the user left it.
77
81
  */
78
82
  persistKey?: string
79
- /** Form column count (default 2). A field with `span: 2` spans all columns. */
83
+ /** Form column count. Defaults to the schema's `form.columns`, else 2. A field
84
+ * with `span: 2` spans all columns. */
80
85
  columns?: 1 | 2 | 3
81
86
  /** Restrict + order the form fields (by field name). Default: all schema fields. */
82
87
  formFields?: string[]
83
- /** Group fields into titled fieldsets. Fields not in any section render in a
84
- * trailing default group. When set, `formFields` ordering is per-section. */
85
- sections?: Array<{ title?: string; fields: string[] }>
88
+ /** Group fields into titled fieldsets, overriding the schema's `form.sections`.
89
+ * Fields not in any section render in a trailing default group. When set,
90
+ * `formFields` ordering is per-section. */
91
+ sections?: FormSection[]
86
92
  /** Dialog width for the modal / drawer presentations. Default 'md'. */
87
93
  formSize?: 'sm' | 'md' | 'lg'
88
94
  onSubmit: (payload: SubmitPayload) => void | Promise<void>
@@ -100,7 +106,7 @@
100
106
  uploads,
101
107
  dependentOptions,
102
108
  persistKey,
103
- columns = 2,
109
+ columns,
104
110
  formFields,
105
111
  sections,
106
112
  formSize = 'md',
@@ -108,25 +114,51 @@
108
114
  onCancel,
109
115
  }: Props = $props()
110
116
 
117
+ let values = $state<Record<string, any>>({})
118
+ let errors = $state<Record<string, string>>({})
119
+ let submitting = $state(false)
120
+ let submitError = $state<string | null>(null)
121
+ // Fields the user has finished with. A field is only allowed to show an error
122
+ // once they have left it, so a form does not scold you for what you have not
123
+ // typed yet; after a failed submit everything counts as visited.
124
+ let touched = $state<Record<string, boolean>>({})
125
+ let submitAttempted = $state(false)
126
+
111
127
  const allFields = $derived(schemaToFormFields(schema))
112
128
  // Apply the include/order list (formFields), else keep the schema order.
113
- const fields = $derived(
129
+ const listedFields = $derived(
114
130
  formFields && formFields.length
115
131
  ? (formFields.map((name) => allFields.find((f) => f.field === name)).filter(Boolean) as FormFieldDescriptor[])
116
132
  : allFields,
117
133
  )
134
+ // Live state per field: a `when` condition re-evaluates against the current
135
+ // values, so answering one question can reveal, lock, or demand another.
136
+ const fieldStates = $derived(new Map(allFields.map((f) => [f.field, fieldState(f, values)] as const)))
137
+ const stateOf = (f: FormFieldDescriptor): FieldState =>
138
+ fieldStates.get(f.field) ?? { visible: true, disabled: f.readonly, required: f.required }
139
+ const fields = $derived(listedFields.filter((f) => stateOf(f).visible))
140
+ // The arrangement comes from the schema (so a built form travels with the
141
+ // entity); the props override it for a one-off layout of a shared schema.
142
+ const layoutColumns = $derived(columns ?? schema.form?.columns ?? 2)
143
+ const layoutSections = $derived(sections ?? schema.form?.sections)
144
+
118
145
  // Grouped layout: each section's resolvable fields, plus a trailing group for
119
146
  // anything not assigned to a section (so no field is ever silently dropped).
120
- const fieldGroups = $derived.by((): Array<{ title?: string; fields: FormFieldDescriptor[] }> => {
121
- if (!sections || !sections.length) return [{ fields }]
147
+ const fieldGroups = $derived.by((): Array<{ title?: string; description?: string; columns?: 1 | 2 | 3; fields: FormFieldDescriptor[] }> => {
148
+ if (!layoutSections || !layoutSections.length) return [{ fields }]
122
149
  const assigned = new Set<string>()
123
- const groups = sections.map((s) => {
150
+ const groups = layoutSections.map((s) => {
124
151
  const gf = s.fields.map((name) => fields.find((f) => f.field === name)).filter(Boolean) as FormFieldDescriptor[]
125
152
  for (const f of gf) assigned.add(f.field)
126
- return { title: s.title, fields: gf }
153
+ // A section can be conditional in its own right, the same way a field is.
154
+ const shown = s.visibleWhen ? sectionVisible(s.visibleWhen, values) : true
155
+ return { title: s.title, description: s.description, columns: s.columns, fields: shown ? gf : [] }
127
156
  })
128
157
  const rest = fields.filter((f) => !assigned.has(f.field))
129
- return rest.length ? [...groups, { fields: rest }] : groups
158
+ // A section whose fields are all hidden by a condition disappears with them,
159
+ // rather than leaving a heading over nothing.
160
+ const shown = groups.filter((g) => g.fields.length)
161
+ return rest.length ? [...shown, { fields: rest }] : shown
130
162
  })
131
163
 
132
164
  // Options for the custom dropdown: prepend a blank "clear" option for
@@ -139,15 +171,21 @@
139
171
  )
140
172
  const overlayed = $derived(presentation !== 'inline')
141
173
 
142
- let values = $state<Record<string, any>>({})
143
- let errors = $state<Record<string, string>>({})
144
- let submitting = $state(false)
145
- let submitError = $state<string | null>(null)
174
+ // The values the form opened with, to tell an edited form from an untouched one.
175
+ let initial = $state<Record<string, any>>({})
146
176
 
147
177
  $effect(() => {
148
- values = buildInitialValues(schema, row)
178
+ // Seed from a local, and copy from that local rather than from `values`:
179
+ // reading the state this effect also writes makes the effect its own
180
+ // dependency, which spins forever.
181
+ const seeded = buildInitialValues(schema, row)
182
+ values = seeded
183
+ initial = { ...seeded }
149
184
  errors = {}
185
+ touched = {}
186
+ submitAttempted = false
150
187
  submitError = null
188
+ confirmingDiscard = false
151
189
  })
152
190
 
153
191
  // Cascade: when a parent field changes, clear a dependent field whose value is
@@ -171,12 +209,50 @@
171
209
  }
172
210
  })
173
211
 
212
+ /**
213
+ * Re-check one field as the user leaves it. Once a field has an error it keeps
214
+ * being checked on every keystroke, so a correction clears the message
215
+ * immediately instead of making them submit again to find out.
216
+ */
217
+ async function checkField(field: string) {
218
+ touched[field] = true
219
+ const message = await validateOne(schema, field, values)
220
+ if (message) errors[field] = message
221
+ else delete errors[field]
222
+ }
223
+
224
+ // Live re-validation, but only for fields already showing an error.
225
+ $effect(() => {
226
+ void values
227
+ for (const field of Object.keys(errors)) void checkField(field)
228
+ })
229
+
230
+ /** Errors the form is allowed to show: visited fields, or everything after a submit. */
231
+ const shownErrors = $derived(
232
+ Object.fromEntries(Object.entries(errors).filter(([field]) => submitAttempted || touched[field])),
233
+ )
234
+ const errorList = $derived(
235
+ fields.filter((f) => shownErrors[f.field]).map((f) => ({ field: f.field, label: f.label, message: shownErrors[f.field]! })),
236
+ )
237
+
238
+ /** Send focus to a field from the error summary. */
239
+ function focusField(field: string) {
240
+ const el = document.getElementById(`sv-ef-${field}`)
241
+ if (el instanceof HTMLElement) el.focus()
242
+ }
243
+
174
244
  async function handleSubmit(event: SubmitEvent) {
175
245
  event.preventDefault()
176
246
  submitError = null
247
+ submitAttempted = true
177
248
  const found = await validateAll(schema, values)
178
249
  errors = found
179
- if (Object.keys(found).length > 0) return
250
+ if (Object.keys(found).length > 0) {
251
+ // Put the user on the first thing they need to fix.
252
+ const first = fields.find((f) => found[f.field])
253
+ if (first) focusField(first.field)
254
+ return
255
+ }
180
256
  submitting = true
181
257
  try {
182
258
  await onSubmit({
@@ -191,11 +267,28 @@
191
267
  }
192
268
  }
193
269
 
270
+ /**
271
+ * Guard against throwing away edits. Closing a dirty form asks first, in the
272
+ * footer rather than through a `confirm()` dialog, so the answer appears where
273
+ * the user is already looking and the form stays on screen behind it.
274
+ */
275
+ let confirmingDiscard = $state(false)
276
+ const isDirty = $derived(
277
+ !submitting && fields.some((f) => !f.readonly && String(values[f.field] ?? '') !== String(initial[f.field] ?? '')),
278
+ )
279
+
194
280
  function close() {
281
+ if (isDirty && !confirmingDiscard) {
282
+ confirmingDiscard = true
283
+ return
284
+ }
285
+ confirmingDiscard = false
195
286
  onCancel?.()
196
287
  }
197
288
  function onKeydown(e: KeyboardEvent) {
198
- if (e.key === 'Escape' && onCancel) close()
289
+ if (e.key !== 'Escape' || !onCancel) return
290
+ // A second Escape confirms, so the keyboard path is not a dead end.
291
+ close()
199
292
  }
200
293
  function stop(e: Event) {
201
294
  e.stopPropagation()
@@ -376,17 +469,31 @@
376
469
  </div>
377
470
  </header>
378
471
 
472
+ {#if errorList.length > 1}
473
+ <!-- One place to see everything that needs fixing, with a jump to each.
474
+ Only worth showing when more than one field failed. -->
475
+ <div class="sv-ep__summary" role="alert">
476
+ <p class="sv-ep__summary-title">Please fix {errorList.length} fields:</p>
477
+ <ul>
478
+ {#each errorList as e (e.field)}
479
+ <li><button type="button" onclick={() => focusField(e.field)}>{e.label}: {e.message}</button></li>
480
+ {/each}
481
+ </ul>
482
+ </div>
483
+ {/if}
484
+
379
485
  {#if fieldGroups.length > 1 || fieldGroups[0]?.title}
380
486
  {#each fieldGroups as g, gi (gi)}
381
487
  <div class="sv-ep__section">
382
488
  {#if g.title}<h4 class="sv-ep__section-title">{g.title}</h4>{/if}
383
- <div class="sv-ep__body" style="--sv-ep-cols: {columns}">
489
+ {#if g.description}<p class="sv-ep__section-desc">{g.description}</p>{/if}
490
+ <div class="sv-ep__body" style="--sv-ep-cols: {g.columns ?? layoutColumns}">
384
491
  {#each g.fields as f (f.field)}{@render fieldRow(f)}{/each}
385
492
  </div>
386
493
  </div>
387
494
  {/each}
388
495
  {:else}
389
- <div class="sv-ep__body" style="--sv-ep-cols: {columns}">
496
+ <div class="sv-ep__body" style="--sv-ep-cols: {layoutColumns}">
390
497
  {#each fields as f (f.field)}{@render fieldRow(f)}{/each}
391
498
  </div>
392
499
  {/if}
@@ -394,21 +501,38 @@
394
501
  {#if submitError}<p class="sv-ep__submit-err" role="alert">{submitError}</p>{/if}
395
502
 
396
503
  <footer class="sv-ep__footer">
397
- {#if onCancel}
398
- <button type="button" class="sv-ep__btn" onclick={close} disabled={submitting}>Cancel</button>
504
+ {#if confirmingDiscard}
505
+ <span class="sv-ep__discard" role="alert">Discard your changes?</span>
506
+ <button type="button" class="sv-ep__btn" onclick={() => (confirmingDiscard = false)}>Keep editing</button>
507
+ <button type="button" class="sv-ep__btn sv-ep__btn--danger" onclick={close}>Discard</button>
508
+ {:else}
509
+ {#if onCancel}
510
+ <button type="button" class="sv-ep__btn" onclick={close} disabled={submitting}>Cancel</button>
511
+ {/if}
512
+ <button type="submit" class="sv-ep__btn sv-ep__btn--primary" disabled={submitting}>
513
+ {submitting ? 'Saving…' : (submitLabel ?? (mode === 'create' ? 'Create' : 'Save'))}
514
+ </button>
399
515
  {/if}
400
- <button type="submit" class="sv-ep__btn sv-ep__btn--primary" disabled={submitting}>
401
- {submitting ? 'Saving…' : (submitLabel ?? (mode === 'create' ? 'Create' : 'Save'))}
402
- </button>
403
516
  </footer>
404
517
  </form>
405
518
  {/snippet}
406
519
 
407
- {#snippet fieldRow(f: FormFieldDescriptor)}
520
+ {#snippet fieldRow(spec: FormFieldDescriptor)}
521
+ {@const st = stateOf(spec)}
522
+ <!-- Every control below already reads `readonly` / `required` off the
523
+ descriptor, so folding the live condition state into a shadow copy
524
+ makes all of them honour it without touching each branch. -->
525
+ {@const f = st.disabled === spec.readonly && st.required === spec.required
526
+ ? spec
527
+ : { ...spec, readonly: st.disabled, required: st.required }}
408
528
  {@const kind = controlKind(f)}
409
529
  {@const lk = f.relation ? lookups?.[f.field] : undefined}
410
530
  {@const depOptions = dependentOptions?.[f.field]}
411
- <div class="sv-ep-field" class:sv-ep-field--wide={f.span === 2} class:sv-ep-field--error={!!errors[f.field]}>
531
+ {@const err = shownErrors[f.field]}
532
+ <!-- Point screen readers at whichever of help / error is on screen. -->
533
+ {@const describedBy = err ? `sv-ee-${f.field}` : f.help ? `sv-eh-${f.field}` : undefined}
534
+ <div class="sv-ep-field" class:sv-ep-field--wide={f.span === 2} class:sv-ep-field--error={!!err}
535
+ onfocusoutcapture={() => { if (!f.readonly) void checkField(f.field) }}>
412
536
  <label for={`sv-ef-${f.field}`}>
413
537
  {f.label}{#if f.required && !f.readonly}<span class="sv-ep-field__req" aria-hidden="true"> *</span>{/if}
414
538
  </label>
@@ -447,31 +571,31 @@
447
571
  <!-- Boolean fields render as the suite's switch (nicer than a raw checkbox). -->
448
572
  <SvSwitchButton id={`sv-ef-${f.field}`} ariaLabel={f.label} checked={!!values[f.field]} disabled={f.readonly} onChange={(v) => (values[f.field] = v)} />
449
573
  {:else if f.editorType === 'number'}
450
- <SvNumberInput id={`sv-ef-${f.field}`} ariaLabel={f.label} value={toNumberValue(values[f.field])} min={f.min} max={f.max} step={f.step} precision={f.precision} prefix={f.prefix} suffix={f.suffix} disabled={f.readonly} invalid={!!errors[f.field]} required={f.required && !f.readonly} placeholder={f.placeholder} onChange={(v) => (values[f.field] = fromNumberValue(v))} />
574
+ <SvNumberInput id={`sv-ef-${f.field}`} ariaLabel={f.label} value={toNumberValue(values[f.field])} min={f.min} max={f.max} step={f.step} precision={f.precision} prefix={f.prefix} suffix={f.suffix} disabled={f.readonly} invalid={!!err} required={f.required && !f.readonly} placeholder={f.placeholder} onChange={(v) => (values[f.field] = fromNumberValue(v))} />
451
575
  {:else if f.editorType === 'color'}
452
- <SvColorInput id={`sv-ef-${f.field}`} ariaLabel={f.label} value={values[f.field] || '#3b82f6'} disabled={f.readonly} invalid={!!errors[f.field]} onChange={(v) => (values[f.field] = v)} />
576
+ <SvColorInput id={`sv-ef-${f.field}`} ariaLabel={f.label} value={values[f.field] || '#3b82f6'} disabled={f.readonly} invalid={!!err} onChange={(v) => (values[f.field] = v)} />
453
577
  {:else if f.editorType === 'password'}
454
- <SvPasswordInput id={`sv-ef-${f.field}`} ariaLabel={f.label} value={values[f.field] ?? ''} disabled={f.readonly} invalid={!!errors[f.field]} required={f.required && !f.readonly} placeholder={f.placeholder} onChange={(v) => (values[f.field] = v)} />
578
+ <SvPasswordInput id={`sv-ef-${f.field}`} ariaLabel={f.label} value={values[f.field] ?? ''} disabled={f.readonly} invalid={!!err} required={f.required && !f.readonly} placeholder={f.placeholder} onChange={(v) => (values[f.field] = v)} />
455
579
  {:else if f.editorType === 'rating' || f.editorType === 'slider'}
456
580
  {@const rmin = f.min ?? 0}
457
581
  {@const rmax = f.max ?? (f.editorType === 'rating' ? 5 : 100)}
458
582
  <SvSlider id={`sv-ef-${f.field}`} ariaLabel={f.label} value={toSliderValue(values[f.field], rmin)} min={rmin} max={rmax} step={1} ticks={f.editorType === 'rating' ? rmax - rmin + 1 : undefined} disabled={f.readonly} onChange={(v) => (values[f.field] = v)} />
459
583
  {:else if f.editorType === 'phone'}
460
- <SvPhoneInput id={`sv-ef-${f.field}`} ariaLabel={f.label} value={values[f.field] ?? ''} disabled={f.readonly} invalid={!!errors[f.field]} required={f.required && !f.readonly} placeholder={f.placeholder} onChange={(v) => (values[f.field] = v)} />
584
+ <SvPhoneInput id={`sv-ef-${f.field}`} ariaLabel={f.label} value={values[f.field] ?? ''} disabled={f.readonly} invalid={!!err} required={f.required && !f.readonly} placeholder={f.placeholder} onChange={(v) => (values[f.field] = v)} />
461
585
  {:else if f.editorType === 'country'}
462
- <SvCountryInput id={`sv-ef-${f.field}`} ariaLabel={f.label} value={values[f.field] ?? null} disabled={f.readonly} invalid={!!errors[f.field]} placeholder={f.placeholder} onChange={(v) => (values[f.field] = v)} />
586
+ <SvCountryInput id={`sv-ef-${f.field}`} ariaLabel={f.label} value={values[f.field] ?? null} disabled={f.readonly} invalid={!!err} placeholder={f.placeholder} onChange={(v) => (values[f.field] = v)} />
463
587
  {:else if f.editorType === 'mask'}
464
- <SvMaskedInput id={`sv-ef-${f.field}`} ariaLabel={f.label} value={values[f.field] ?? ''} mask={f.mask ?? ''} disabled={f.readonly} invalid={!!errors[f.field]} required={f.required && !f.readonly} placeholder={f.placeholder} onChange={(masked) => (values[f.field] = masked)} />
588
+ <SvMaskedInput id={`sv-ef-${f.field}`} ariaLabel={f.label} value={values[f.field] ?? ''} mask={f.mask ?? ''} disabled={f.readonly} invalid={!!err} required={f.required && !f.readonly} placeholder={f.placeholder} onChange={(masked) => (values[f.field] = masked)} />
465
589
  {:else if f.editorType === 'date'}
466
- <SvDateTimePicker id={`sv-ef-${f.field}`} ariaLabel={f.label} value={values[f.field] ?? null} dropDownDisplayMode="calendar" formatString="yyyy-MM-dd" nullable disabled={f.readonly} invalid={!!errors[f.field]} required={f.required && !f.readonly} placeholder={f.placeholder ?? 'yyyy-mm-dd'} onChange={(d) => (values[f.field] = toDateString(d))} />
590
+ <SvDateTimePicker id={`sv-ef-${f.field}`} ariaLabel={f.label} value={values[f.field] ?? null} dropDownDisplayMode="calendar" formatString="yyyy-MM-dd" nullable disabled={f.readonly} invalid={!!err} required={f.required && !f.readonly} placeholder={f.placeholder ?? 'yyyy-mm-dd'} onChange={(d) => (values[f.field] = toDateString(d))} />
467
591
  {:else if f.editorType === 'datetime'}
468
- <SvDateTimePicker id={`sv-ef-${f.field}`} ariaLabel={f.label} value={values[f.field] ?? null} formatString="yyyy-MM-dd HH:mm" nullable disabled={f.readonly} invalid={!!errors[f.field]} required={f.required && !f.readonly} placeholder={f.placeholder ?? 'yyyy-mm-dd hh:mm'} onChange={(d) => (values[f.field] = toDateTimeString(d))} />
592
+ <SvDateTimePicker id={`sv-ef-${f.field}`} ariaLabel={f.label} value={values[f.field] ?? null} formatString="yyyy-MM-dd HH:mm" nullable disabled={f.readonly} invalid={!!err} required={f.required && !f.readonly} placeholder={f.placeholder ?? 'yyyy-mm-dd hh:mm'} onChange={(d) => (values[f.field] = toDateTimeString(d))} />
469
593
  {:else if f.editorType === 'chips'}
470
594
  <!-- Multi-value entry: stores a string[]. (Previously a chips field fell back to a single-select.) -->
471
- <SvTagsInput id={`sv-ef-${f.field}`} ariaLabel={f.label} value={toTags(values[f.field])} disabled={f.readonly} invalid={!!errors[f.field]} placeholder={f.placeholder ?? 'Add…'} onChange={(tags) => (values[f.field] = tags)} />
595
+ <SvTagsInput id={`sv-ef-${f.field}`} ariaLabel={f.label} value={toTags(values[f.field])} disabled={f.readonly} invalid={!!err} placeholder={f.placeholder ?? 'Add…'} onChange={(tags) => (values[f.field] = tags)} />
472
596
  {:else if kind === 'select'}
473
597
  {#if f.readonly}
474
- <input id={`sv-ef-${f.field}`} type="text" value={values[f.field] ?? ''} disabled />
598
+ <input id={`sv-ef-${f.field}`} aria-invalid={!!err} aria-describedby={describedBy} type="text" value={values[f.field] ?? ''} disabled />
475
599
  {:else}
476
600
  <!-- Custom dropdown: its panel portals to <body> (position:fixed),
477
601
  so it floats above the modal and never grows the form body
@@ -487,21 +611,21 @@
487
611
  </div>
488
612
  {/if}
489
613
  {:else if kind === 'textarea'}
490
- <textarea id={`sv-ef-${f.field}`} bind:value={values[f.field]} disabled={f.readonly} placeholder={f.placeholder}></textarea>
614
+ <textarea id={`sv-ef-${f.field}`} aria-invalid={!!err} aria-describedby={describedBy} bind:value={values[f.field]} disabled={f.readonly} placeholder={f.placeholder}></textarea>
491
615
  {:else if kind === 'number'}
492
- <input id={`sv-ef-${f.field}`} type="number" bind:value={values[f.field]} disabled={f.readonly} placeholder={f.placeholder} />
616
+ <input id={`sv-ef-${f.field}`} aria-invalid={!!err} aria-describedby={describedBy} type="number" bind:value={values[f.field]} disabled={f.readonly} placeholder={f.placeholder} />
493
617
  {:else if kind === 'date'}
494
- <input id={`sv-ef-${f.field}`} type="date" bind:value={values[f.field]} disabled={f.readonly} />
618
+ <input id={`sv-ef-${f.field}`} aria-invalid={!!err} aria-describedby={describedBy} type="date" bind:value={values[f.field]} disabled={f.readonly} />
495
619
  {:else if kind === 'datetime'}
496
- <input id={`sv-ef-${f.field}`} type="datetime-local" bind:value={values[f.field]} disabled={f.readonly} />
620
+ <input id={`sv-ef-${f.field}`} aria-invalid={!!err} aria-describedby={describedBy} type="datetime-local" bind:value={values[f.field]} disabled={f.readonly} />
497
621
  {:else if kind === 'time'}
498
- <input id={`sv-ef-${f.field}`} type="time" bind:value={values[f.field]} disabled={f.readonly} />
622
+ <input id={`sv-ef-${f.field}`} aria-invalid={!!err} aria-describedby={describedBy} type="time" bind:value={values[f.field]} disabled={f.readonly} />
499
623
  {:else}
500
- <input id={`sv-ef-${f.field}`} type="text" bind:value={values[f.field]} disabled={f.readonly} placeholder={f.placeholder} />
624
+ <input id={`sv-ef-${f.field}`} aria-invalid={!!err} aria-describedby={describedBy} type="text" bind:value={values[f.field]} disabled={f.readonly} placeholder={f.placeholder} />
501
625
  {/if}
502
626
 
503
- {#if f.help && !errors[f.field]}<p class="sv-ep-field__help">{f.help}</p>{/if}
504
- {#if errors[f.field]}<p class="sv-ep-field__err">{errors[f.field]}</p>{/if}
627
+ {#if f.help && !err}<p class="sv-ep-field__help" id={`sv-eh-${f.field}`}>{f.help}</p>{/if}
628
+ {#if err}<p class="sv-ep-field__err" id={`sv-ee-${f.field}`}>{err}</p>{/if}
505
629
  </div>
506
630
  {/snippet}
507
631
 
@@ -726,6 +850,9 @@
726
850
  .sv-ep__section { display: flex; flex-direction: column; }
727
851
  .sv-ep__section + .sv-ep__section { border-top: 1px solid var(--sg-border, #e6e8ec); }
728
852
  .sv-ep__section-title { margin: 0; padding: 14px 18px 0; font-size: 12.5px; font-weight: 700; letter-spacing: 0.02em; text-transform: uppercase; color: var(--sg-muted, #64748b); }
853
+ .sv-ep__section-desc { margin: 4px 0 0; padding: 0 18px; font-size: 12px; color: var(--sg-muted, #64748b); }
854
+ .sv-ep__discard { margin-right: auto; font-size: 12.5px; font-weight: 600; color: var(--ep-danger); }
855
+ .sv-ep__btn--danger { border-color: var(--ep-danger); background: var(--ep-danger); color: #fff; }
729
856
  .sv-ep__section .sv-ep__body { flex: 0 0 auto; overflow: visible; }
730
857
  .sv-ep-field {
731
858
  display: flex;
@@ -807,6 +934,42 @@
807
934
  font-weight: 500;
808
935
  color: var(--ep-danger);
809
936
  }
937
+ .sv-ep__summary {
938
+ margin: 0 18px 12px;
939
+ padding: 10px 12px;
940
+ border: 1px solid var(--ep-danger);
941
+ border-left-width: 3px;
942
+ border-radius: 6px;
943
+ background: color-mix(in srgb, var(--ep-danger) 7%, var(--sg-bg, #fff));
944
+ }
945
+ .sv-ep__summary-title {
946
+ margin: 0 0 6px;
947
+ font-size: 12.5px;
948
+ font-weight: 600;
949
+ color: var(--ep-danger);
950
+ }
951
+ .sv-ep__summary ul {
952
+ margin: 0;
953
+ padding-left: 18px;
954
+ display: flex;
955
+ flex-direction: column;
956
+ gap: 3px;
957
+ }
958
+ .sv-ep__summary li {
959
+ font-size: 12px;
960
+ color: var(--sg-fg, #0f172a);
961
+ }
962
+ .sv-ep__summary button {
963
+ background: none;
964
+ border: 0;
965
+ padding: 0;
966
+ font: inherit;
967
+ color: inherit;
968
+ text-align: left;
969
+ cursor: pointer;
970
+ text-decoration: underline;
971
+ text-underline-offset: 2px;
972
+ }
810
973
  .sv-ep__submit-err {
811
974
  margin: 0;
812
975
  padding: 0 18px 4px;