@ngstarter-ui/components 1.0.21

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 (415) hide show
  1. package/README.md +0 -0
  2. package/fesm2022/ngstarter-ui-components-action-required.mjs +42 -0
  3. package/fesm2022/ngstarter-ui-components-action-required.mjs.map +1 -0
  4. package/fesm2022/ngstarter-ui-components-alert.mjs +132 -0
  5. package/fesm2022/ngstarter-ui-components-alert.mjs.map +1 -0
  6. package/fesm2022/ngstarter-ui-components-announcement.mjs +86 -0
  7. package/fesm2022/ngstarter-ui-components-announcement.mjs.map +1 -0
  8. package/fesm2022/ngstarter-ui-components-autocomplete.mjs +360 -0
  9. package/fesm2022/ngstarter-ui-components-autocomplete.mjs.map +1 -0
  10. package/fesm2022/ngstarter-ui-components-avatar.mjs +235 -0
  11. package/fesm2022/ngstarter-ui-components-avatar.mjs.map +1 -0
  12. package/fesm2022/ngstarter-ui-components-badge.mjs +97 -0
  13. package/fesm2022/ngstarter-ui-components-badge.mjs.map +1 -0
  14. package/fesm2022/ngstarter-ui-components-block-loader.mjs +48 -0
  15. package/fesm2022/ngstarter-ui-components-block-loader.mjs.map +1 -0
  16. package/fesm2022/ngstarter-ui-components-bottom-sheet.mjs +327 -0
  17. package/fesm2022/ngstarter-ui-components-bottom-sheet.mjs.map +1 -0
  18. package/fesm2022/ngstarter-ui-components-breadcrumbs.mjs +209 -0
  19. package/fesm2022/ngstarter-ui-components-breadcrumbs.mjs.map +1 -0
  20. package/fesm2022/ngstarter-ui-components-button-toggle.mjs +175 -0
  21. package/fesm2022/ngstarter-ui-components-button-toggle.mjs.map +1 -0
  22. package/fesm2022/ngstarter-ui-components-button.mjs +70 -0
  23. package/fesm2022/ngstarter-ui-components-button.mjs.map +1 -0
  24. package/fesm2022/ngstarter-ui-components-card-overlay.mjs +49 -0
  25. package/fesm2022/ngstarter-ui-components-card-overlay.mjs.map +1 -0
  26. package/fesm2022/ngstarter-ui-components-card.mjs +199 -0
  27. package/fesm2022/ngstarter-ui-components-card.mjs.map +1 -0
  28. package/fesm2022/ngstarter-ui-components-carousel.mjs +614 -0
  29. package/fesm2022/ngstarter-ui-components-carousel.mjs.map +1 -0
  30. package/fesm2022/ngstarter-ui-components-checkbox.mjs +300 -0
  31. package/fesm2022/ngstarter-ui-components-checkbox.mjs.map +1 -0
  32. package/fesm2022/ngstarter-ui-components-chips.mjs +589 -0
  33. package/fesm2022/ngstarter-ui-components-chips.mjs.map +1 -0
  34. package/fesm2022/ngstarter-ui-components-code-highlighter.mjs +347 -0
  35. package/fesm2022/ngstarter-ui-components-code-highlighter.mjs.map +1 -0
  36. package/fesm2022/ngstarter-ui-components-color-picker.mjs +713 -0
  37. package/fesm2022/ngstarter-ui-components-color-picker.mjs.map +1 -0
  38. package/fesm2022/ngstarter-ui-components-color-scheme.mjs +106 -0
  39. package/fesm2022/ngstarter-ui-components-color-scheme.mjs.map +1 -0
  40. package/fesm2022/ngstarter-ui-components-color-switcher.mjs +72 -0
  41. package/fesm2022/ngstarter-ui-components-color-switcher.mjs.map +1 -0
  42. package/fesm2022/ngstarter-ui-components-command-bar.mjs +57 -0
  43. package/fesm2022/ngstarter-ui-components-command-bar.mjs.map +1 -0
  44. package/fesm2022/ngstarter-ui-components-comment-editor.mjs +1024 -0
  45. package/fesm2022/ngstarter-ui-components-comment-editor.mjs.map +1 -0
  46. package/fesm2022/ngstarter-ui-components-comparison-slider.mjs +177 -0
  47. package/fesm2022/ngstarter-ui-components-comparison-slider.mjs.map +1 -0
  48. package/fesm2022/ngstarter-ui-components-confirm.mjs +85 -0
  49. package/fesm2022/ngstarter-ui-components-confirm.mjs.map +1 -0
  50. package/fesm2022/ngstarter-ui-components-content-editor-code-block.component-Bk6QTli8.mjs +173 -0
  51. package/fesm2022/ngstarter-ui-components-content-editor-code-block.component-Bk6QTli8.mjs.map +1 -0
  52. package/fesm2022/ngstarter-ui-components-content-editor-content-editor-content-editable.directive-Bvfa2dqh.mjs +124 -0
  53. package/fesm2022/ngstarter-ui-components-content-editor-content-editor-content-editable.directive-Bvfa2dqh.mjs.map +1 -0
  54. package/fesm2022/ngstarter-ui-components-content-editor-cursor-controller-4Ak8VqGX.mjs +99 -0
  55. package/fesm2022/ngstarter-ui-components-content-editor-cursor-controller-4Ak8VqGX.mjs.map +1 -0
  56. package/fesm2022/ngstarter-ui-components-content-editor-divider-block.component-C_iRTCPH.mjs +33 -0
  57. package/fesm2022/ngstarter-ui-components-content-editor-divider-block.component-C_iRTCPH.mjs.map +1 -0
  58. package/fesm2022/ngstarter-ui-components-content-editor-embed-block-BbkC_t86.mjs +354 -0
  59. package/fesm2022/ngstarter-ui-components-content-editor-embed-block-BbkC_t86.mjs.map +1 -0
  60. package/fesm2022/ngstarter-ui-components-content-editor-heading-block.component-D9_CxTY1.mjs +114 -0
  61. package/fesm2022/ngstarter-ui-components-content-editor-heading-block.component-D9_CxTY1.mjs.map +1 -0
  62. package/fesm2022/ngstarter-ui-components-content-editor-image-block.component-B4zJyUg1.mjs +146 -0
  63. package/fesm2022/ngstarter-ui-components-content-editor-image-block.component-B4zJyUg1.mjs.map +1 -0
  64. package/fesm2022/ngstarter-ui-components-content-editor-list-block.component-Cv6wx5Xe.mjs +215 -0
  65. package/fesm2022/ngstarter-ui-components-content-editor-list-block.component-Cv6wx5Xe.mjs.map +1 -0
  66. package/fesm2022/ngstarter-ui-components-content-editor-ngstarter-ui-components-content-editor-1Zi2nAX5.mjs +2548 -0
  67. package/fesm2022/ngstarter-ui-components-content-editor-ngstarter-ui-components-content-editor-1Zi2nAX5.mjs.map +1 -0
  68. package/fesm2022/ngstarter-ui-components-content-editor-paragraph-block.component-C9bQvDYU.mjs +110 -0
  69. package/fesm2022/ngstarter-ui-components-content-editor-paragraph-block.component-C9bQvDYU.mjs.map +1 -0
  70. package/fesm2022/ngstarter-ui-components-content-editor-quote-block.component-BbHds2r2.mjs +141 -0
  71. package/fesm2022/ngstarter-ui-components-content-editor-quote-block.component-BbHds2r2.mjs.map +1 -0
  72. package/fesm2022/ngstarter-ui-components-content-editor-table-block.component-DlDh7Fnn.mjs +1604 -0
  73. package/fesm2022/ngstarter-ui-components-content-editor-table-block.component-DlDh7Fnn.mjs.map +1 -0
  74. package/fesm2022/ngstarter-ui-components-content-editor-video-block.component-m4DTihP2.mjs +175 -0
  75. package/fesm2022/ngstarter-ui-components-content-editor-video-block.component-m4DTihP2.mjs.map +1 -0
  76. package/fesm2022/ngstarter-ui-components-content-editor.mjs +2 -0
  77. package/fesm2022/ngstarter-ui-components-content-editor.mjs.map +1 -0
  78. package/fesm2022/ngstarter-ui-components-content-fade.mjs +35 -0
  79. package/fesm2022/ngstarter-ui-components-content-fade.mjs.map +1 -0
  80. package/fesm2022/ngstarter-ui-components-cookie-popup.mjs +107 -0
  81. package/fesm2022/ngstarter-ui-components-cookie-popup.mjs.map +1 -0
  82. package/fesm2022/ngstarter-ui-components-core.mjs +1330 -0
  83. package/fesm2022/ngstarter-ui-components-core.mjs.map +1 -0
  84. package/fesm2022/ngstarter-ui-components-country-select.mjs +489 -0
  85. package/fesm2022/ngstarter-ui-components-country-select.mjs.map +1 -0
  86. package/fesm2022/ngstarter-ui-components-crop.mjs +183 -0
  87. package/fesm2022/ngstarter-ui-components-crop.mjs.map +1 -0
  88. package/fesm2022/ngstarter-ui-components-currency-select.mjs +397 -0
  89. package/fesm2022/ngstarter-ui-components-currency-select.mjs.map +1 -0
  90. package/fesm2022/ngstarter-ui-components-data-view.mjs +1494 -0
  91. package/fesm2022/ngstarter-ui-components-data-view.mjs.map +1 -0
  92. package/fesm2022/ngstarter-ui-components-date-format-select.mjs +154 -0
  93. package/fesm2022/ngstarter-ui-components-date-format-select.mjs.map +1 -0
  94. package/fesm2022/ngstarter-ui-components-datepicker.mjs +1159 -0
  95. package/fesm2022/ngstarter-ui-components-datepicker.mjs.map +1 -0
  96. package/fesm2022/ngstarter-ui-components-dialog.mjs +357 -0
  97. package/fesm2022/ngstarter-ui-components-dialog.mjs.map +1 -0
  98. package/fesm2022/ngstarter-ui-components-divider.mjs +42 -0
  99. package/fesm2022/ngstarter-ui-components-divider.mjs.map +1 -0
  100. package/fesm2022/ngstarter-ui-components-drawer.mjs +132 -0
  101. package/fesm2022/ngstarter-ui-components-drawer.mjs.map +1 -0
  102. package/fesm2022/ngstarter-ui-components-emoji-picker.mjs +245 -0
  103. package/fesm2022/ngstarter-ui-components-emoji-picker.mjs.map +1 -0
  104. package/fesm2022/ngstarter-ui-components-empty-state.mjs +75 -0
  105. package/fesm2022/ngstarter-ui-components-empty-state.mjs.map +1 -0
  106. package/fesm2022/ngstarter-ui-components-expand.mjs +56 -0
  107. package/fesm2022/ngstarter-ui-components-expand.mjs.map +1 -0
  108. package/fesm2022/ngstarter-ui-components-expansion.mjs +193 -0
  109. package/fesm2022/ngstarter-ui-components-expansion.mjs.map +1 -0
  110. package/fesm2022/ngstarter-ui-components-filter-builder.mjs +333 -0
  111. package/fesm2022/ngstarter-ui-components-filter-builder.mjs.map +1 -0
  112. package/fesm2022/ngstarter-ui-components-form-field.mjs +230 -0
  113. package/fesm2022/ngstarter-ui-components-form-field.mjs.map +1 -0
  114. package/fesm2022/ngstarter-ui-components-form-renderer-autocomplete-many-field-BKQVlZHV.mjs +124 -0
  115. package/fesm2022/ngstarter-ui-components-form-renderer-autocomplete-many-field-BKQVlZHV.mjs.map +1 -0
  116. package/fesm2022/ngstarter-ui-components-form-renderer-checkbox-field-CoyKdvhV.mjs +22 -0
  117. package/fesm2022/ngstarter-ui-components-form-renderer-checkbox-field-CoyKdvhV.mjs.map +1 -0
  118. package/fesm2022/ngstarter-ui-components-form-renderer-datepicker-field-Bzc0TPO9.mjs +44 -0
  119. package/fesm2022/ngstarter-ui-components-form-renderer-datepicker-field-Bzc0TPO9.mjs.map +1 -0
  120. package/fesm2022/ngstarter-ui-components-form-renderer-divider-content-CwGzDCZv.mjs +17 -0
  121. package/fesm2022/ngstarter-ui-components-form-renderer-divider-content-CwGzDCZv.mjs.map +1 -0
  122. package/fesm2022/ngstarter-ui-components-form-renderer-image-content-ICTwkZPa.mjs +17 -0
  123. package/fesm2022/ngstarter-ui-components-form-renderer-image-content-ICTwkZPa.mjs.map +1 -0
  124. package/fesm2022/ngstarter-ui-components-form-renderer-input-field-RYxi-Mpw.mjs +35 -0
  125. package/fesm2022/ngstarter-ui-components-form-renderer-input-field-RYxi-Mpw.mjs.map +1 -0
  126. package/fesm2022/ngstarter-ui-components-form-renderer-radio-group-field-Cv3AGpoq.mjs +38 -0
  127. package/fesm2022/ngstarter-ui-components-form-renderer-radio-group-field-Cv3AGpoq.mjs.map +1 -0
  128. package/fesm2022/ngstarter-ui-components-form-renderer-select-field-eLcwI-BY.mjs +39 -0
  129. package/fesm2022/ngstarter-ui-components-form-renderer-select-field-eLcwI-BY.mjs.map +1 -0
  130. package/fesm2022/ngstarter-ui-components-form-renderer-text-content-BjzH_M3-.mjs +24 -0
  131. package/fesm2022/ngstarter-ui-components-form-renderer-text-content-BjzH_M3-.mjs.map +1 -0
  132. package/fesm2022/ngstarter-ui-components-form-renderer-textarea-field-4zH7FTQ1.mjs +37 -0
  133. package/fesm2022/ngstarter-ui-components-form-renderer-textarea-field-4zH7FTQ1.mjs.map +1 -0
  134. package/fesm2022/ngstarter-ui-components-form-renderer-timezone-field-BpH65Hd-.mjs +35 -0
  135. package/fesm2022/ngstarter-ui-components-form-renderer-timezone-field-BpH65Hd-.mjs.map +1 -0
  136. package/fesm2022/ngstarter-ui-components-form-renderer-toggle-field-iyqUrWxt.mjs +22 -0
  137. package/fesm2022/ngstarter-ui-components-form-renderer-toggle-field-iyqUrWxt.mjs.map +1 -0
  138. package/fesm2022/ngstarter-ui-components-form-renderer.mjs +317 -0
  139. package/fesm2022/ngstarter-ui-components-form-renderer.mjs.map +1 -0
  140. package/fesm2022/ngstarter-ui-components-gauge.mjs +44 -0
  141. package/fesm2022/ngstarter-ui-components-gauge.mjs.map +1 -0
  142. package/fesm2022/ngstarter-ui-components-grid.mjs +78 -0
  143. package/fesm2022/ngstarter-ui-components-grid.mjs.map +1 -0
  144. package/fesm2022/ngstarter-ui-components-guided-tour.mjs +736 -0
  145. package/fesm2022/ngstarter-ui-components-guided-tour.mjs.map +1 -0
  146. package/fesm2022/ngstarter-ui-components-headless-stepper.mjs +192 -0
  147. package/fesm2022/ngstarter-ui-components-headless-stepper.mjs.map +1 -0
  148. package/fesm2022/ngstarter-ui-components-icon.mjs +61 -0
  149. package/fesm2022/ngstarter-ui-components-icon.mjs.map +1 -0
  150. package/fesm2022/ngstarter-ui-components-image-designer.mjs +4016 -0
  151. package/fesm2022/ngstarter-ui-components-image-designer.mjs.map +1 -0
  152. package/fesm2022/ngstarter-ui-components-image-placeholder.mjs +20 -0
  153. package/fesm2022/ngstarter-ui-components-image-placeholder.mjs.map +1 -0
  154. package/fesm2022/ngstarter-ui-components-image-resizer.mjs +151 -0
  155. package/fesm2022/ngstarter-ui-components-image-resizer.mjs.map +1 -0
  156. package/fesm2022/ngstarter-ui-components-image-viewer.mjs +349 -0
  157. package/fesm2022/ngstarter-ui-components-image-viewer.mjs.map +1 -0
  158. package/fesm2022/ngstarter-ui-components-image-zoom-viewer.mjs +162 -0
  159. package/fesm2022/ngstarter-ui-components-image-zoom-viewer.mjs.map +1 -0
  160. package/fesm2022/ngstarter-ui-components-incidents.mjs +257 -0
  161. package/fesm2022/ngstarter-ui-components-incidents.mjs.map +1 -0
  162. package/fesm2022/ngstarter-ui-components-inline-text-edit.mjs +179 -0
  163. package/fesm2022/ngstarter-ui-components-inline-text-edit.mjs.map +1 -0
  164. package/fesm2022/ngstarter-ui-components-input-mask.mjs +180 -0
  165. package/fesm2022/ngstarter-ui-components-input-mask.mjs.map +1 -0
  166. package/fesm2022/ngstarter-ui-components-input-validator.mjs +24 -0
  167. package/fesm2022/ngstarter-ui-components-input-validator.mjs.map +1 -0
  168. package/fesm2022/ngstarter-ui-components-input.mjs +152 -0
  169. package/fesm2022/ngstarter-ui-components-input.mjs.map +1 -0
  170. package/fesm2022/ngstarter-ui-components-kanban-board.mjs +156 -0
  171. package/fesm2022/ngstarter-ui-components-kanban-board.mjs.map +1 -0
  172. package/fesm2022/ngstarter-ui-components-kbd.mjs +31 -0
  173. package/fesm2022/ngstarter-ui-components-kbd.mjs.map +1 -0
  174. package/fesm2022/ngstarter-ui-components-layout.mjs +199 -0
  175. package/fesm2022/ngstarter-ui-components-layout.mjs.map +1 -0
  176. package/fesm2022/ngstarter-ui-components-list.mjs +279 -0
  177. package/fesm2022/ngstarter-ui-components-list.mjs.map +1 -0
  178. package/fesm2022/ngstarter-ui-components-logo.mjs +51 -0
  179. package/fesm2022/ngstarter-ui-components-logo.mjs.map +1 -0
  180. package/fesm2022/ngstarter-ui-components-marquee.mjs +76 -0
  181. package/fesm2022/ngstarter-ui-components-marquee.mjs.map +1 -0
  182. package/fesm2022/ngstarter-ui-components-menu.mjs +851 -0
  183. package/fesm2022/ngstarter-ui-components-menu.mjs.map +1 -0
  184. package/fesm2022/ngstarter-ui-components-micro-chart.mjs +928 -0
  185. package/fesm2022/ngstarter-ui-components-micro-chart.mjs.map +1 -0
  186. package/fesm2022/ngstarter-ui-components-navigation.mjs +439 -0
  187. package/fesm2022/ngstarter-ui-components-navigation.mjs.map +1 -0
  188. package/fesm2022/ngstarter-ui-components-notifications.mjs +181 -0
  189. package/fesm2022/ngstarter-ui-components-notifications.mjs.map +1 -0
  190. package/fesm2022/ngstarter-ui-components-number-input.mjs +293 -0
  191. package/fesm2022/ngstarter-ui-components-number-input.mjs.map +1 -0
  192. package/fesm2022/ngstarter-ui-components-option.mjs +157 -0
  193. package/fesm2022/ngstarter-ui-components-option.mjs.map +1 -0
  194. package/fesm2022/ngstarter-ui-components-overlay.mjs +112 -0
  195. package/fesm2022/ngstarter-ui-components-overlay.mjs.map +1 -0
  196. package/fesm2022/ngstarter-ui-components-page-loading-bar.mjs +77 -0
  197. package/fesm2022/ngstarter-ui-components-page-loading-bar.mjs.map +1 -0
  198. package/fesm2022/ngstarter-ui-components-paginator.mjs +297 -0
  199. package/fesm2022/ngstarter-ui-components-paginator.mjs.map +1 -0
  200. package/fesm2022/ngstarter-ui-components-panel.mjs +123 -0
  201. package/fesm2022/ngstarter-ui-components-panel.mjs.map +1 -0
  202. package/fesm2022/ngstarter-ui-components-password-strength.mjs +335 -0
  203. package/fesm2022/ngstarter-ui-components-password-strength.mjs.map +1 -0
  204. package/fesm2022/ngstarter-ui-components-phone-input.mjs +651 -0
  205. package/fesm2022/ngstarter-ui-components-phone-input.mjs.map +1 -0
  206. package/fesm2022/ngstarter-ui-components-pin-input.mjs +193 -0
  207. package/fesm2022/ngstarter-ui-components-pin-input.mjs.map +1 -0
  208. package/fesm2022/ngstarter-ui-components-popover.mjs +302 -0
  209. package/fesm2022/ngstarter-ui-components-popover.mjs.map +1 -0
  210. package/fesm2022/ngstarter-ui-components-progress-bar.mjs +68 -0
  211. package/fesm2022/ngstarter-ui-components-progress-bar.mjs.map +1 -0
  212. package/fesm2022/ngstarter-ui-components-radio-card.mjs +102 -0
  213. package/fesm2022/ngstarter-ui-components-radio-card.mjs.map +1 -0
  214. package/fesm2022/ngstarter-ui-components-radio.mjs +147 -0
  215. package/fesm2022/ngstarter-ui-components-radio.mjs.map +1 -0
  216. package/fesm2022/ngstarter-ui-components-rail-nav.mjs +87 -0
  217. package/fesm2022/ngstarter-ui-components-rail-nav.mjs.map +1 -0
  218. package/fesm2022/ngstarter-ui-components-resizable-container.mjs +74 -0
  219. package/fesm2022/ngstarter-ui-components-resizable-container.mjs.map +1 -0
  220. package/fesm2022/ngstarter-ui-components-screen-loader.mjs +95 -0
  221. package/fesm2022/ngstarter-ui-components-screen-loader.mjs.map +1 -0
  222. package/fesm2022/ngstarter-ui-components-scroll-spy.mjs +219 -0
  223. package/fesm2022/ngstarter-ui-components-scroll-spy.mjs.map +1 -0
  224. package/fesm2022/ngstarter-ui-components-scrollbar-area.mjs +459 -0
  225. package/fesm2022/ngstarter-ui-components-scrollbar-area.mjs.map +1 -0
  226. package/fesm2022/ngstarter-ui-components-segmented.mjs +218 -0
  227. package/fesm2022/ngstarter-ui-components-segmented.mjs.map +1 -0
  228. package/fesm2022/ngstarter-ui-components-select.mjs +496 -0
  229. package/fesm2022/ngstarter-ui-components-select.mjs.map +1 -0
  230. package/fesm2022/ngstarter-ui-components-side-panel.mjs +107 -0
  231. package/fesm2022/ngstarter-ui-components-side-panel.mjs.map +1 -0
  232. package/fesm2022/ngstarter-ui-components-sidebar.mjs +435 -0
  233. package/fesm2022/ngstarter-ui-components-sidebar.mjs.map +1 -0
  234. package/fesm2022/ngstarter-ui-components-sidenav.mjs +354 -0
  235. package/fesm2022/ngstarter-ui-components-sidenav.mjs.map +1 -0
  236. package/fesm2022/ngstarter-ui-components-signature-pad.mjs +452 -0
  237. package/fesm2022/ngstarter-ui-components-signature-pad.mjs.map +1 -0
  238. package/fesm2022/ngstarter-ui-components-skeleton.mjs +22 -0
  239. package/fesm2022/ngstarter-ui-components-skeleton.mjs.map +1 -0
  240. package/fesm2022/ngstarter-ui-components-slide-toggle.mjs +93 -0
  241. package/fesm2022/ngstarter-ui-components-slide-toggle.mjs.map +1 -0
  242. package/fesm2022/ngstarter-ui-components-slider.mjs +481 -0
  243. package/fesm2022/ngstarter-ui-components-slider.mjs.map +1 -0
  244. package/fesm2022/ngstarter-ui-components-snack-bar.mjs +354 -0
  245. package/fesm2022/ngstarter-ui-components-snack-bar.mjs.map +1 -0
  246. package/fesm2022/ngstarter-ui-components-sort.mjs +140 -0
  247. package/fesm2022/ngstarter-ui-components-sort.mjs.map +1 -0
  248. package/fesm2022/ngstarter-ui-components-spinner.mjs +75 -0
  249. package/fesm2022/ngstarter-ui-components-spinner.mjs.map +1 -0
  250. package/fesm2022/ngstarter-ui-components-splash-screen.mjs +93 -0
  251. package/fesm2022/ngstarter-ui-components-splash-screen.mjs.map +1 -0
  252. package/fesm2022/ngstarter-ui-components-split.mjs +948 -0
  253. package/fesm2022/ngstarter-ui-components-split.mjs.map +1 -0
  254. package/fesm2022/ngstarter-ui-components-stepper.mjs +103 -0
  255. package/fesm2022/ngstarter-ui-components-stepper.mjs.map +1 -0
  256. package/fesm2022/ngstarter-ui-components-suggestions.mjs +72 -0
  257. package/fesm2022/ngstarter-ui-components-suggestions.mjs.map +1 -0
  258. package/fesm2022/ngstarter-ui-components-tab-panel.mjs +265 -0
  259. package/fesm2022/ngstarter-ui-components-tab-panel.mjs.map +1 -0
  260. package/fesm2022/ngstarter-ui-components-table.mjs +648 -0
  261. package/fesm2022/ngstarter-ui-components-table.mjs.map +1 -0
  262. package/fesm2022/ngstarter-ui-components-tabs.mjs +591 -0
  263. package/fesm2022/ngstarter-ui-components-tabs.mjs.map +1 -0
  264. package/fesm2022/ngstarter-ui-components-text-editor.mjs +1012 -0
  265. package/fesm2022/ngstarter-ui-components-text-editor.mjs.map +1 -0
  266. package/fesm2022/ngstarter-ui-components-thumbnail-maker.mjs +212 -0
  267. package/fesm2022/ngstarter-ui-components-thumbnail-maker.mjs.map +1 -0
  268. package/fesm2022/ngstarter-ui-components-tiles.mjs +634 -0
  269. package/fesm2022/ngstarter-ui-components-tiles.mjs.map +1 -0
  270. package/fesm2022/ngstarter-ui-components-timeline.mjs +122 -0
  271. package/fesm2022/ngstarter-ui-components-timeline.mjs.map +1 -0
  272. package/fesm2022/ngstarter-ui-components-timepicker.mjs +486 -0
  273. package/fesm2022/ngstarter-ui-components-timepicker.mjs.map +1 -0
  274. package/fesm2022/ngstarter-ui-components-timezone-select.mjs +371 -0
  275. package/fesm2022/ngstarter-ui-components-timezone-select.mjs.map +1 -0
  276. package/fesm2022/ngstarter-ui-components-toolbar.mjs +299 -0
  277. package/fesm2022/ngstarter-ui-components-toolbar.mjs.map +1 -0
  278. package/fesm2022/ngstarter-ui-components-tooltip.mjs +506 -0
  279. package/fesm2022/ngstarter-ui-components-tooltip.mjs.map +1 -0
  280. package/fesm2022/ngstarter-ui-components-tree.mjs +200 -0
  281. package/fesm2022/ngstarter-ui-components-tree.mjs.map +1 -0
  282. package/fesm2022/ngstarter-ui-components-upload.mjs +330 -0
  283. package/fesm2022/ngstarter-ui-components-upload.mjs.map +1 -0
  284. package/fesm2022/ngstarter-ui-components-video-player.mjs +516 -0
  285. package/fesm2022/ngstarter-ui-components-video-player.mjs.map +1 -0
  286. package/fesm2022/ngstarter-ui-components-video-viewer.mjs +218 -0
  287. package/fesm2022/ngstarter-ui-components-video-viewer.mjs.map +1 -0
  288. package/fesm2022/ngstarter-ui-components-visual-builder.mjs +18 -0
  289. package/fesm2022/ngstarter-ui-components-visual-builder.mjs.map +1 -0
  290. package/fesm2022/ngstarter-ui-components.mjs +6 -0
  291. package/fesm2022/ngstarter-ui-components.mjs.map +1 -0
  292. package/package.json +535 -0
  293. package/styles/_common.scss +456 -0
  294. package/styles/_global.scss +91 -0
  295. package/styles/themes/default.scss +2 -0
  296. package/types/ngstarter-ui-components-action-required.d.ts +14 -0
  297. package/types/ngstarter-ui-components-alert.d.ts +50 -0
  298. package/types/ngstarter-ui-components-announcement.d.ts +59 -0
  299. package/types/ngstarter-ui-components-autocomplete.d.ts +83 -0
  300. package/types/ngstarter-ui-components-avatar.d.ts +69 -0
  301. package/types/ngstarter-ui-components-badge.d.ts +38 -0
  302. package/types/ngstarter-ui-components-block-loader.d.ts +21 -0
  303. package/types/ngstarter-ui-components-bottom-sheet.d.ts +149 -0
  304. package/types/ngstarter-ui-components-breadcrumbs.d.ts +104 -0
  305. package/types/ngstarter-ui-components-button-toggle.d.ts +54 -0
  306. package/types/ngstarter-ui-components-button.d.ts +27 -0
  307. package/types/ngstarter-ui-components-card-overlay.d.ts +20 -0
  308. package/types/ngstarter-ui-components-card.d.ts +85 -0
  309. package/types/ngstarter-ui-components-carousel.d.ts +76 -0
  310. package/types/ngstarter-ui-components-checkbox.d.ts +94 -0
  311. package/types/ngstarter-ui-components-chips.d.ts +189 -0
  312. package/types/ngstarter-ui-components-code-highlighter.d.ts +28 -0
  313. package/types/ngstarter-ui-components-color-picker.d.ts +92 -0
  314. package/types/ngstarter-ui-components-color-scheme.d.ts +44 -0
  315. package/types/ngstarter-ui-components-color-switcher.d.ts +26 -0
  316. package/types/ngstarter-ui-components-command-bar.d.ts +28 -0
  317. package/types/ngstarter-ui-components-comment-editor.d.ts +194 -0
  318. package/types/ngstarter-ui-components-comparison-slider.d.ts +42 -0
  319. package/types/ngstarter-ui-components-confirm.d.ts +34 -0
  320. package/types/ngstarter-ui-components-content-editor.d.ts +321 -0
  321. package/types/ngstarter-ui-components-content-fade.d.ts +17 -0
  322. package/types/ngstarter-ui-components-cookie-popup.d.ts +41 -0
  323. package/types/ngstarter-ui-components-core.d.ts +421 -0
  324. package/types/ngstarter-ui-components-country-select.d.ts +78 -0
  325. package/types/ngstarter-ui-components-crop.d.ts +59 -0
  326. package/types/ngstarter-ui-components-currency-select.d.ts +82 -0
  327. package/types/ngstarter-ui-components-data-view.d.ts +391 -0
  328. package/types/ngstarter-ui-components-date-format-select.d.ts +59 -0
  329. package/types/ngstarter-ui-components-datepicker.d.ts +384 -0
  330. package/types/ngstarter-ui-components-dialog.d.ts +115 -0
  331. package/types/ngstarter-ui-components-divider.d.ts +18 -0
  332. package/types/ngstarter-ui-components-drawer.d.ts +32 -0
  333. package/types/ngstarter-ui-components-emoji-picker.d.ts +49 -0
  334. package/types/ngstarter-ui-components-empty-state.d.ts +33 -0
  335. package/types/ngstarter-ui-components-expand.d.ts +26 -0
  336. package/types/ngstarter-ui-components-expansion.d.ts +68 -0
  337. package/types/ngstarter-ui-components-filter-builder.d.ts +106 -0
  338. package/types/ngstarter-ui-components-form-field.d.ts +107 -0
  339. package/types/ngstarter-ui-components-form-renderer.d.ts +121 -0
  340. package/types/ngstarter-ui-components-gauge.d.ts +21 -0
  341. package/types/ngstarter-ui-components-grid.d.ts +45 -0
  342. package/types/ngstarter-ui-components-guided-tour.d.ts +227 -0
  343. package/types/ngstarter-ui-components-headless-stepper.d.ts +65 -0
  344. package/types/ngstarter-ui-components-icon.d.ts +17 -0
  345. package/types/ngstarter-ui-components-image-designer.d.ts +357 -0
  346. package/types/ngstarter-ui-components-image-placeholder.d.ts +8 -0
  347. package/types/ngstarter-ui-components-image-resizer.d.ts +35 -0
  348. package/types/ngstarter-ui-components-image-viewer.d.ts +63 -0
  349. package/types/ngstarter-ui-components-image-zoom-viewer.d.ts +34 -0
  350. package/types/ngstarter-ui-components-incidents.d.ts +119 -0
  351. package/types/ngstarter-ui-components-inline-text-edit.d.ts +39 -0
  352. package/types/ngstarter-ui-components-input-mask.d.ts +36 -0
  353. package/types/ngstarter-ui-components-input-validator.d.ts +5 -0
  354. package/types/ngstarter-ui-components-input.d.ts +53 -0
  355. package/types/ngstarter-ui-components-kanban-board.d.ts +68 -0
  356. package/types/ngstarter-ui-components-kbd.d.ts +13 -0
  357. package/types/ngstarter-ui-components-layout.d.ts +83 -0
  358. package/types/ngstarter-ui-components-list.d.ts +98 -0
  359. package/types/ngstarter-ui-components-logo.d.ts +26 -0
  360. package/types/ngstarter-ui-components-marquee.d.ts +27 -0
  361. package/types/ngstarter-ui-components-menu.d.ts +199 -0
  362. package/types/ngstarter-ui-components-micro-chart.d.ts +195 -0
  363. package/types/ngstarter-ui-components-navigation.d.ts +136 -0
  364. package/types/ngstarter-ui-components-notifications.d.ts +84 -0
  365. package/types/ngstarter-ui-components-number-input.d.ts +99 -0
  366. package/types/ngstarter-ui-components-option.d.ts +61 -0
  367. package/types/ngstarter-ui-components-overlay.d.ts +12 -0
  368. package/types/ngstarter-ui-components-page-loading-bar.d.ts +20 -0
  369. package/types/ngstarter-ui-components-paginator.d.ts +145 -0
  370. package/types/ngstarter-ui-components-panel.d.ts +59 -0
  371. package/types/ngstarter-ui-components-password-strength.d.ts +109 -0
  372. package/types/ngstarter-ui-components-phone-input.d.ts +103 -0
  373. package/types/ngstarter-ui-components-pin-input.d.ts +48 -0
  374. package/types/ngstarter-ui-components-popover.d.ts +94 -0
  375. package/types/ngstarter-ui-components-progress-bar.d.ts +30 -0
  376. package/types/ngstarter-ui-components-radio-card.d.ts +37 -0
  377. package/types/ngstarter-ui-components-radio.d.ts +45 -0
  378. package/types/ngstarter-ui-components-rail-nav.d.ts +36 -0
  379. package/types/ngstarter-ui-components-resizable-container.d.ts +25 -0
  380. package/types/ngstarter-ui-components-screen-loader.d.ts +34 -0
  381. package/types/ngstarter-ui-components-scroll-spy.d.ts +63 -0
  382. package/types/ngstarter-ui-components-scrollbar-area.d.ts +67 -0
  383. package/types/ngstarter-ui-components-segmented.d.ts +65 -0
  384. package/types/ngstarter-ui-components-select.d.ts +126 -0
  385. package/types/ngstarter-ui-components-side-panel.d.ts +42 -0
  386. package/types/ngstarter-ui-components-sidebar.d.ts +143 -0
  387. package/types/ngstarter-ui-components-sidenav.d.ts +86 -0
  388. package/types/ngstarter-ui-components-signature-pad.d.ts +49 -0
  389. package/types/ngstarter-ui-components-skeleton.d.ts +9 -0
  390. package/types/ngstarter-ui-components-slide-toggle.d.ts +41 -0
  391. package/types/ngstarter-ui-components-slider.d.ts +85 -0
  392. package/types/ngstarter-ui-components-snack-bar.d.ts +142 -0
  393. package/types/ngstarter-ui-components-sort.d.ts +66 -0
  394. package/types/ngstarter-ui-components-spinner.d.ts +28 -0
  395. package/types/ngstarter-ui-components-splash-screen.d.ts +31 -0
  396. package/types/ngstarter-ui-components-split.d.ts +210 -0
  397. package/types/ngstarter-ui-components-stepper.d.ts +44 -0
  398. package/types/ngstarter-ui-components-suggestions.d.ts +32 -0
  399. package/types/ngstarter-ui-components-tab-panel.d.ts +96 -0
  400. package/types/ngstarter-ui-components-table.d.ts +277 -0
  401. package/types/ngstarter-ui-components-tabs.d.ts +145 -0
  402. package/types/ngstarter-ui-components-text-editor.d.ts +191 -0
  403. package/types/ngstarter-ui-components-thumbnail-maker.d.ts +35 -0
  404. package/types/ngstarter-ui-components-tiles.d.ts +109 -0
  405. package/types/ngstarter-ui-components-timeline.d.ts +57 -0
  406. package/types/ngstarter-ui-components-timepicker.d.ts +115 -0
  407. package/types/ngstarter-ui-components-timezone-select.d.ts +75 -0
  408. package/types/ngstarter-ui-components-toolbar.d.ts +74 -0
  409. package/types/ngstarter-ui-components-tooltip.d.ts +52 -0
  410. package/types/ngstarter-ui-components-tree.d.ts +60 -0
  411. package/types/ngstarter-ui-components-upload.d.ts +134 -0
  412. package/types/ngstarter-ui-components-video-player.d.ts +67 -0
  413. package/types/ngstarter-ui-components-video-viewer.d.ts +98 -0
  414. package/types/ngstarter-ui-components-visual-builder.d.ts +8 -0
  415. package/types/ngstarter-ui-components.d.ts +2 -0
@@ -0,0 +1,4016 @@
1
+ import { moveItemInArray, CdkDropList, CdkDrag, CdkDragPlaceholder } from '@angular/cdk/drag-drop';
2
+ import * as i0 from '@angular/core';
3
+ import { signal, inject, PLATFORM_ID, Injectable, InjectionToken, computed, ChangeDetectionStrategy, Component, DestroyRef, viewChild, input, booleanAttribute, output, numberAttribute, effect, untracked, forwardRef } from '@angular/core';
4
+ import * as i1 from '@angular/common';
5
+ import { isPlatformBrowser, CommonModule, DecimalPipe } from '@angular/common';
6
+ import { HttpClient } from '@angular/common/http';
7
+ import { Subject, map, isObservable } from 'rxjs';
8
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
9
+ import { Panel, PanelHeader, PanelContent, PanelSidebar } from '@ngstarter-ui/components/panel';
10
+ import { Toolbar, ToolbarItem, ToolbarSpacer, ToolbarTitle } from '@ngstarter-ui/components/toolbar';
11
+ import { Button } from '@ngstarter-ui/components/button';
12
+ import { Divider } from '@ngstarter-ui/components/divider';
13
+ import { Icon } from '@ngstarter-ui/components/icon';
14
+ import { TabPanel, TabPanelAside, TabPanelAsideContentDirective, TabPanelContent, TabPanelItem, TabPanelItemIconDirective, TabPanelItemText, TabPanelNav } from '@ngstarter-ui/components/tab-panel';
15
+ import { UploadTriggerDirective } from '@ngstarter-ui/components/upload';
16
+ import { List, ListItem, ListItemLine } from '@ngstarter-ui/components/list';
17
+ import { ProgressSpinner } from '@ngstarter-ui/components/spinner';
18
+ import Konva from 'konva';
19
+ import * as i1$1 from '@angular/forms';
20
+ import { FormsModule } from '@angular/forms';
21
+ import { TabGroup, Tab } from '@ngstarter-ui/components/tabs';
22
+ import { ColorSwitcher } from '@ngstarter-ui/components/color-switcher';
23
+ import { Accordion, ExpansionPanel, ExpansionPanelHeader, ExpansionPanelTitle } from '@ngstarter-ui/components/expansion';
24
+ import { FormField, Label } from '@ngstarter-ui/components/form-field';
25
+ import { Input } from '@ngstarter-ui/components/input';
26
+ import { Select, Option } from '@ngstarter-ui/components/select';
27
+ import { ScrollbarArea } from '@ngstarter-ui/components/scrollbar-area';
28
+ import { ComponentPortal, CdkPortalOutlet } from '@angular/cdk/portal';
29
+ import { Slider, SliderThumb } from '@ngstarter-ui/components/slider';
30
+ import { Popover, PopoverContent, PopoverTriggerForDirective } from '@ngstarter-ui/components/popover';
31
+ import { Menu, MenuItem, MenuTrigger } from '@ngstarter-ui/components/menu';
32
+ import { ButtonToggle, ButtonToggleGroup } from '@ngstarter-ui/components/button-toggle';
33
+ import { Ripple } from '@ngstarter-ui/components/core';
34
+ import { SlideToggle } from '@ngstarter-ui/components/slide-toggle';
35
+ import { ColorPicker, ColorPickerThumbnail, ColorPickerTriggerForDirective } from '@ngstarter-ui/components/color-picker';
36
+
37
+ /**
38
+ * A custom Konva filter for color tinting.
39
+ * It modifies existing pixel colors by a factor without completely replacing them.
40
+ * Factors are between -1 and 1.
41
+ */
42
+ const TintFilter = function (imageData) {
43
+ const data = imageData.data;
44
+ const n = data.length;
45
+ const tintR = this.getAttr('tintR') || 0;
46
+ const tintG = this.getAttr('tintG') || 0;
47
+ const tintB = this.getAttr('tintB') || 0;
48
+ if (tintR === 0 && tintG === 0 && tintB === 0)
49
+ return;
50
+ for (let i = 0; i < n; i += 4) {
51
+ if (tintR > 0)
52
+ data[i] += (255 - data[i]) * tintR;
53
+ else if (tintR < 0)
54
+ data[i] += data[i] * tintR;
55
+ if (tintG > 0)
56
+ data[i + 1] += (255 - data[i + 1]) * tintG;
57
+ else if (tintG < 0)
58
+ data[i + 1] += data[i + 1] * tintG;
59
+ if (tintB > 0)
60
+ data[i + 2] += (255 - data[i + 2]) * tintB;
61
+ else if (tintB < 0)
62
+ data[i + 2] += data[i + 2] * tintB;
63
+ }
64
+ };
65
+ class ImageDesignerService {
66
+ stage;
67
+ workspaceLayer;
68
+ uiLayer;
69
+ canvasRect;
70
+ maskOverlay;
71
+ resizeObserver;
72
+ minScale = 0.1;
73
+ maxScale = 5;
74
+ scale = signal(1, ...(ngDevMode ? [{ debugName: "scale" }] : /* istanbul ignore next */ []));
75
+ isInitialized = signal(false, ...(ngDevMode ? [{ debugName: "isInitialized" }] : /* istanbul ignore next */ []));
76
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
77
+ loadedFonts = new Set();
78
+ fonts = signal([
79
+ 'Inter',
80
+ 'Roboto',
81
+ 'Open Sans',
82
+ 'Lato',
83
+ 'Montserrat',
84
+ 'Oswald',
85
+ 'Source Sans Pro',
86
+ 'Slabo 27px',
87
+ 'Raleway',
88
+ 'PT Sans',
89
+ 'Merriweather',
90
+ 'Nunito',
91
+ 'Playfair Display',
92
+ 'Poppins',
93
+ 'Ubuntu',
94
+ 'Lora',
95
+ 'Muli',
96
+ 'Arvo',
97
+ 'Rubik',
98
+ 'Courier Prime',
99
+ 'Pacifico',
100
+ 'Dancing Script',
101
+ 'Bangers',
102
+ 'Special Elite',
103
+ 'Press Start 2P',
104
+ 'Monoton',
105
+ 'Creepster',
106
+ 'Lobster',
107
+ 'Orbitron',
108
+ 'Righteous',
109
+ 'Satisfy',
110
+ 'Shadows Into Light',
111
+ 'Permanent Marker',
112
+ 'Fredoka One',
113
+ 'Amatic SC',
114
+ 'Cinzel',
115
+ 'Great Vibes',
116
+ 'Kaushan Script'
117
+ ], ...(ngDevMode ? [{ debugName: "fonts" }] : /* istanbul ignore next */ []));
118
+ _layers = signal([], ...(ngDevMode ? [{ debugName: "_layers" }] : /* istanbul ignore next */ []));
119
+ layers = this._layers.asReadonly();
120
+ selectedLayerId = signal(null, ...(ngDevMode ? [{ debugName: "selectedLayerId" }] : /* istanbul ignore next */ []));
121
+ _change$ = new Subject();
122
+ change$ = this._change$.asObservable();
123
+ undoStack = [];
124
+ redoStack = [];
125
+ canUndo = signal(false, ...(ngDevMode ? [{ debugName: "canUndo" }] : /* istanbul ignore next */ []));
126
+ canRedo = signal(false, ...(ngDevMode ? [{ debugName: "canRedo" }] : /* istanbul ignore next */ []));
127
+ historyLimit = 50;
128
+ transformer;
129
+ selectionRect;
130
+ hoverRect;
131
+ hoveredShape;
132
+ selectionBordersGroup;
133
+ selectionStartPos;
134
+ isSelecting = false;
135
+ isDragging = false;
136
+ wasSelectedBeforeClick = false;
137
+ snapLines = [];
138
+ snapSettings = {
139
+ showGuidelines: true,
140
+ snapToShapes: true,
141
+ snapToStageCenter: true,
142
+ snapToStageBorders: true,
143
+ guidelineColor: 'blue',
144
+ snapRange: 5
145
+ };
146
+ defaultFont = 'Inter';
147
+ ngOnDestroy() {
148
+ this.destroy();
149
+ }
150
+ async init(container, imageSize, defaultFont = 'Inter', scale = 1, minScale = 0.1, maxScale = 5) {
151
+ if (!this.isBrowser) {
152
+ console.log('ImageDesignerService.init skipped (not a browser)');
153
+ return;
154
+ }
155
+ console.log('ImageDesignerService.init starting', { container, imageSize, defaultFont, scale, minScale, maxScale });
156
+ console.log('Container dimensions:', container.offsetWidth, 'x', container.offsetHeight);
157
+ this.defaultFont = defaultFont;
158
+ this.minScale = minScale;
159
+ this.maxScale = maxScale;
160
+ this.setScale(scale);
161
+ try {
162
+ this.stage = new Konva.Stage({
163
+ container: container,
164
+ width: container.offsetWidth || imageSize.width + 100,
165
+ height: container.offsetHeight || imageSize.height + 100,
166
+ });
167
+ console.log('Konva.Stage created');
168
+ }
169
+ catch (e) {
170
+ console.error('Failed to create Konva.Stage:', e);
171
+ return;
172
+ }
173
+ this.workspaceLayer = new Konva.Layer();
174
+ this.stage.add(this.workspaceLayer);
175
+ this.uiLayer = new Konva.Layer();
176
+ this.stage.add(this.uiLayer);
177
+ this.canvasRect = new Konva.Rect({
178
+ x: (this.stage.width() || imageSize.width) / 2,
179
+ y: (this.stage.height() || imageSize.height) / 2,
180
+ width: imageSize.width,
181
+ height: imageSize.height,
182
+ fill: 'white',
183
+ stroke: '#d9dcdf',
184
+ strokeWidth: 1,
185
+ name: 'canvasRect'
186
+ });
187
+ this.canvasRect.offsetX(imageSize.width / 2);
188
+ this.canvasRect.offsetY(imageSize.height / 2);
189
+ this.canvasRect.scaleX(this.scale());
190
+ this.canvasRect.scaleY(this.scale());
191
+ this.workspaceLayer.add(this.canvasRect);
192
+ this.transformer = new Konva.Transformer({
193
+ rotateEnabled: true,
194
+ enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'top-center', 'bottom-center', 'middle-left', 'middle-right'],
195
+ name: 'transformer',
196
+ // Ensure transformer is always above the mask
197
+ boundBoxFunc: (oldBox, newBox) => {
198
+ // Limit minimum size
199
+ if (Math.abs(newBox.width) < 5 || Math.abs(newBox.height) < 5) {
200
+ return oldBox;
201
+ }
202
+ return newBox;
203
+ },
204
+ });
205
+ this.uiLayer.add(this.transformer);
206
+ this.transformer.on('transform', () => {
207
+ this.updateSelectionBorders();
208
+ const nodes = this.transformer?.nodes() || [];
209
+ nodes.forEach((node) => {
210
+ if (node instanceof Konva.Text) {
211
+ // If we resized vertically (scaleY changed), scale fontSize
212
+ const scaleY = node.scaleY();
213
+ if (Math.abs(scaleY - this.scale()) > 0.001) {
214
+ node.fontSize(node.fontSize() * (scaleY / this.scale()));
215
+ }
216
+ // Scale width according to scaleX
217
+ const scaleX = node.scaleX();
218
+ node.width(node.width() * (scaleX / this.scale()));
219
+ // Reset scale to current canvas scale
220
+ node.scaleX(this.scale());
221
+ node.scaleY(this.scale());
222
+ // Update offset because width/fontSize changed
223
+ node.offsetX(node.width() / 2);
224
+ node.offsetY(node.height() / 2);
225
+ }
226
+ });
227
+ if (nodes.length > 0) {
228
+ const guides = this.getSnappingGuides(nodes);
229
+ this.drawSnappingLines(guides);
230
+ this.updateShapesOriginalPositions(nodes);
231
+ }
232
+ this.notifyChange();
233
+ });
234
+ this.transformer.on('dragmove', (e) => {
235
+ const target = e.target;
236
+ if (this.transformer && target === this.transformer) {
237
+ const nodes = this.transformer.nodes();
238
+ if (nodes.length > 0) {
239
+ const targetBox = this.getNodesClientRect(nodes);
240
+ const guides = this.getSnappingGuides(nodes);
241
+ let dx = 0;
242
+ let dy = 0;
243
+ const vGuide = guides.vertical[0];
244
+ const hGuide = guides.horizontal[0];
245
+ if (vGuide) {
246
+ dx = vGuide.guide - vGuide.offset - targetBox.x;
247
+ }
248
+ if (hGuide) {
249
+ dy = hGuide.guide - hGuide.offset - targetBox.y;
250
+ }
251
+ if (dx !== 0 || dy !== 0) {
252
+ nodes.forEach((node) => {
253
+ const p = node.getAbsolutePosition();
254
+ node.setAbsolutePosition({
255
+ x: p.x + dx,
256
+ y: p.y + dy
257
+ });
258
+ });
259
+ }
260
+ const finalGuides = this.getSnappingGuides(nodes);
261
+ this.drawSnappingLines(finalGuides);
262
+ this.updateSelectionBorders();
263
+ }
264
+ }
265
+ });
266
+ this.transformer.on('dragend', () => {
267
+ this.clearSnapLines();
268
+ const nodes = this.transformer?.nodes() || [];
269
+ if (nodes.length > 0) {
270
+ this.updateShapesOriginalPositions(nodes);
271
+ }
272
+ this.notifyChange();
273
+ });
274
+ this.transformer.on('transformend', () => {
275
+ this.clearSnapLines();
276
+ const nodes = this.transformer?.nodes() || [];
277
+ if (nodes.length > 0) {
278
+ nodes.forEach(node => {
279
+ if (!(node instanceof Konva.Text)) {
280
+ const scaleX = node.scaleX();
281
+ const scaleY = node.scaleY();
282
+ node.width(node.width() * (scaleX / this.scale()));
283
+ node.height(node.height() * (scaleY / this.scale()));
284
+ node.scaleX(this.scale());
285
+ node.scaleY(this.scale());
286
+ }
287
+ // If the node has effects, we MUST re-cache it with the new size
288
+ const id = node.id();
289
+ const config = this._layers().find(l => l.id === id);
290
+ if (config) {
291
+ // Re-apply effects and re-cache
292
+ this.applyEffectsToShape(node, config);
293
+ }
294
+ });
295
+ // We update the signal AFTER applying effects to ensure consistency
296
+ this.updateShapesOriginalPositions(nodes);
297
+ }
298
+ this.notifyChange();
299
+ });
300
+ this.selectionRect = new Konva.Rect({
301
+ fill: 'rgba(0,0,255,0.1)',
302
+ stroke: '#3b82f6',
303
+ strokeWidth: 1,
304
+ visible: false,
305
+ name: 'selectionRect',
306
+ zIndex: 1001
307
+ });
308
+ this.uiLayer.add(this.selectionRect);
309
+ this.hoverRect = new Konva.Rect({
310
+ stroke: '#3b82f6',
311
+ strokeWidth: 1,
312
+ listening: false,
313
+ visible: false,
314
+ name: 'hoverRect'
315
+ });
316
+ this.uiLayer.add(this.hoverRect);
317
+ this.selectionBordersGroup = new Konva.Group({
318
+ listening: false,
319
+ name: 'selectionBordersGroup'
320
+ });
321
+ this.uiLayer.add(this.selectionBordersGroup);
322
+ this.maskOverlay = new Konva.Shape({
323
+ fill: 'rgba(255, 255, 255, 0.8)',
324
+ listening: false,
325
+ name: 'maskOverlay',
326
+ zIndex: 1000,
327
+ fillRule: 'evenodd',
328
+ sceneFunc: (context, shape) => {
329
+ const stage = shape.getStage();
330
+ if (!stage || !this.canvasRect)
331
+ return;
332
+ const stageWidth = stage.width();
333
+ const stageHeight = stage.height();
334
+ const canvasX = this.canvasRect.x();
335
+ const canvasY = this.canvasRect.y();
336
+ const canvasWidth = this.canvasRect.width() * this.canvasRect.scaleX();
337
+ const canvasHeight = this.canvasRect.height() * this.canvasRect.scaleY();
338
+ context.beginPath();
339
+ // Наружный прямоугольник (весь Stage)
340
+ context.rect(0, 0, stageWidth, stageHeight);
341
+ // Внутренний прямоугольник (canvasRect)
342
+ // Since canvasRect has offset at its center, its top-left corner is at (x - width*scale/2, y - height*scale/2)
343
+ // We draw it in counter-clockwise direction to create a hole
344
+ context.rect(canvasX + canvasWidth / 2, canvasY - canvasHeight / 2, -canvasWidth, canvasHeight);
345
+ context.closePath();
346
+ context.fillShape(shape);
347
+ }
348
+ });
349
+ this.uiLayer.add(this.maskOverlay);
350
+ await this.loadAllFonts();
351
+ this.updateSize(container, imageSize);
352
+ // Add a small delay to ensure everything is settled and then center again
353
+ setTimeout(() => {
354
+ this.updateSize(container, imageSize);
355
+ this.workspaceLayer?.batchDraw();
356
+ this.uiLayer?.batchDraw();
357
+ }, 50);
358
+ // Final draw to ensure everything is rendered
359
+ this.workspaceLayer?.batchDraw();
360
+ this.uiLayer?.batchDraw();
361
+ console.log('Konva Canvas Rect and layers added and drawn');
362
+ this.setupResizeObserver(container, imageSize);
363
+ this.setupSelectionListeners();
364
+ this.isInitialized.set(true);
365
+ this.undoStack = [this.getSnapshot()];
366
+ this.updateHistorySignals();
367
+ }
368
+ handleGlobalMouseUp = (e) => {
369
+ if (!this.isSelecting) {
370
+ return;
371
+ }
372
+ this.isSelecting = false;
373
+ if (!this.selectionRect || !this.workspaceLayer || !this.transformer)
374
+ return;
375
+ if (!this.selectionRect.visible()) {
376
+ // If no selection rect was drawn, but we were selecting,
377
+ // it means it was a simple click that didn't move much.
378
+ const pos = this.stage?.getPointerPosition();
379
+ const shape = pos ? this.stage?.getIntersection(pos) : null;
380
+ if (shape && shape.id()) {
381
+ this.selectLayer(shape.id());
382
+ }
383
+ else {
384
+ this.selectLayer(null);
385
+ }
386
+ return;
387
+ }
388
+ this.selectionRect.visible(false);
389
+ const box = this.selectionRect.getClientRect();
390
+ const shapes = this.workspaceLayer.find('.rect, .text, .image, .pattern').filter((shape) => {
391
+ if (shape === this.canvasRect || shape === this.selectionRect || shape === this.transformer) {
392
+ return false;
393
+ }
394
+ if (!shape.visible() || !shape.listening()) {
395
+ return false;
396
+ }
397
+ const layer = this._layers().find(l => l.id === shape.id());
398
+ if (layer?.locked) {
399
+ return false;
400
+ }
401
+ return Konva.Util.haveIntersection(box, shape.getClientRect());
402
+ });
403
+ this.transformer.nodes(shapes);
404
+ if (shapes.length === 1) {
405
+ const shape = shapes[0];
406
+ const layer = this._layers().find(l => l.id === shape.id());
407
+ this.updateTransformer(shape, !!layer?.locked);
408
+ this.selectedLayerId.set(shape.id());
409
+ }
410
+ else if (shapes.length > 1) {
411
+ // Multiple selection: show default anchors for all
412
+ this.transformer.setAttrs({
413
+ rotateEnabled: true,
414
+ enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'top-center', 'bottom-center', 'middle-left', 'middle-right'],
415
+ });
416
+ this.selectedLayerId.set(null); // Or some multi-select id
417
+ }
418
+ this.transformer.forceUpdate();
419
+ this.updateSelectionBorders();
420
+ this.workspaceLayer.batchDraw();
421
+ };
422
+ handleGlobalMouseMove = (e) => {
423
+ if (!this.isSelecting || !this.selectionRect || !this.selectionStartPos || !this.stage) {
424
+ return;
425
+ }
426
+ const container = this.stage.container();
427
+ const rect = container.getBoundingClientRect();
428
+ let clientX, clientY;
429
+ if ('touches' in e) {
430
+ clientX = e.touches[0].clientX;
431
+ clientY = e.touches[0].clientY;
432
+ }
433
+ else {
434
+ clientX = e.clientX;
435
+ clientY = e.clientY;
436
+ }
437
+ const pos = {
438
+ x: clientX - rect.left,
439
+ y: clientY - rect.top
440
+ };
441
+ const rectX = Math.min(pos.x, this.selectionStartPos.x);
442
+ const rectY = Math.min(pos.y, this.selectionStartPos.y);
443
+ const rectWidth = Math.abs(pos.x - this.selectionStartPos.x);
444
+ const rectHeight = Math.abs(pos.y - this.selectionStartPos.y);
445
+ if (!isNaN(rectX) && !isNaN(rectY) && !isNaN(rectWidth) && !isNaN(rectHeight)) {
446
+ if (rectWidth > 5 || rectHeight > 5) {
447
+ this.selectionRect.visible(true);
448
+ }
449
+ this.selectionRect.setAttrs({
450
+ x: rectX,
451
+ y: rectY,
452
+ width: rectWidth,
453
+ height: rectHeight,
454
+ strokeWidth: 1,
455
+ });
456
+ this.selectionRect.moveToTop();
457
+ }
458
+ this.uiLayer?.moveToTop();
459
+ this.workspaceLayer?.batchDraw();
460
+ this.uiLayer?.batchDraw();
461
+ };
462
+ makeTextEditable(textNode) {
463
+ if (!this.stage)
464
+ return;
465
+ // Save original text and settings to restore if cancelled
466
+ textNode.setAttr('originalText', textNode.text());
467
+ textNode.setAttr('originalWidth', textNode.width());
468
+ // Fix width to current width during editing to enable wrapping
469
+ const initialWidth = textNode.width();
470
+ textNode.width(initialWidth);
471
+ // Hide text node but keep transformer visible
472
+ textNode.opacity(0);
473
+ this.transformer?.forceUpdate();
474
+ this.updateSelectionBorders();
475
+ const stage = this.stage;
476
+ const textPosition = textNode.getAbsolutePosition();
477
+ const stageBox = stage.container().getBoundingClientRect();
478
+ const areaPosition = {
479
+ x: stageBox.left + textPosition.x,
480
+ y: stageBox.top + textPosition.y,
481
+ };
482
+ // Create textarea
483
+ const textarea = document.createElement('textarea');
484
+ document.body.appendChild(textarea);
485
+ // Set styles
486
+ textarea.value = textNode.text();
487
+ textarea.style.position = 'absolute';
488
+ textarea.style.top = areaPosition.y + 'px';
489
+ textarea.style.left = areaPosition.x + 'px';
490
+ textarea.style.width = (textNode.width() * textNode.getAbsoluteScale().x) + 'px';
491
+ textarea.style.height = (textNode.height() * textNode.getAbsoluteScale().y + 10) + 'px';
492
+ textarea.style.fontSize = (textNode.fontSize() * textNode.getAbsoluteScale().y) + 'px';
493
+ textarea.style.border = 'none';
494
+ textarea.style.padding = (textNode.padding() * textNode.getAbsoluteScale().x) + 'px';
495
+ textarea.style.margin = '0px';
496
+ textarea.style.overflow = 'hidden';
497
+ textarea.style.background = 'none';
498
+ textarea.style.outline = 'none';
499
+ textarea.style.resize = 'none';
500
+ textarea.style.lineHeight = textNode.lineHeight().toString();
501
+ textarea.style.fontFamily = textNode.fontFamily();
502
+ textarea.style.fontWeight = (textNode.getAttr('fontWeight') || textNode.fontStyle() || 'normal').toString();
503
+ textarea.style.fontStyle = textNode.fontStyle()?.includes('italic') ? 'italic' : 'normal';
504
+ textarea.style.transformOrigin = 'left top';
505
+ textarea.style.textAlign = textNode.align();
506
+ textarea.style.color = textNode.fill();
507
+ textarea.style.boxSizing = 'border-box';
508
+ textarea.style.zIndex = '1000';
509
+ const rotation = textNode.getAbsoluteRotation();
510
+ let transform = '';
511
+ if (rotation) {
512
+ transform += 'rotateZ(' + rotation + 'deg)';
513
+ }
514
+ // Offset based on Konva's offset (textNode is centered by default in addLayer)
515
+ const px = 0;
516
+ // apply configuration
517
+ // we need to skip transform for now as it's tricky with absolute positioning and offsets
518
+ // But since we use absolutePosition which already accounts for many things, let's see.
519
+ // If textNode has offset (centered), we need to adjust textarea position
520
+ const offsetX = textNode.offsetX() * textNode.getAbsoluteScale().x;
521
+ const offsetY = textNode.offsetY() * textNode.getAbsoluteScale().y;
522
+ textarea.style.left = (areaPosition.x - offsetX) + 'px';
523
+ textarea.style.top = (areaPosition.y - offsetY) + 'px';
524
+ textarea.style.transform = transform;
525
+ textarea.focus();
526
+ let isFinished = false;
527
+ const removeTextarea = () => {
528
+ if (isFinished)
529
+ return;
530
+ isFinished = true;
531
+ const originalText = textNode.getAttr('originalText');
532
+ const originalWidth = textNode.getAttr('originalWidth');
533
+ if (originalText !== undefined) {
534
+ textNode.text(originalText);
535
+ }
536
+ if (originalWidth !== undefined) {
537
+ textNode.width(originalWidth);
538
+ }
539
+ if (textarea.parentNode) {
540
+ textarea.parentNode.removeChild(textarea);
541
+ }
542
+ window.removeEventListener('click', handleOutsideClick);
543
+ textarea.removeEventListener('blur', handleBlur);
544
+ textNode.opacity(1);
545
+ this.transformer?.forceUpdate();
546
+ this.updateSelectionBorders();
547
+ this.workspaceLayer?.batchDraw();
548
+ };
549
+ const updateText = () => {
550
+ const newText = textarea.value;
551
+ const originalText = textNode.getAttr('originalText');
552
+ if (newText !== originalText) {
553
+ const id = textNode.id();
554
+ if (id) {
555
+ // Keep the fixed width when updating the layer
556
+ this.updateLayer(id, { text: newText, width: textNode.width() });
557
+ }
558
+ else {
559
+ textNode.text(newText);
560
+ textNode.offsetX(textNode.width() / 2);
561
+ textNode.offsetY(textNode.height() / 2);
562
+ this.workspaceLayer?.batchDraw();
563
+ }
564
+ // Restore transformer nodes after update if it was selected
565
+ if (this.selectedLayerId() === textNode.id()) {
566
+ this.transformer?.nodes([textNode]);
567
+ }
568
+ }
569
+ // Clear originalText and originalWidth after update so removeTextarea doesn't restore it
570
+ textNode.setAttr('originalText', undefined);
571
+ textNode.setAttr('originalWidth', undefined);
572
+ };
573
+ const handleBlur = () => {
574
+ updateText();
575
+ removeTextarea();
576
+ };
577
+ textarea.addEventListener('blur', handleBlur);
578
+ textarea.addEventListener('keydown', (e) => {
579
+ // hide on enter
580
+ // but don't hide on shift + enter
581
+ if (e.keyCode === 13 && !e.shiftKey) {
582
+ e.preventDefault();
583
+ textarea.blur();
584
+ }
585
+ // on escape do not update
586
+ if (e.keyCode === 27) {
587
+ removeTextarea();
588
+ }
589
+ });
590
+ textarea.addEventListener('input', () => {
591
+ // update node text to update transformer
592
+ textNode.text(textarea.value);
593
+ this.transformer?.forceUpdate();
594
+ this.updateSelectionBorders();
595
+ // re-calculate size
596
+ // Width is fixed, only height should change
597
+ textarea.style.height = 'auto';
598
+ textarea.style.height = textarea.scrollHeight + 'px';
599
+ });
600
+ const handleOutsideClick = (e) => {
601
+ if (e.target !== textarea) {
602
+ // blur will handle the rest
603
+ textarea.blur();
604
+ }
605
+ };
606
+ // Use timeout to avoid immediate trigger from the same click that opened it
607
+ setTimeout(() => {
608
+ window.addEventListener('click', handleOutsideClick);
609
+ });
610
+ }
611
+ setupSelectionListeners() {
612
+ if (!this.stage)
613
+ return;
614
+ if (this.isBrowser) {
615
+ window.addEventListener('mouseup', this.handleGlobalMouseUp);
616
+ window.addEventListener('touchend', this.handleGlobalMouseUp);
617
+ window.addEventListener('mousemove', this.handleGlobalMouseMove);
618
+ window.addEventListener('touchmove', this.handleGlobalMouseMove);
619
+ }
620
+ this.stage.on('mousedown touchstart', (e) => {
621
+ // If right click - do nothing
622
+ if (e.evt instanceof MouseEvent && e.evt.button === 2) {
623
+ return;
624
+ }
625
+ // If click on transformer - do nothing
626
+ const isTransformer = e.target.getParent()?.className === 'Transformer';
627
+ if (isTransformer) {
628
+ return;
629
+ }
630
+ // If click on empty area, canvasRect or a locked layer - start selection
631
+ const shapeAtPointer = e.target;
632
+ const layerConfig = this._layers().find(l => l.id === shapeAtPointer.id());
633
+ const isLocked = !!layerConfig?.locked;
634
+ if (e.target === this.stage || e.target === this.canvasRect || isLocked) {
635
+ this.isSelecting = true;
636
+ this.uiLayer?.moveToTop();
637
+ const pos = this.stage?.getPointerPosition();
638
+ if (pos) {
639
+ this.selectionStartPos = { x: pos.x, y: pos.y };
640
+ this.selectionRect?.visible(false);
641
+ this.selectionRect?.width(0);
642
+ this.selectionRect?.height(0);
643
+ this.selectionRect?.moveToTop();
644
+ }
645
+ return;
646
+ }
647
+ // Find the clicked shape
648
+ const shape = e.target;
649
+ const id = shape.id();
650
+ if (id) {
651
+ const isShiftPressed = e.evt.shiftKey;
652
+ const currentNodes = this.transformer?.nodes() || [];
653
+ this.wasSelectedBeforeClick = currentNodes.includes(shape);
654
+ if (isShiftPressed) {
655
+ // Toggle selection
656
+ if (currentNodes.includes(shape)) {
657
+ const newNodes = currentNodes.filter(n => n !== shape);
658
+ this.transformer?.nodes(newNodes);
659
+ this.updateSelectionBorders();
660
+ if (newNodes.length === 1 && newNodes[0].id()) {
661
+ this.selectedLayerId.set(newNodes[0].id());
662
+ }
663
+ else if (newNodes.length === 0) {
664
+ this.selectedLayerId.set(null);
665
+ }
666
+ else {
667
+ this.selectedLayerId.set(null); // Multi-selection or none
668
+ }
669
+ }
670
+ else {
671
+ const newNodes = [...currentNodes, shape];
672
+ this.transformer?.nodes(newNodes);
673
+ this.updateSelectionBorders();
674
+ if (newNodes.length === 1) {
675
+ this.selectedLayerId.set(shape.id());
676
+ }
677
+ else {
678
+ this.selectedLayerId.set(null); // Multi-selection
679
+ }
680
+ }
681
+ }
682
+ else {
683
+ // Normal click
684
+ // Only change selection on mousedown if the object is NOT already selected.
685
+ // This allows moving the whole selection group.
686
+ if (!currentNodes.includes(shape)) {
687
+ this.selectLayer(id);
688
+ }
689
+ }
690
+ }
691
+ else {
692
+ // Click on something without ID (like the canvas but not the canvasRect handler above)
693
+ this.selectLayer(null);
694
+ }
695
+ });
696
+ this.stage.on('click tap', (e) => {
697
+ // If right click - do nothing
698
+ if (e.evt instanceof MouseEvent && e.evt.button === 2) {
699
+ return;
700
+ }
701
+ // If click on transformer - do nothing
702
+ const isTransformer = e.target.getParent()?.className === 'Transformer';
703
+ if (isTransformer) {
704
+ return;
705
+ }
706
+ const shape = e.target;
707
+ const currentNodesOnStage = this.transformer?.nodes() || [];
708
+ if (this.wasSelectedBeforeClick && currentNodesOnStage.length === 1 && currentNodesOnStage[0] === shape && shape instanceof Konva.Text) {
709
+ this.makeTextEditable(shape);
710
+ return;
711
+ }
712
+ // If click on empty area or canvasRect - do nothing
713
+ if (e.target === this.stage || e.target === this.canvasRect) {
714
+ return;
715
+ }
716
+ const id = shape.id();
717
+ if (!id)
718
+ return;
719
+ const isShiftPressed = e.evt.shiftKey;
720
+ if (isShiftPressed)
721
+ return; // Handled in mousedown
722
+ const currentNodesOnClick = this.transformer?.nodes() || [];
723
+ // If we clicked on an object that is part of a multi-selection,
724
+ // and it was a simple click (not a drag), then select only this object.
725
+ if (currentNodesOnClick.length > 1 && currentNodesOnClick.includes(shape)) {
726
+ this.selectLayer(id);
727
+ }
728
+ });
729
+ this.stage.on('mousemove touchmove', (e) => {
730
+ // Logic moved to handleGlobalMouseMove
731
+ });
732
+ }
733
+ selectLayer(id) {
734
+ this.selectedLayerId.set(id);
735
+ this.hideHover();
736
+ if (!this.transformer || !this.workspaceLayer)
737
+ return;
738
+ if (!id) {
739
+ this.transformer.nodes([]);
740
+ }
741
+ else {
742
+ const shape = this.workspaceLayer.findOne('#' + id);
743
+ if (shape && shape.visible()) {
744
+ const layer = this._layers().find(l => l.id === id);
745
+ this.transformer.nodes([shape]);
746
+ this.updateTransformer(shape, !!layer?.locked);
747
+ }
748
+ else {
749
+ this.transformer.nodes([]);
750
+ }
751
+ }
752
+ this.updateSelectionBorders();
753
+ this.transformer.moveToTop();
754
+ this.uiLayer?.moveToTop();
755
+ this.workspaceLayer.batchDraw();
756
+ this.uiLayer?.batchDraw();
757
+ }
758
+ updateTransformer(shape, isLocked) {
759
+ if (!this.transformer)
760
+ return;
761
+ if (isLocked) {
762
+ this.transformer.setAttrs({
763
+ rotateEnabled: false,
764
+ enabledAnchors: [],
765
+ });
766
+ }
767
+ else {
768
+ if (shape instanceof Konva.Text) {
769
+ this.transformer.setAttrs({
770
+ rotateEnabled: true,
771
+ enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'middle-left', 'middle-right'],
772
+ });
773
+ }
774
+ else {
775
+ this.transformer.setAttrs({
776
+ rotateEnabled: true,
777
+ enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'top-center', 'bottom-center', 'middle-left', 'middle-right'],
778
+ });
779
+ }
780
+ }
781
+ this.transformer.forceUpdate();
782
+ }
783
+ toggleLayerVisibility(id) {
784
+ const layer = this._layers().find(l => l.id === id);
785
+ if (!layer || !this.workspaceLayer)
786
+ return;
787
+ const newVisible = layer.visible === false;
788
+ this._layers.update(layers => layers.map(l => l.id === id ? { ...l, visible: newVisible } : l));
789
+ const shape = this.workspaceLayer.findOne('#' + id);
790
+ if (shape) {
791
+ shape.visible(newVisible);
792
+ if (!newVisible && this.selectedLayerId() === id) {
793
+ this.selectLayer(null);
794
+ }
795
+ this.workspaceLayer.batchDraw();
796
+ }
797
+ }
798
+ toggleLayerLock(id) {
799
+ const layer = this._layers().find(l => l.id === id);
800
+ if (!layer || !this.workspaceLayer)
801
+ return;
802
+ const newLocked = !layer.locked;
803
+ this._layers.update(layers => layers.map(l => l.id === id ? { ...l, locked: newLocked } : l));
804
+ const shape = this.workspaceLayer.findOne('#' + id);
805
+ if (shape) {
806
+ shape.listening(true);
807
+ shape.draggable(!newLocked);
808
+ // Update transformer if the locked shape is currently selected
809
+ if (this.selectedLayerId() === id) {
810
+ this.updateTransformer(shape, newLocked);
811
+ }
812
+ this.workspaceLayer.batchDraw();
813
+ }
814
+ }
815
+ async flipHorizontal(id) {
816
+ const layer = this._layers().find(l => l.id === id);
817
+ if (!layer)
818
+ return;
819
+ const currentScaleX = layer['scaleX'] ?? 1;
820
+ await this.updateLayer(id, { scaleX: currentScaleX * -1 });
821
+ }
822
+ async flipVertical(id) {
823
+ const layer = this._layers().find(l => l.id === id);
824
+ if (!layer)
825
+ return;
826
+ const currentScaleY = layer['scaleY'] ?? 1;
827
+ await this.updateLayer(id, { scaleY: currentScaleY * -1 });
828
+ }
829
+ async fitToPage(id) {
830
+ const layer = this._layers().find(l => l.id === id);
831
+ if (!layer || !this.canvasRect)
832
+ return;
833
+ const canvasWidth = this.canvasRect.width();
834
+ const canvasHeight = this.canvasRect.height();
835
+ const canvasCenterX = this.canvasRect.x();
836
+ const canvasCenterY = this.canvasRect.y();
837
+ const shape = this.workspaceLayer?.findOne('#' + id);
838
+ if (!shape)
839
+ return;
840
+ // Use actual shape dimensions if available, otherwise fallback to layer config
841
+ const layerWidth = (shape instanceof Konva.Text) ? shape.width() : (layer.width || shape.width());
842
+ const layerHeight = (shape instanceof Konva.Text) ? shape.height() : (layer.height || shape.height());
843
+ if (layerWidth === 0 || layerHeight === 0)
844
+ return;
845
+ const scale = Math.min(canvasWidth / layerWidth, canvasHeight / layerHeight) * this.scale();
846
+ await this.updateLayer(id, {
847
+ x: canvasCenterX,
848
+ y: canvasCenterY,
849
+ width: layerWidth,
850
+ height: layerHeight,
851
+ scaleX: scale,
852
+ scaleY: scale,
853
+ rotation: 0
854
+ });
855
+ }
856
+ async fillPage(id) {
857
+ const layer = this._layers().find(l => l.id === id);
858
+ if (!layer || !this.canvasRect)
859
+ return;
860
+ const canvasWidth = this.canvasRect.width();
861
+ const canvasHeight = this.canvasRect.height();
862
+ const canvasCenterX = this.canvasRect.x();
863
+ const canvasCenterY = this.canvasRect.y();
864
+ const shape = this.workspaceLayer?.findOne('#' + id);
865
+ if (!shape)
866
+ return;
867
+ // Use actual shape dimensions if available, otherwise fallback to layer config
868
+ const layerWidth = (shape instanceof Konva.Text) ? shape.width() : (layer.width || shape.width());
869
+ const layerHeight = (shape instanceof Konva.Text) ? shape.height() : (layer.height || shape.height());
870
+ if (layerWidth === 0 || layerHeight === 0)
871
+ return;
872
+ const scale = Math.max(canvasWidth / layerWidth, canvasHeight / layerHeight) * this.scale();
873
+ await this.updateLayer(id, {
874
+ x: canvasCenterX,
875
+ y: canvasCenterY,
876
+ width: layerWidth,
877
+ height: layerHeight,
878
+ scaleX: scale,
879
+ scaleY: scale,
880
+ rotation: 0
881
+ });
882
+ }
883
+ deleteLayer(id) {
884
+ const layer = this._layers().find(l => l.id === id);
885
+ if (!layer || !this.workspaceLayer || layer.locked)
886
+ return;
887
+ this.hideHover();
888
+ // Remove from signal
889
+ this._layers.update(layers => layers.filter(l => l.id !== id));
890
+ this.notifyChange();
891
+ // Remove from Konva
892
+ const shape = this.workspaceLayer.findOne('#' + id);
893
+ if (shape) {
894
+ if (this.transformer) {
895
+ const nodes = this.transformer.nodes();
896
+ if (nodes.includes(shape)) {
897
+ this.transformer.nodes(nodes.filter(n => n !== shape));
898
+ }
899
+ }
900
+ shape.destroy();
901
+ if (this.selectedLayerId() === id) {
902
+ this.selectLayer(null);
903
+ }
904
+ else {
905
+ this.updateSelectionBorders();
906
+ }
907
+ this.workspaceLayer.batchDraw();
908
+ }
909
+ }
910
+ deleteSelectedLayers() {
911
+ if (!this.transformer || !this.workspaceLayer)
912
+ return;
913
+ const nodes = this.transformer.nodes();
914
+ if (nodes.length === 0)
915
+ return;
916
+ this.hideHover();
917
+ const idsToDelete = nodes.map(node => node.id()).filter((id) => {
918
+ const layer = this._layers().find(l => l.id === id);
919
+ return !!id && !!layer && !layer.locked;
920
+ });
921
+ if (idsToDelete.length === 0)
922
+ return;
923
+ // Remove from signal
924
+ this._layers.update(layers => layers.filter(l => !l.id || !idsToDelete.includes(l.id)));
925
+ this.notifyChange();
926
+ // Remove from Konva
927
+ idsToDelete.forEach(id => {
928
+ const shape = this.workspaceLayer?.findOne('#' + id);
929
+ if (shape) {
930
+ shape.destroy();
931
+ }
932
+ });
933
+ this.transformer.nodes([]);
934
+ this.selectLayer(null);
935
+ this.workspaceLayer.batchDraw();
936
+ }
937
+ moveSelectedLayers(dx, dy) {
938
+ if (!this.transformer || !this.workspaceLayer)
939
+ return;
940
+ const nodes = this.transformer.nodes();
941
+ if (nodes.length === 0)
942
+ return;
943
+ let moved = false;
944
+ nodes.forEach(node => {
945
+ const id = node.id();
946
+ const layer = this._layers().find(l => l.id === id);
947
+ if (layer && !layer.locked) {
948
+ node.x(node.x() + dx);
949
+ node.y(node.y() + dy);
950
+ moved = true;
951
+ }
952
+ });
953
+ if (moved) {
954
+ this.updateSelectionBorders();
955
+ this.workspaceLayer.batchDraw();
956
+ this.updateShapesOriginalPositions(nodes);
957
+ this.notifyChange();
958
+ }
959
+ }
960
+ showHover(shape) {
961
+ if (!this.hoverRect || !this.workspaceLayer)
962
+ return;
963
+ this.hoveredShape = shape;
964
+ const box = shape.getClientRect({ relativeTo: this.workspaceLayer });
965
+ this.hoverRect.setAttrs({
966
+ x: box.x,
967
+ y: box.y,
968
+ width: box.width,
969
+ height: box.height,
970
+ strokeWidth: 1,
971
+ visible: true
972
+ });
973
+ this.hoverRect.moveToTop();
974
+ this.workspaceLayer.batchDraw();
975
+ }
976
+ hideHover() {
977
+ this.hoveredShape = undefined;
978
+ if (!this.hoverRect)
979
+ return;
980
+ this.hoverRect.visible(false);
981
+ this.workspaceLayer?.batchDraw();
982
+ }
983
+ updateSelectionBorders() {
984
+ if (!this.selectionBordersGroup || !this.transformer || !this.workspaceLayer)
985
+ return;
986
+ this.selectionBordersGroup.destroyChildren();
987
+ const nodes = this.transformer.nodes();
988
+ if (nodes.length > 1) {
989
+ nodes.forEach(node => {
990
+ const box = node.getClientRect({ relativeTo: this.workspaceLayer });
991
+ const rect = new Konva.Rect({
992
+ x: box.x,
993
+ y: box.y,
994
+ width: box.width,
995
+ height: box.height,
996
+ stroke: '#3b82f6',
997
+ strokeWidth: 1,
998
+ listening: false,
999
+ name: 'selectionBorder'
1000
+ });
1001
+ this.selectionBordersGroup?.add(rect);
1002
+ });
1003
+ this.selectionBordersGroup.visible(true);
1004
+ }
1005
+ else {
1006
+ this.selectionBordersGroup.visible(false);
1007
+ }
1008
+ this.selectionBordersGroup.moveToTop();
1009
+ this.uiLayer?.batchDraw();
1010
+ }
1011
+ async addLayer(config, updateList = true) {
1012
+ if (!this.workspaceLayer || !this.canvasRect)
1013
+ return undefined;
1014
+ if (config.type === 'text' && config['fontFamily']) {
1015
+ await this.loadFont(config['fontFamily']);
1016
+ }
1017
+ else if (config.type === 'text' && !config['fontFamily']) {
1018
+ await this.loadFont(this.defaultFont);
1019
+ }
1020
+ if (updateList) {
1021
+ const newConfig = { ...config, id: config.id || Math.random().toString(36).substr(2, 9) };
1022
+ // New layer should be at the top of the UI list (index 0)
1023
+ this._layers.update(layers => [newConfig, ...layers]);
1024
+ config = newConfig;
1025
+ if (!this.isSnapshotLoading) {
1026
+ this.notifyChange();
1027
+ }
1028
+ }
1029
+ let shape;
1030
+ const canvasWidth = this.canvasRect?.width() || 0;
1031
+ const canvasHeight = this.canvasRect?.height() || 0;
1032
+ const canvasX = this.canvasRect?.x() || 0;
1033
+ const canvasY = this.canvasRect?.y() || 0;
1034
+ // x and y are relative to canvas center
1035
+ const x = (config.x ?? 0) * this.scale() + canvasX;
1036
+ const y = (config.y ?? 0) * this.scale() + canvasY;
1037
+ if (isNaN(x) || isNaN(y)) {
1038
+ console.warn('ImageDesignerService.addLayer: Calculated layer position is NaN', { x, y, config, canvasWidth, canvasHeight, canvasX, canvasY, scale: this.scale() });
1039
+ return undefined;
1040
+ }
1041
+ if (config.type === 'text') {
1042
+ const fontSize = config['fontSize'] || 18;
1043
+ const padding = 10;
1044
+ shape = new Konva.Text({
1045
+ id: config.id,
1046
+ name: 'text',
1047
+ x,
1048
+ y,
1049
+ text: config['text'] || 'New Text',
1050
+ fontSize,
1051
+ fontFamily: config['fontFamily'] || this.defaultFont,
1052
+ fontStyle: config['fontWeight'] ? `${config['fontWeight']}` : 'normal',
1053
+ fill: config['fill'] || '#000000',
1054
+ opacity: config['opacity'] ?? 1,
1055
+ align: config['align'] || 'left',
1056
+ scaleX: this.scale(),
1057
+ scaleY: this.scale(),
1058
+ draggable: !config.locked,
1059
+ listening: true,
1060
+ visible: config.visible !== false,
1061
+ padding: padding,
1062
+ wrap: 'word',
1063
+ width: config.width || undefined,
1064
+ });
1065
+ if (config['fontWeight']) {
1066
+ shape.setAttr('fontWeight', config['fontWeight']);
1067
+ }
1068
+ // If width is not provided, let's set a default width for headings to enable wrapping/resizing properly
1069
+ if (!config.width) {
1070
+ // Measure text width and add extra horizontal padding
1071
+ const textWidth = shape.width() + 55;
1072
+ shape.width(textWidth);
1073
+ }
1074
+ shape.offsetX(shape.width() / 2);
1075
+ shape.offsetY(shape.height() / 2);
1076
+ this.applyEffectsToShape(shape, config);
1077
+ }
1078
+ else if (config.type === 'image') {
1079
+ const imageObj = new Image();
1080
+ imageObj.crossOrigin = 'anonymous';
1081
+ imageObj.onload = () => {
1082
+ if (shape) {
1083
+ shape.image(imageObj);
1084
+ if (!config.width || !config.height) {
1085
+ shape.width(imageObj.width);
1086
+ shape.height(imageObj.height);
1087
+ }
1088
+ shape.offsetX(shape.width() / 2);
1089
+ shape.offsetY(shape.height() / 2);
1090
+ if (this.transformer?.nodes().includes(shape)) {
1091
+ this.transformer.forceUpdate();
1092
+ }
1093
+ this.applyEffectsToShape(shape, config);
1094
+ this.workspaceLayer?.batchDraw();
1095
+ }
1096
+ };
1097
+ imageObj.src = config['src'] || '';
1098
+ shape = new Konva.Image({
1099
+ id: config.id,
1100
+ name: 'image',
1101
+ x,
1102
+ y,
1103
+ width: config.width || 0,
1104
+ height: config.height || 0,
1105
+ image: undefined,
1106
+ opacity: config['opacity'] ?? 1,
1107
+ scaleX: this.scale(),
1108
+ scaleY: this.scale(),
1109
+ draggable: !config.locked,
1110
+ listening: true,
1111
+ visible: config.visible !== false,
1112
+ });
1113
+ shape.offsetX(shape.width() / 2);
1114
+ shape.offsetY(shape.height() / 2);
1115
+ }
1116
+ else if (config.type === 'shape') {
1117
+ const imageObj = new Image();
1118
+ imageObj.onload = () => {
1119
+ if (shape) {
1120
+ shape.image(imageObj);
1121
+ shape.offsetX(shape.width() / 2);
1122
+ shape.offsetY(shape.height() / 2);
1123
+ if (this.transformer?.nodes().includes(shape)) {
1124
+ this.transformer.forceUpdate();
1125
+ }
1126
+ this.applyEffectsToShape(shape, config);
1127
+ this.workspaceLayer?.batchDraw();
1128
+ }
1129
+ };
1130
+ imageObj.src = config['data'] || '';
1131
+ shape = new Konva.Image({
1132
+ id: config.id,
1133
+ name: 'rect',
1134
+ x,
1135
+ y,
1136
+ width: config.width || 100,
1137
+ height: config.height || 100,
1138
+ image: undefined,
1139
+ opacity: config['opacity'] ?? 1,
1140
+ scaleX: this.scale(),
1141
+ scaleY: this.scale(),
1142
+ draggable: !config.locked,
1143
+ listening: true,
1144
+ visible: config.visible !== false,
1145
+ });
1146
+ shape.offsetX(shape.width() / 2);
1147
+ shape.offsetY(shape.height() / 2);
1148
+ }
1149
+ else if (config.type === 'pattern') {
1150
+ const patternImageObj = new Image();
1151
+ patternImageObj.onload = () => {
1152
+ if (shape) {
1153
+ const rect = shape;
1154
+ rect.fillPatternImage(patternImageObj);
1155
+ // If original pattern is too big, scale it down.
1156
+ // Or just provide a default scale for better look.
1157
+ const patternScale = 0.5; // Fixed pattern scale for better look or based on config
1158
+ rect.fillPatternScale({ x: patternScale, y: patternScale });
1159
+ this.workspaceLayer?.batchDraw();
1160
+ }
1161
+ };
1162
+ patternImageObj.src = typeof config.patternImage === 'string' ? config.patternImage : (config.patternImage?.src || '');
1163
+ shape = new Konva.Rect({
1164
+ id: config.id,
1165
+ name: 'pattern',
1166
+ x,
1167
+ y,
1168
+ width: config.width || canvasWidth,
1169
+ height: config.height || canvasHeight,
1170
+ opacity: config['opacity'] ?? 1,
1171
+ scaleX: this.scale(),
1172
+ scaleY: this.scale(),
1173
+ draggable: !config.locked,
1174
+ listening: true,
1175
+ visible: config.visible !== false,
1176
+ fillPatternRepeat: 'repeat',
1177
+ });
1178
+ shape.offsetX(shape.width() / 2);
1179
+ shape.offsetY(shape.height() / 2);
1180
+ // Ensure pattern stays centered even if shape size changes
1181
+ // Konva's fillPatternOffset is in local coordinates of the shape (after scale/offset?)
1182
+ // Actually it's simpler: if we want it centered, we just need to keep it consistent.
1183
+ }
1184
+ if (shape) {
1185
+ if (config.id) {
1186
+ shape.id(config.id);
1187
+ }
1188
+ this.workspaceLayer.add(shape);
1189
+ // Select the new layer automatically
1190
+ if (updateList && config.id) {
1191
+ this.selectLayer(config.id);
1192
+ }
1193
+ // Update Konva Z-index based on the array order
1194
+ if (updateList) {
1195
+ this.reorderLayers(0, 0); // This will refresh all Z-indices based on the current _layers()
1196
+ }
1197
+ this.workspaceLayer.batchDraw();
1198
+ shape.on('mouseenter', () => {
1199
+ if (this.isDragging || this.isSelecting || !this.hoverRect)
1200
+ return;
1201
+ // Don't show hover for already selected shapes
1202
+ const selectedNodes = this.transformer?.nodes() || [];
1203
+ if (selectedNodes.includes(shape))
1204
+ return;
1205
+ this.showHover(shape);
1206
+ });
1207
+ shape.on('mouseleave', () => {
1208
+ this.hideHover();
1209
+ });
1210
+ shape.dragBoundFunc((pos) => {
1211
+ const nodes = this.transformer?.nodes() || [];
1212
+ if (nodes.length > 1 && nodes.includes(shape)) {
1213
+ return pos;
1214
+ }
1215
+ const guides = this.getSnappingGuides(shape, pos);
1216
+ let x = pos.x;
1217
+ let y = pos.y;
1218
+ if (guides.vertical.length > 0) {
1219
+ const vGuide = guides.vertical[0];
1220
+ x = vGuide.guide - vGuide.offset + shape.offsetX() * shape.scaleX();
1221
+ }
1222
+ if (guides.horizontal.length > 0) {
1223
+ const hGuide = guides.horizontal[0];
1224
+ y = hGuide.guide - hGuide.offset + shape.offsetY() * shape.scaleY();
1225
+ }
1226
+ return { x, y };
1227
+ });
1228
+ shape.on('dragstart', () => {
1229
+ this.isDragging = true;
1230
+ this.hideHover();
1231
+ });
1232
+ shape.on('dragmove', (e) => {
1233
+ const nodes = this.transformer?.nodes() || [];
1234
+ if (nodes.length > 1 && nodes.includes(shape)) {
1235
+ // If part of group, let transformer.on('dragmove') handle it
1236
+ return;
1237
+ }
1238
+ if (nodes.includes(shape) && nodes.length > 1) {
1239
+ nodes.forEach(node => {
1240
+ this.updateShapeOriginalPosition(node, false);
1241
+ });
1242
+ }
1243
+ else {
1244
+ this.updateShapeOriginalPosition(shape, false);
1245
+ }
1246
+ const absPos = shape.getAbsolutePosition();
1247
+ const guides = this.getSnappingGuides(shape, absPos);
1248
+ this.drawSnappingLines(guides);
1249
+ });
1250
+ shape.on('dragend', () => {
1251
+ this.isDragging = false;
1252
+ this.clearSnapLines();
1253
+ const nodes = this.transformer?.nodes() || [];
1254
+ if (nodes.includes(shape) && nodes.length > 1) {
1255
+ // Batch update signal for all nodes in the group
1256
+ this.updateShapesOriginalPositions(nodes);
1257
+ }
1258
+ else {
1259
+ // Single shape drag - update signal
1260
+ this.updateShapeOriginalPosition(shape);
1261
+ }
1262
+ this.notifyChange();
1263
+ });
1264
+ shape.on('transform', () => {
1265
+ if (shape.name() === 'pattern') {
1266
+ const rect = shape;
1267
+ const scaleX = rect.scaleX();
1268
+ const scaleY = rect.scaleY();
1269
+ rect.width(rect.width() * (scaleX / this.scale()));
1270
+ rect.height(rect.height() * (scaleY / this.scale()));
1271
+ rect.scaleX(this.scale());
1272
+ rect.scaleY(this.scale());
1273
+ rect.offsetX(rect.width() / 2);
1274
+ rect.offsetY(rect.height() / 2);
1275
+ }
1276
+ const absPos = shape.getAbsolutePosition();
1277
+ const guides = this.getSnappingGuides(shape, absPos);
1278
+ this.drawSnappingLines(guides);
1279
+ this.updateShapeOriginalPosition(shape);
1280
+ });
1281
+ shape.on('transformend', () => {
1282
+ this.clearSnapLines();
1283
+ if (!(shape instanceof Konva.Text)) {
1284
+ const scaleX = shape.scaleX();
1285
+ const scaleY = shape.scaleY();
1286
+ shape.width(shape.width() * (scaleX / this.scale()));
1287
+ shape.height(shape.height() * (scaleY / this.scale()));
1288
+ shape.scaleX(this.scale());
1289
+ shape.scaleY(this.scale());
1290
+ shape.offsetX(shape.width() / 2);
1291
+ shape.offsetY(shape.height() / 2);
1292
+ }
1293
+ this.updateShapeOriginalPosition(shape);
1294
+ this.notifyChange();
1295
+ });
1296
+ shape.setAttr('originalX', config.x ?? 0);
1297
+ shape.setAttr('originalY', config.y ?? 0);
1298
+ // Ensure UI layer and its elements are on top
1299
+ this.uiLayer?.moveToTop();
1300
+ this.maskOverlay?.moveToTop();
1301
+ this.transformer?.moveToTop();
1302
+ this.hoverRect?.moveToTop();
1303
+ }
1304
+ return config.id;
1305
+ }
1306
+ updateShapeOriginalPosition(shape, updateSignal = true) {
1307
+ if (!this.canvasRect)
1308
+ return;
1309
+ const scale = this.scale();
1310
+ const originalX = (shape.x() - this.canvasRect.x()) / scale;
1311
+ const originalY = (shape.y() - this.canvasRect.y()) / scale;
1312
+ const originalScaleX = shape.scaleX() / scale;
1313
+ const originalScaleY = shape.scaleY() / scale;
1314
+ shape.setAttr('originalX', originalX);
1315
+ shape.setAttr('originalY', originalY);
1316
+ shape.setAttr('originalScaleX', originalScaleX);
1317
+ shape.setAttr('originalScaleY', originalScaleY);
1318
+ if (updateSignal) {
1319
+ // Update the signal
1320
+ const id = shape.id();
1321
+ this._layers.update(layers => layers.map(l => l.id === id ? {
1322
+ ...l,
1323
+ x: originalX,
1324
+ y: originalY,
1325
+ width: shape.width(),
1326
+ height: shape.height(),
1327
+ scaleX: originalScaleX,
1328
+ scaleY: originalScaleY,
1329
+ rotation: shape.rotation()
1330
+ } : l));
1331
+ }
1332
+ }
1333
+ updateShapesOriginalPositions(shapes) {
1334
+ if (!this.canvasRect)
1335
+ return;
1336
+ const scale = this.scale();
1337
+ const updates = new Map();
1338
+ shapes.forEach(shape => {
1339
+ const originalX = (shape.x() - this.canvasRect.x()) / scale;
1340
+ const originalY = (shape.y() - this.canvasRect.y()) / scale;
1341
+ const originalScaleX = shape.scaleX() / scale;
1342
+ const originalScaleY = shape.scaleY() / scale;
1343
+ shape.setAttr('originalX', originalX);
1344
+ shape.setAttr('originalY', originalY);
1345
+ shape.setAttr('originalScaleX', originalScaleX);
1346
+ shape.setAttr('originalScaleY', originalScaleY);
1347
+ updates.set(shape.id(), {
1348
+ x: originalX,
1349
+ y: originalY,
1350
+ width: shape.width(),
1351
+ height: shape.height(),
1352
+ scaleX: originalScaleX,
1353
+ scaleY: originalScaleY,
1354
+ rotation: shape.rotation()
1355
+ });
1356
+ });
1357
+ // Update the signal once for all shapes
1358
+ this._layers.update(layers => layers.map(l => {
1359
+ const update = updates.get(l.id);
1360
+ return update ? { ...l, ...update } : l;
1361
+ }));
1362
+ }
1363
+ getNodesClientRect(nodes) {
1364
+ if (nodes.length === 0)
1365
+ return { x: 0, y: 0, width: 0, height: 0 };
1366
+ let minX = Infinity;
1367
+ let minY = Infinity;
1368
+ let maxX = -Infinity;
1369
+ let maxY = -Infinity;
1370
+ nodes.forEach(node => {
1371
+ const box = node.getClientRect();
1372
+ minX = Math.min(minX, box.x);
1373
+ minY = Math.min(minY, box.y);
1374
+ maxX = Math.max(maxX, box.x + box.width);
1375
+ maxY = Math.max(maxY, box.y + box.height);
1376
+ });
1377
+ return {
1378
+ x: minX,
1379
+ y: minY,
1380
+ width: maxX - minX,
1381
+ height: maxY - minY
1382
+ };
1383
+ }
1384
+ getSnappingGuides(targetNode, pos) {
1385
+ if (!this.canvasRect || !this.workspaceLayer)
1386
+ return { vertical: [], horizontal: [] };
1387
+ const nodes = Array.isArray(targetNode) ? targetNode : [targetNode];
1388
+ const isGroup = nodes.length > 1;
1389
+ const snapRange = this.snapSettings.snapRange;
1390
+ const resultV = [];
1391
+ const resultH = [];
1392
+ // Bases for snapping
1393
+ const basesV = [];
1394
+ const basesH = [];
1395
+ // 1. Stage centers and borders
1396
+ if (this.snapSettings.snapToStageCenter || this.snapSettings.snapToStageBorders) {
1397
+ const cw = this.canvasRect.width() * this.scale();
1398
+ const ch = this.canvasRect.height() * this.scale();
1399
+ const cx = this.canvasRect.x();
1400
+ const cy = this.canvasRect.y();
1401
+ if (this.snapSettings.snapToStageBorders) {
1402
+ basesV.push(cx); // Left
1403
+ basesV.push(cx + cw); // Right
1404
+ basesH.push(cy); // Top
1405
+ basesH.push(cy + ch); // Bottom
1406
+ }
1407
+ if (this.snapSettings.snapToStageCenter) {
1408
+ basesV.push(cx + cw / 2); // Center V
1409
+ basesH.push(cy + ch / 2); // Center H
1410
+ }
1411
+ }
1412
+ // 2. Other shapes
1413
+ if (this.snapSettings.snapToShapes) {
1414
+ const selectedNodes = this.transformer?.nodes() || [];
1415
+ this.workspaceLayer.getChildren().forEach((child) => {
1416
+ if (nodes.includes(child) || child === this.canvasRect || child.name() === 'selection-rect' || (selectedNodes.length > 0 && selectedNodes.includes(child)))
1417
+ return;
1418
+ const box = child.getClientRect();
1419
+ basesV.push(box.x);
1420
+ basesV.push(box.x + box.width / 2);
1421
+ basesV.push(box.x + box.width);
1422
+ basesH.push(box.y);
1423
+ basesH.push(box.y + box.height / 2);
1424
+ basesH.push(box.y + box.height);
1425
+ });
1426
+ }
1427
+ const targetBox = isGroup ? this.getNodesClientRect(nodes) : nodes[0].getClientRect();
1428
+ // If pos is provided (from dragBoundFunc for single node), we use it.
1429
+ // For group drag via transformer, we don't use pos here because we apply delta later.
1430
+ let targetX = targetBox.x;
1431
+ let targetY = targetBox.y;
1432
+ if (!isGroup && pos) {
1433
+ targetX = pos.x - nodes[0].offsetX() * nodes[0].scaleX();
1434
+ targetY = pos.y - nodes[0].offsetY() * nodes[0].scaleY();
1435
+ }
1436
+ const targetWidth = targetBox.width;
1437
+ const targetHeight = targetBox.height;
1438
+ basesV.forEach((base) => {
1439
+ const vSnaps = [
1440
+ { guide: base, offset: 0, diff: Math.abs(base - targetX) },
1441
+ { guide: base, offset: targetWidth / 2, diff: Math.abs(base - (targetX + targetWidth / 2)) },
1442
+ { guide: base, offset: targetWidth, diff: Math.abs(base - (targetX + targetWidth)) }
1443
+ ];
1444
+ vSnaps.forEach(s => {
1445
+ if (s.diff < snapRange) {
1446
+ resultV.push(s);
1447
+ }
1448
+ });
1449
+ });
1450
+ basesH.forEach((base) => {
1451
+ const hSnaps = [
1452
+ { guide: base, offset: 0, diff: Math.abs(base - targetY) },
1453
+ { guide: base, offset: targetHeight / 2, diff: Math.abs(base - (targetY + targetHeight / 2)) },
1454
+ { guide: base, offset: targetHeight, diff: Math.abs(base - (targetY + targetHeight)) }
1455
+ ];
1456
+ hSnaps.forEach(s => {
1457
+ if (s.diff < snapRange) {
1458
+ resultH.push(s);
1459
+ }
1460
+ });
1461
+ });
1462
+ return {
1463
+ vertical: resultV.sort((a, b) => a.diff - b.diff),
1464
+ horizontal: resultH.sort((a, b) => a.diff - b.diff)
1465
+ };
1466
+ }
1467
+ drawSnappingLines(guides) {
1468
+ this.clearSnapLines();
1469
+ if (!this.uiLayer || !this.snapSettings.showGuidelines)
1470
+ return;
1471
+ const vGuide = guides.vertical[0];
1472
+ const hGuide = guides.horizontal[0];
1473
+ if (vGuide) {
1474
+ const line = new Konva.Line({
1475
+ points: [vGuide.guide, 0, vGuide.guide, this.stage.height()],
1476
+ stroke: this.snapSettings.guidelineColor,
1477
+ strokeWidth: 1,
1478
+ dash: [4, 6],
1479
+ name: 'snap-line'
1480
+ });
1481
+ this.uiLayer.add(line);
1482
+ this.snapLines.push(line);
1483
+ }
1484
+ if (hGuide) {
1485
+ const line = new Konva.Line({
1486
+ points: [0, hGuide.guide, this.stage.width(), hGuide.guide],
1487
+ stroke: this.snapSettings.guidelineColor,
1488
+ strokeWidth: 1,
1489
+ dash: [4, 6],
1490
+ name: 'snap-line'
1491
+ });
1492
+ this.uiLayer.add(line);
1493
+ this.snapLines.push(line);
1494
+ }
1495
+ this.uiLayer.batchDraw();
1496
+ }
1497
+ clearSnapLines() {
1498
+ this.snapLines.forEach(l => l.destroy());
1499
+ this.snapLines = [];
1500
+ this.uiLayer?.batchDraw();
1501
+ }
1502
+ consecutiveUpdatesCount = 0;
1503
+ lastUpdateSizeTime = 0;
1504
+ lastUpdateSizeDimensions = { width: 0, height: 0 };
1505
+ isSnapshotLoading = false;
1506
+ setupResizeObserver(container, imageSize) {
1507
+ this.resizeObserver = new ResizeObserver(() => {
1508
+ // Use requestAnimationFrame to avoid "ResizeObserver loop completed with undelivered notifications" error
1509
+ // This ensures that the size update happens in the next frame after the layout has settled.
1510
+ requestAnimationFrame(() => {
1511
+ if (this.stage) {
1512
+ this.updateSize(container, imageSize);
1513
+ }
1514
+ });
1515
+ });
1516
+ this.resizeObserver.observe(container);
1517
+ }
1518
+ updateSize(container, imageSize, fitToContainer = false, notify = false) {
1519
+ if (!this.stage || !this.canvasRect || !this.workspaceLayer || !this.uiLayer) {
1520
+ return;
1521
+ }
1522
+ const now = Date.now();
1523
+ if (this.lastUpdateSizeTime && now - this.lastUpdateSizeTime < 50) {
1524
+ this.consecutiveUpdatesCount++;
1525
+ if (this.consecutiveUpdatesCount > 100) {
1526
+ console.warn('ImageDesignerService.updateSize: Potential infinite loop detected, aborting');
1527
+ return;
1528
+ }
1529
+ }
1530
+ else {
1531
+ this.consecutiveUpdatesCount = 0;
1532
+ }
1533
+ this.lastUpdateSizeTime = now;
1534
+ this.lastUpdateSizeDimensions = { width: imageSize.width, height: imageSize.height };
1535
+ const width = Math.round(container.offsetWidth || imageSize.width || 800);
1536
+ const height = Math.round(container.offsetHeight || imageSize.height || 600);
1537
+ console.log('ImageDesignerService.updateSize dimensions:', width, 'x', height);
1538
+ // Skip if size is effectively 0 to avoid incorrect centering
1539
+ if (width === 0 || height === 0) {
1540
+ console.warn('ImageDesignerService.updateSize: dimensions are 0, skipping');
1541
+ return;
1542
+ }
1543
+ if (fitToContainer) {
1544
+ const padding = 40;
1545
+ const availableWidth = width - padding * 2;
1546
+ const availableHeight = height - padding * 2;
1547
+ const scaleX = availableWidth / (imageSize.width || 1);
1548
+ const scaleY = availableHeight / (imageSize.height || 1);
1549
+ const newScale = Math.min(scaleX, scaleY);
1550
+ const clampedScale = Math.min(Math.max(newScale, this.minScale), this.maxScale);
1551
+ this.scale.set(clampedScale);
1552
+ }
1553
+ // Skip if size hasn't changed to avoid unnecessary re-draws and potential loops (unless scale is being updated)
1554
+ // Use a small epsilon to avoid loops due to sub-pixel changes
1555
+ const sizeChanged = Math.abs(this.stage.width() - width) > 0.5 || Math.abs(this.stage.height() - height) > 0.5;
1556
+ const canvasSizeChanged = Math.abs(this.canvasRect.width() - imageSize.width) > 0.5 || Math.abs(this.canvasRect.height() - imageSize.height) > 0.5;
1557
+ const scaleChanged = Math.abs(this.canvasRect.scaleX() - this.scale()) > 0.001;
1558
+ if (!sizeChanged && !scaleChanged && !canvasSizeChanged) {
1559
+ return;
1560
+ }
1561
+ if (isNaN(width) || isNaN(height)) {
1562
+ console.warn('ImageDesignerService.updateSize: width or height is NaN', { width, height, container, imageSize });
1563
+ return;
1564
+ }
1565
+ console.log('Updating stage size to:', width, 'x', height);
1566
+ this.stage.width(width);
1567
+ this.stage.height(height);
1568
+ const canvasWidth = imageSize.width || 0;
1569
+ const canvasHeight = imageSize.height || 0;
1570
+ console.log('Setting canvasRect size to:', canvasWidth, 'x', canvasHeight);
1571
+ this.canvasRect.width(canvasWidth);
1572
+ this.canvasRect.height(canvasHeight);
1573
+ this.canvasRect.offsetX(canvasWidth / 2);
1574
+ this.canvasRect.offsetY(canvasHeight / 2);
1575
+ const canvasX = width / 2;
1576
+ const canvasY = height / 2;
1577
+ if (!isNaN(canvasX) && !isNaN(canvasY)) {
1578
+ console.log('Centering canvasRect at:', canvasX, canvasY);
1579
+ this.canvasRect.x(canvasX);
1580
+ this.canvasRect.y(canvasY);
1581
+ this.canvasRect.scaleX(this.scale());
1582
+ this.canvasRect.scaleY(this.scale());
1583
+ // Update positions of all elements in workspaceLayer relative to new canvas position
1584
+ this.workspaceLayer.getChildren().forEach(child => {
1585
+ if (child !== this.canvasRect && child.name() !== 'transformer') {
1586
+ const originalX = child.attrs['originalX'] ?? 0;
1587
+ const originalY = child.attrs['originalY'] ?? 0;
1588
+ child.x(originalX * this.scale() + canvasX);
1589
+ child.y(originalY * this.scale() + canvasY);
1590
+ // Use the original scale for layers that have one (like flipped ones)
1591
+ const originalScaleX = child.attrs['originalScaleX'] ?? 1;
1592
+ const originalScaleY = child.attrs['originalScaleY'] ?? 1;
1593
+ child.scaleX(this.scale() * originalScaleX);
1594
+ child.scaleY(this.scale() * originalScaleY);
1595
+ }
1596
+ });
1597
+ this.selectionRect?.visible(false); // Hide selection rect on resize as its coordinates are now invalid
1598
+ this.uiLayer.moveToTop(); // Ensure UI layer is always on top of workspace layer
1599
+ this.maskOverlay?.moveToTop(); // Mask on top of everything for clipping effect
1600
+ this.transformer?.moveToTop();
1601
+ this.hoverRect?.moveToTop();
1602
+ this.transformer?.forceUpdate();
1603
+ this.updateSelectionBorders();
1604
+ if (this.hoveredShape && this.hoverRect?.visible()) {
1605
+ this.showHover(this.hoveredShape);
1606
+ }
1607
+ }
1608
+ else {
1609
+ console.warn('ImageDesignerService.updateSize: Calculated canvas positions are NaN', { canvasX, canvasY, width, height, canvasWidth, scale: this.scale() });
1610
+ }
1611
+ this.uiLayer?.batchDraw();
1612
+ this.workspaceLayer?.batchDraw();
1613
+ if (notify) {
1614
+ this.notifyChange();
1615
+ }
1616
+ }
1617
+ setScale(scale) {
1618
+ const clampedScale = Math.min(Math.max(scale, this.minScale), this.maxScale);
1619
+ if (!this.isBrowser) {
1620
+ this.scale.set(clampedScale);
1621
+ return;
1622
+ }
1623
+ this.scale.set(clampedScale);
1624
+ if (this.stage && this.canvasRect && this.workspaceLayer && this.uiLayer) {
1625
+ const container = this.stage.container();
1626
+ const imageSize = {
1627
+ width: this.canvasRect.width(),
1628
+ height: this.canvasRect.height()
1629
+ };
1630
+ // Update canvas positions and scales based on new scale
1631
+ this.updateSize(container, imageSize);
1632
+ this.transformer?.forceUpdate();
1633
+ this.uiLayer.batchDraw();
1634
+ this.workspaceLayer.batchDraw();
1635
+ }
1636
+ }
1637
+ zoomIn() {
1638
+ this.setScale(this.scale() + 0.1);
1639
+ }
1640
+ zoomOut() {
1641
+ this.setScale(this.scale() - 0.1);
1642
+ }
1643
+ setMinMaxScale(minScale, maxScale) {
1644
+ this.minScale = minScale;
1645
+ this.maxScale = maxScale;
1646
+ this.setScale(this.scale());
1647
+ }
1648
+ updateSnapSettings(settings) {
1649
+ this.snapSettings = { ...this.snapSettings, ...settings };
1650
+ }
1651
+ setCanvasBackground(config, notify = true) {
1652
+ if (!this.canvasRect)
1653
+ return;
1654
+ if (typeof config === 'string') {
1655
+ this.canvasRect.fill(config);
1656
+ this.canvasRect.fillLinearGradientStartPoint(null);
1657
+ this.canvasRect.fillLinearGradientEndPoint(null);
1658
+ this.canvasRect.fillLinearGradientColorStops([]);
1659
+ }
1660
+ else {
1661
+ this.canvasRect.fill(null);
1662
+ this.canvasRect.fillLinearGradientStartPoint({ x: config.x0 || 0, y: config.y0 || 0 });
1663
+ this.canvasRect.fillLinearGradientEndPoint({ x: config.x1 || 0, y: config.y1 || 0 });
1664
+ this.canvasRect.fillLinearGradientColorStops(config.colorStops);
1665
+ }
1666
+ this.workspaceLayer?.batchDraw();
1667
+ if (notify) {
1668
+ this.notifyChange();
1669
+ }
1670
+ }
1671
+ reorderLayers(previousIndex, currentIndex) {
1672
+ if (!this.workspaceLayer || previousIndex === currentIndex)
1673
+ return;
1674
+ let updatedLayers = [];
1675
+ this._layers.update(layers => {
1676
+ const newLayers = [...layers];
1677
+ moveItemInArray(newLayers, previousIndex, currentIndex);
1678
+ // Update Konva nodes Z-index based on the new array order.
1679
+ // Array: [Top, ..., Bottom]
1680
+ // Konva: [Bottom, ..., Top]
1681
+ // So we need to reverse the array when applying to Konva Z-indices.
1682
+ const reversedLayers = [...newLayers].reverse();
1683
+ reversedLayers.forEach((layer, index) => {
1684
+ const shape = this.workspaceLayer?.findOne('#' + layer.id);
1685
+ if (shape) {
1686
+ // Layers start from zIndex 1 because canvasRect is at 0
1687
+ shape.zIndex(index + 1);
1688
+ }
1689
+ });
1690
+ // Ensure canvasRect is at the bottom
1691
+ this.canvasRect?.zIndex(0);
1692
+ // Ensure transformer, selectionRect and hoverRect are always on top
1693
+ this.uiLayer?.moveToTop();
1694
+ this.maskOverlay?.moveToTop();
1695
+ this.transformer?.moveToTop();
1696
+ this.selectionRect?.moveToTop();
1697
+ this.hoverRect?.moveToTop();
1698
+ this.workspaceLayer?.batchDraw();
1699
+ this.uiLayer?.batchDraw();
1700
+ updatedLayers = newLayers;
1701
+ return newLayers;
1702
+ });
1703
+ if (updatedLayers.length > 0) {
1704
+ this.notifyChange();
1705
+ }
1706
+ }
1707
+ currentSnapshotVersion = 0;
1708
+ getSnapshot() {
1709
+ return {
1710
+ version: this.currentSnapshotVersion,
1711
+ imageSize: {
1712
+ width: this.canvasRect?.width() || 0,
1713
+ height: this.canvasRect?.height() || 0
1714
+ },
1715
+ layers: this._layers().map(layer => {
1716
+ // Ensure that any temporary 'selected' property is not included in the snapshot
1717
+ const { selected, ...rest } = layer;
1718
+ return rest;
1719
+ }),
1720
+ background: this.canvasRect?.fill() || 'white',
1721
+ backgroundConfig: this.canvasRect?.fillLinearGradientColorStops()?.length ? {
1722
+ x0: this.canvasRect.fillLinearGradientStartPoint().x,
1723
+ y0: this.canvasRect.fillLinearGradientStartPoint().y,
1724
+ x1: this.canvasRect.fillLinearGradientEndPoint().x,
1725
+ y1: this.canvasRect.fillLinearGradientEndPoint().y,
1726
+ colorStops: this.canvasRect.fillLinearGradientColorStops()
1727
+ } : undefined
1728
+ };
1729
+ }
1730
+ async loadSnapshot(snapshot, resetHistory = false) {
1731
+ if (!this.stage || !this.workspaceLayer || !this.canvasRect) {
1732
+ console.warn('loadSnapshot called but service is not fully initialized');
1733
+ return;
1734
+ }
1735
+ if (this.isSnapshotLoading) {
1736
+ console.log('loadSnapshot ignored - already loading');
1737
+ return;
1738
+ }
1739
+ this.isSnapshotLoading = true;
1740
+ try {
1741
+ console.log('loadSnapshot starting', snapshot);
1742
+ this.currentSnapshotVersion = snapshot.version ?? 0;
1743
+ // Clear current state
1744
+ this.selectedLayerId.set(null);
1745
+ this.transformer?.nodes([]);
1746
+ // Remove all shapes from workspace except canvasRect
1747
+ // Critical: iterate over a copy of the children array, as destroy() modifies it
1748
+ const children = [...this.workspaceLayer.getChildren()];
1749
+ children.forEach((child) => {
1750
+ if (child !== this.canvasRect) {
1751
+ child.destroy();
1752
+ }
1753
+ });
1754
+ const { imageSize, layers, background, backgroundConfig } = snapshot;
1755
+ // Update size BEFORE adding layers so that addLayer uses correct canvas position and scale
1756
+ this.updateSize(this.stage.container(), imageSize);
1757
+ // Restore background
1758
+ if (backgroundConfig) {
1759
+ this.setCanvasBackground(backgroundConfig, false);
1760
+ }
1761
+ else if (background) {
1762
+ this.setCanvasBackground(background, false);
1763
+ }
1764
+ // Restore layers
1765
+ const reversedLayers = [...layers].reverse();
1766
+ const newLayers = [];
1767
+ for (const layerConfig of reversedLayers) {
1768
+ const id = layerConfig.id || Math.random().toString(36).substr(2, 9);
1769
+ const configWithId = { ...layerConfig, id };
1770
+ // We add layers to Konva but don't update the signal inside addLayer
1771
+ await this.addLayer(configWithId, false);
1772
+ // We collect them in the same order as in addLayer(..., true) which is [newConfig, ...layers]
1773
+ newLayers.unshift(configWithId);
1774
+ }
1775
+ this._layers.set(newLayers);
1776
+ this.workspaceLayer.batchDraw();
1777
+ this.uiLayer?.batchDraw();
1778
+ }
1779
+ finally {
1780
+ this.isSnapshotLoading = false;
1781
+ if (resetHistory) {
1782
+ this.resetHistory();
1783
+ }
1784
+ else {
1785
+ this.notifyChange(false);
1786
+ this.updateHistorySignals();
1787
+ }
1788
+ }
1789
+ }
1790
+ async loadAllFonts() {
1791
+ const fontPromises = [];
1792
+ // Always load default font
1793
+ fontPromises.push(this.loadFont(this.defaultFont));
1794
+ this._layers().forEach(layer => {
1795
+ if (layer.type === 'text' && layer['fontFamily']) {
1796
+ fontPromises.push(this.loadFont(layer['fontFamily']));
1797
+ }
1798
+ else if (layer.type === 'text') {
1799
+ fontPromises.push(this.loadFont(this.defaultFont));
1800
+ }
1801
+ });
1802
+ await Promise.all(fontPromises);
1803
+ }
1804
+ loadFont(fontFamily) {
1805
+ if (!this.isBrowser || this.loadedFonts.has(fontFamily)) {
1806
+ return Promise.resolve();
1807
+ }
1808
+ return new Promise((resolve) => {
1809
+ const link = document.createElement('link');
1810
+ link.rel = 'stylesheet';
1811
+ link.href = `https://fonts.googleapis.com/css2?family=${fontFamily.replace(/ /g, '+')}:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap`;
1812
+ link.onload = () => {
1813
+ this.loadedFonts.add(fontFamily);
1814
+ // Wait for font to be ready for rendering
1815
+ if ('fonts' in document) {
1816
+ document.fonts.load(`1em "${fontFamily}"`).then(() => {
1817
+ resolve();
1818
+ }).catch(() => {
1819
+ resolve();
1820
+ });
1821
+ }
1822
+ else {
1823
+ // Fallback: small timeout
1824
+ setTimeout(() => resolve(), 100);
1825
+ }
1826
+ };
1827
+ link.onerror = () => {
1828
+ console.error(`Failed to load font: ${fontFamily}`);
1829
+ resolve();
1830
+ };
1831
+ document.head.appendChild(link);
1832
+ });
1833
+ }
1834
+ getLayerThumbnail(id, effectId) {
1835
+ if (!this.workspaceLayer)
1836
+ return null;
1837
+ const shape = this.workspaceLayer.findOne('#' + id);
1838
+ if (!shape)
1839
+ return null;
1840
+ try {
1841
+ if (effectId) {
1842
+ // Clone the shape to apply the effect without affecting the original
1843
+ const clone = shape.clone();
1844
+ const layer = this._layers().find(l => l.id === id);
1845
+ if (layer) {
1846
+ const tempConfig = { ...layer, effect: effectId };
1847
+ this.applyEffectsToShape(clone, tempConfig);
1848
+ }
1849
+ const dataUrl = clone.toDataURL({
1850
+ pixelRatio: 0.5,
1851
+ quality: 0.5
1852
+ });
1853
+ clone.destroy();
1854
+ return dataUrl;
1855
+ }
1856
+ // Create a small thumbnail of the shape
1857
+ return shape.toDataURL({
1858
+ pixelRatio: 0.5,
1859
+ quality: 0.5
1860
+ });
1861
+ }
1862
+ catch (e) {
1863
+ console.warn('Failed to generate thumbnail for layer', id, e);
1864
+ return null;
1865
+ }
1866
+ }
1867
+ async updateLayer(id, config) {
1868
+ const layer = this._layers().find(l => l.id === id);
1869
+ if (!layer)
1870
+ return;
1871
+ const shape = this.workspaceLayer?.findOne('#' + id);
1872
+ if (config['scaleX'] !== undefined && shape) {
1873
+ shape.scaleX(config['scaleX']);
1874
+ }
1875
+ if (config['scaleY'] !== undefined && shape) {
1876
+ shape.scaleY(config['scaleY']);
1877
+ }
1878
+ if (config['x'] !== undefined && shape) {
1879
+ shape.x(config['x']);
1880
+ }
1881
+ if (config['y'] !== undefined && shape) {
1882
+ shape.y(config['y']);
1883
+ }
1884
+ if (config['rotation'] !== undefined && shape) {
1885
+ shape.rotation(config['rotation']);
1886
+ }
1887
+ if (config['width'] !== undefined && shape && !(shape instanceof Konva.Text)) {
1888
+ shape.width(config['width']);
1889
+ shape.offsetX(shape.width() / 2);
1890
+ }
1891
+ if (config['height'] !== undefined && shape && !(shape instanceof Konva.Text)) {
1892
+ shape.height(config['height']);
1893
+ shape.offsetY(shape.height() / 2);
1894
+ }
1895
+ if (config['width'] !== undefined && shape instanceof Konva.Text) {
1896
+ shape.width(config['width']);
1897
+ shape.offsetX(shape.width() / 2);
1898
+ }
1899
+ if (shape) {
1900
+ this.updateShapeOriginalPosition(shape, true);
1901
+ }
1902
+ this._layers.update(layers => layers.map(l => l.id === id ? { ...l, ...config } : l));
1903
+ const updatedLayer = this._layers().find(l => l.id === id);
1904
+ if (shape && updatedLayer) {
1905
+ this.applyEffectsToShape(shape, updatedLayer);
1906
+ }
1907
+ // If font-related properties changed, ensure the font variant is loaded and applied
1908
+ if (this.isBrowser && shape instanceof Konva.Text && (config['fontFamily'] !== undefined ||
1909
+ config['fontWeight'] !== undefined ||
1910
+ config['fontStyle'] !== undefined)) {
1911
+ const family = config['fontFamily'] || shape.fontFamily();
1912
+ const weight = config['fontWeight'] || shape.getAttr('fontWeight') || 400;
1913
+ const isItalic = (config['fontStyle'] === 'italic') || (shape.fontStyle() || '').includes('italic');
1914
+ const fontStr = `${isItalic ? 'italic ' : ''}${weight} 12px "${family}"`;
1915
+ if (config['fontFamily']) {
1916
+ await this.loadFont(config['fontFamily']);
1917
+ this.workspaceLayer?.batchDraw();
1918
+ }
1919
+ try {
1920
+ await document.fonts.load(fontStr);
1921
+ this.workspaceLayer?.batchDraw();
1922
+ }
1923
+ catch (e) {
1924
+ console.warn('Failed to load font variant:', fontStr);
1925
+ }
1926
+ }
1927
+ if (shape) {
1928
+ if (config.fill !== undefined) {
1929
+ const type = config.type || layer.type;
1930
+ if (type === 'shape' || type === 'pattern') {
1931
+ // For SVG shapes and patterns, we can replace the color in the SVG data.
1932
+ const currentLayer = this._layers().find(l => l.id === id);
1933
+ const dataKey = type === 'shape' ? 'data' : 'patternImage';
1934
+ const dataUrl = currentLayer?.[dataKey];
1935
+ if (typeof dataUrl === 'string' && dataUrl.startsWith('data:image/svg+xml') &&
1936
+ config.fill && !config.fill.startsWith('data:image') && !config.fill.startsWith('http')) {
1937
+ const commaIndex = dataUrl.indexOf(',');
1938
+ if (commaIndex !== -1) {
1939
+ const header = dataUrl.substring(0, commaIndex);
1940
+ const isBase64 = header.includes('base64');
1941
+ const svgText = isBase64
1942
+ ? atob(dataUrl.substring(commaIndex + 1))
1943
+ : decodeURIComponent(dataUrl.substring(commaIndex + 1));
1944
+ const newFill = config.fill;
1945
+ // Replace both fill and stroke colors
1946
+ const newSvgText = svgText
1947
+ .replace(/fill="[#][0-9a-fA-F]{3,6}"/g, `fill="${newFill}"`)
1948
+ .replace(/fill='%23[0-9a-fA-F]{3,6}'/g, `fill='${newFill}'`)
1949
+ .replace(/stroke="[#][0-9a-fA-F]{3,6}"/g, `stroke="${newFill}"`)
1950
+ .replace(/stroke='%23[0-9a-fA-F]{3,6}'/g, `stroke='${newFill}'`);
1951
+ const newDataUrl = isBase64
1952
+ ? `${header},${btoa(newSvgText)}`
1953
+ : `${header},${encodeURIComponent(newSvgText)}`;
1954
+ const img = new Image();
1955
+ img.crossOrigin = 'anonymous';
1956
+ img.onload = () => {
1957
+ if (type === 'shape') {
1958
+ shape.image(img);
1959
+ }
1960
+ else {
1961
+ shape.setAttr('fillPatternImage', img);
1962
+ }
1963
+ this.workspaceLayer?.batchDraw();
1964
+ };
1965
+ img.src = newDataUrl;
1966
+ // Update the data in the signal too
1967
+ this._layers.update(layers => layers.map(l => l.id === id ? { ...l, [dataKey]: newDataUrl } : l));
1968
+ }
1969
+ }
1970
+ else if (config.fill && (config.fill.startsWith('data:image') || config.fill.startsWith('http'))) {
1971
+ const img = new Image();
1972
+ img.crossOrigin = 'anonymous';
1973
+ img.onload = () => {
1974
+ shape.setAttr('fillPatternImage', img);
1975
+ this.workspaceLayer?.batchDraw();
1976
+ };
1977
+ img.src = config.fill;
1978
+ if (type === 'pattern') {
1979
+ this._layers.update(layers => layers.map(l => l.id === id ? { ...l, patternImage: config.fill } : l));
1980
+ }
1981
+ }
1982
+ else {
1983
+ shape.setAttr('fill', config.fill);
1984
+ if (type !== 'pattern') {
1985
+ shape.setAttr('fillPatternImage', null);
1986
+ }
1987
+ }
1988
+ }
1989
+ else if (config.fill && (config.fill.startsWith('data:image') || config.fill.startsWith('http'))) {
1990
+ // It's a pattern for other types
1991
+ const img = new Image();
1992
+ img.crossOrigin = 'anonymous';
1993
+ img.onload = () => {
1994
+ shape.setAttr('fillPatternImage', img);
1995
+ this.workspaceLayer?.batchDraw();
1996
+ };
1997
+ img.src = config.fill;
1998
+ }
1999
+ else {
2000
+ shape.setAttr('fill', config.fill);
2001
+ shape.setAttr('fillPatternImage', null);
2002
+ }
2003
+ }
2004
+ if (config['opacity'] !== undefined) {
2005
+ shape.opacity(config['opacity']);
2006
+ }
2007
+ if (config['text'] !== undefined && shape instanceof Konva.Text) {
2008
+ shape.text(config['text']);
2009
+ // Update offset because content changed
2010
+ shape.offsetX(shape.width() / 2);
2011
+ shape.offsetY(shape.height() / 2);
2012
+ }
2013
+ if (config['fontSize'] !== undefined && shape instanceof Konva.Text) {
2014
+ shape.fontSize(config['fontSize']);
2015
+ // Update offset because size changed
2016
+ shape.offsetX(shape.width() / 2);
2017
+ shape.offsetY(shape.height() / 2);
2018
+ }
2019
+ if (config['width'] !== undefined && shape instanceof Konva.Text) {
2020
+ shape.width(config['width'] * this.scale());
2021
+ // Update offset because width changed
2022
+ shape.offsetX(shape.width() / 2);
2023
+ shape.offsetY(shape.height() / 2);
2024
+ }
2025
+ if (config['fontFamily'] !== undefined && shape instanceof Konva.Text) {
2026
+ shape.fontFamily(config['fontFamily']);
2027
+ }
2028
+ if (config['fontWeight'] !== undefined && shape instanceof Konva.Text) {
2029
+ shape.setAttr('fontWeight', config['fontWeight']);
2030
+ // Konva's fontStyle property is used for weight and style.
2031
+ // If we have numeric fontWeight, we can try to use it directly in fontStyle as well.
2032
+ const currentStyle = (shape.fontStyle() || '').includes('italic') ? 'italic' : '';
2033
+ shape.fontStyle(`${config['fontWeight']} ${currentStyle}`.trim());
2034
+ // Update offset because size changed
2035
+ shape.offsetX(shape.width() / 2);
2036
+ shape.offsetY(shape.height() / 2);
2037
+ // Force text refresh to update layout before transformer update
2038
+ this.workspaceLayer?.batchDraw();
2039
+ }
2040
+ if (config['fontStyle'] !== undefined && shape instanceof Konva.Text) {
2041
+ // If fontStyle is provided, it might override fontWeight if we are not careful.
2042
+ // But usually fontStyle in this app means 'italic' or 'normal'.
2043
+ const currentWeight = shape.getAttr('fontWeight') || 400;
2044
+ if (config['fontStyle'] === 'italic') {
2045
+ shape.fontStyle(currentWeight + ' italic');
2046
+ }
2047
+ else {
2048
+ shape.fontStyle(currentWeight.toString());
2049
+ }
2050
+ // Update offset because size changed
2051
+ shape.offsetX(shape.width() / 2);
2052
+ shape.offsetY(shape.height() / 2);
2053
+ // Force text refresh to update layout before transformer update
2054
+ this.workspaceLayer?.batchDraw();
2055
+ }
2056
+ if (config['textDecoration'] !== undefined && shape instanceof Konva.Text) {
2057
+ shape.textDecoration(config['textDecoration']);
2058
+ }
2059
+ if (config['align'] !== undefined && shape instanceof Konva.Text) {
2060
+ shape.align(config['align']);
2061
+ }
2062
+ if (config['lineHeight'] !== undefined && shape instanceof Konva.Text) {
2063
+ shape.lineHeight(config['lineHeight']);
2064
+ }
2065
+ if (config['letterSpacing'] !== undefined && shape instanceof Konva.Text) {
2066
+ shape.letterSpacing(config['letterSpacing']);
2067
+ }
2068
+ if (config['textCase'] !== undefined && shape instanceof Konva.Text) {
2069
+ const text = layer.text || '';
2070
+ if (config['textCase'] === 'upper') {
2071
+ shape.text(text.toUpperCase());
2072
+ }
2073
+ else {
2074
+ shape.text(text);
2075
+ }
2076
+ }
2077
+ this.workspaceLayer?.batchDraw();
2078
+ this.uiLayer?.batchDraw();
2079
+ this.transformer?.forceUpdate();
2080
+ this.updateSelectionBorders();
2081
+ this.notifyChange();
2082
+ // If text properties changed, update again after a short delay
2083
+ // to handle cases where text layout hasn't updated yet.
2084
+ if (shape instanceof Konva.Text && (config['fontFamily'] !== undefined ||
2085
+ config['fontSize'] !== undefined ||
2086
+ config['text'] !== undefined ||
2087
+ config['fontWeight'] !== undefined ||
2088
+ config['fontStyle'] !== undefined)) {
2089
+ setTimeout(() => {
2090
+ if (shape instanceof Konva.Text) {
2091
+ // Re-calculate offsets one more time as dimensions might have changed
2092
+ // after the browser fully applied the font metrics.
2093
+ shape.offsetX(shape.width() / 2);
2094
+ shape.offsetY(shape.height() / 2);
2095
+ }
2096
+ if (this.transformer) {
2097
+ // Re-assigning nodes is the most reliable way to force Transformer
2098
+ // to re-calculate its boundaries and adapt to the new text size.
2099
+ const currentNodes = this.transformer.nodes();
2100
+ this.transformer.nodes([]);
2101
+ this.transformer.nodes(currentNodes);
2102
+ }
2103
+ this.workspaceLayer?.batchDraw();
2104
+ this.uiLayer?.batchDraw();
2105
+ this.transformer?.forceUpdate();
2106
+ this.updateSelectionBorders();
2107
+ }, 100);
2108
+ }
2109
+ }
2110
+ }
2111
+ getBase64Image() {
2112
+ if (!this.stage || !this.canvasRect) {
2113
+ return undefined;
2114
+ }
2115
+ // Capture original state
2116
+ const wasUiLayerVisible = this.uiLayer?.visible() || false;
2117
+ const originalStroke = this.canvasRect.stroke();
2118
+ const originalStrokeWidth = this.canvasRect.strokeWidth();
2119
+ // Hide UI layer to exclude transformers, snap lines, etc. from export
2120
+ this.uiLayer?.visible(false);
2121
+ // Temporarily hide canvas border
2122
+ this.canvasRect.stroke(null);
2123
+ this.canvasRect.strokeWidth(0);
2124
+ // Get the client rect of the canvas area relative to the stage
2125
+ const rect = this.canvasRect.getClientRect({ relativeTo: this.stage });
2126
+ // Ensure we are in a browser
2127
+ if (!this.isBrowser) {
2128
+ return undefined;
2129
+ }
2130
+ try {
2131
+ const dataUrl = this.stage.toDataURL({
2132
+ x: rect.x,
2133
+ y: rect.y,
2134
+ width: rect.width,
2135
+ height: rect.height,
2136
+ pixelRatio: 2 // High quality
2137
+ });
2138
+ return dataUrl;
2139
+ }
2140
+ catch (e) {
2141
+ console.error('Failed to get base64 image:', e);
2142
+ return undefined;
2143
+ }
2144
+ finally {
2145
+ // Restore UI layer visibility
2146
+ if (wasUiLayerVisible) {
2147
+ this.uiLayer?.visible(true);
2148
+ }
2149
+ // Restore canvas border
2150
+ this.canvasRect.stroke(originalStroke);
2151
+ this.canvasRect.strokeWidth(originalStrokeWidth);
2152
+ }
2153
+ }
2154
+ downloadImage() {
2155
+ if (!this.stage || !this.canvasRect) {
2156
+ return;
2157
+ }
2158
+ // Capture original state
2159
+ const wasUiLayerVisible = this.uiLayer?.visible() || false;
2160
+ const originalStroke = this.canvasRect.stroke();
2161
+ const originalStrokeWidth = this.canvasRect.strokeWidth();
2162
+ // Hide UI layer to exclude transformers, snap lines, etc. from export
2163
+ this.uiLayer?.visible(false);
2164
+ // Temporarily hide canvas border
2165
+ this.canvasRect.stroke(null);
2166
+ this.canvasRect.strokeWidth(0);
2167
+ // Get the client rect of the canvas area relative to the stage
2168
+ const rect = this.canvasRect.getClientRect({ relativeTo: this.stage });
2169
+ // Ensure we are in a browser
2170
+ if (!this.isBrowser) {
2171
+ return;
2172
+ }
2173
+ try {
2174
+ const dataUrl = this.stage.toDataURL({
2175
+ x: rect.x,
2176
+ y: rect.y,
2177
+ width: rect.width,
2178
+ height: rect.height,
2179
+ pixelRatio: 2 // High quality
2180
+ });
2181
+ const timestamp = new Date().getTime();
2182
+ const filename = `${timestamp}.png`;
2183
+ const downloadLink = document.createElement('a');
2184
+ downloadLink.href = dataUrl;
2185
+ downloadLink.download = filename;
2186
+ document.body.appendChild(downloadLink);
2187
+ downloadLink.click();
2188
+ document.body.removeChild(downloadLink);
2189
+ }
2190
+ catch (e) {
2191
+ console.error('Failed to export image:', e);
2192
+ }
2193
+ finally {
2194
+ // Restore UI layer visibility
2195
+ if (wasUiLayerVisible) {
2196
+ this.uiLayer?.visible(true);
2197
+ }
2198
+ // Restore canvas border
2199
+ this.canvasRect.stroke(originalStroke);
2200
+ this.canvasRect.strokeWidth(originalStrokeWidth);
2201
+ }
2202
+ }
2203
+ notifyChange(incrementVersion = true) {
2204
+ if (this.isSnapshotLoading)
2205
+ return;
2206
+ if (incrementVersion) {
2207
+ this.currentSnapshotVersion++;
2208
+ this.saveHistory();
2209
+ }
2210
+ this._change$.next();
2211
+ }
2212
+ saveHistory() {
2213
+ const snapshot = this.getSnapshot();
2214
+ // Don't save if it's the same as the last one
2215
+ if (this.undoStack.length > 0) {
2216
+ const last = this.undoStack[this.undoStack.length - 1];
2217
+ if (JSON.stringify(last.layers) === JSON.stringify(snapshot.layers) &&
2218
+ JSON.stringify(last.imageSize) === JSON.stringify(snapshot.imageSize) &&
2219
+ last.background === snapshot.background &&
2220
+ JSON.stringify(last.backgroundConfig) === JSON.stringify(snapshot.backgroundConfig)) {
2221
+ return;
2222
+ }
2223
+ }
2224
+ this.undoStack.push(snapshot);
2225
+ if (this.undoStack.length > this.historyLimit) {
2226
+ this.undoStack.shift();
2227
+ }
2228
+ this.redoStack = [];
2229
+ this.updateHistorySignals();
2230
+ }
2231
+ undo() {
2232
+ if (this.undoStack.length <= 1)
2233
+ return;
2234
+ const current = this.undoStack.pop();
2235
+ this.redoStack.push(current);
2236
+ const previous = this.undoStack[this.undoStack.length - 1];
2237
+ this.loadSnapshot(previous, false);
2238
+ }
2239
+ redo() {
2240
+ if (this.redoStack.length === 0)
2241
+ return;
2242
+ const next = this.redoStack.pop();
2243
+ this.undoStack.push(next);
2244
+ this.loadSnapshot(next, false);
2245
+ }
2246
+ updateHistorySignals() {
2247
+ this.canUndo.set(this.undoStack.length > 1);
2248
+ this.canRedo.set(this.redoStack.length > 0);
2249
+ }
2250
+ resetHistory() {
2251
+ this.undoStack = [this.getSnapshot()];
2252
+ this.redoStack = [];
2253
+ this.updateHistorySignals();
2254
+ }
2255
+ applyEffectsToShape(shape, config) {
2256
+ const filters = [];
2257
+ let tintR = 0;
2258
+ let tintG = 0;
2259
+ let tintB = 0;
2260
+ if (config['effect'] === 'grayscale') {
2261
+ filters.push(Konva.Filters.Grayscale);
2262
+ }
2263
+ else if (config['effect'] === 'sepia') {
2264
+ filters.push(Konva.Filters.Sepia);
2265
+ }
2266
+ else if (config['effect'] === 'invert') {
2267
+ filters.push(Konva.Filters.Invert);
2268
+ }
2269
+ else if (config['effect'] === 'cold') {
2270
+ tintR = -0.15;
2271
+ tintG = 0.05;
2272
+ tintB = 0.25;
2273
+ filters.push(TintFilter);
2274
+ }
2275
+ else if (config['effect'] === 'warm') {
2276
+ tintR = 0.25;
2277
+ tintG = 0.1;
2278
+ tintB = -0.15;
2279
+ filters.push(TintFilter);
2280
+ }
2281
+ if (config['temperatureEnabled']) {
2282
+ const temp = (config['temperature'] ?? 0) / 100; // -1 to 1
2283
+ if (!filters.includes(TintFilter))
2284
+ filters.push(TintFilter);
2285
+ if (temp > 0) {
2286
+ tintR += (1 - tintR) * temp * 0.2;
2287
+ tintG += (1 - tintG) * temp * 0.1;
2288
+ tintB -= (1 + tintB) * temp * 0.2;
2289
+ }
2290
+ else {
2291
+ const absTemp = Math.abs(temp);
2292
+ tintR -= (1 + tintR) * absTemp * 0.2;
2293
+ tintG += (1 - tintG) * absTemp * 0.1;
2294
+ tintB += (1 - tintB) * absTemp * 0.2;
2295
+ }
2296
+ }
2297
+ shape.setAttr('tintR', tintR);
2298
+ shape.setAttr('tintG', tintG);
2299
+ shape.setAttr('tintB', tintB);
2300
+ if (config['blurEnabled']) {
2301
+ filters.push(Konva.Filters.Blur);
2302
+ shape.setAttr('blurRadius', config['blur'] ?? 0);
2303
+ }
2304
+ if (config['brightnessEnabled']) {
2305
+ filters.push(Konva.Filters.Brighten);
2306
+ shape.setAttr('brightness', config['brightness'] ?? 0);
2307
+ }
2308
+ if (config['contrastEnabled']) {
2309
+ filters.push(Konva.Filters.Contrast);
2310
+ shape.setAttr('contrast', config['contrast'] ?? 0);
2311
+ }
2312
+ if (config['saturationEnabled']) {
2313
+ filters.push(Konva.Filters.HSL);
2314
+ shape.setAttr('saturation', config['saturation'] ?? 0);
2315
+ }
2316
+ if (config['vibranceEnabled']) {
2317
+ // Konva doesn't have Vibrance by default, using HSL saturation as fallback
2318
+ if (!filters.includes(Konva.Filters.HSL)) {
2319
+ filters.push(Konva.Filters.HSL);
2320
+ }
2321
+ shape.setAttr('saturation', (config['saturation'] ?? 0) + (config['vibrance'] ?? 0) / 2);
2322
+ }
2323
+ if (config['borderEnabled']) {
2324
+ shape.setAttr('stroke', config['borderColor'] || config['fill'] || '#000000');
2325
+ shape.setAttr('strokeWidth', config['border'] ?? 0);
2326
+ }
2327
+ else {
2328
+ shape.setAttr('strokeWidth', 0);
2329
+ }
2330
+ if (config['cornerRadiusEnabled']) {
2331
+ const radius = config['cornerRadius'] ?? 0;
2332
+ if (shape instanceof Konva.Rect || shape instanceof Konva.Image) {
2333
+ shape.cornerRadius(radius);
2334
+ }
2335
+ }
2336
+ else {
2337
+ if (shape instanceof Konva.Rect || shape instanceof Konva.Image) {
2338
+ shape.cornerRadius(0);
2339
+ }
2340
+ }
2341
+ if (config['shadowEnabled']) {
2342
+ shape.setAttr('shadowColor', config['shadowColor'] || '#000000');
2343
+ shape.setAttr('shadowBlur', config['shadowBlur'] ?? 15);
2344
+ shape.setAttr('shadowOpacity', (config['shadowOpacity'] ?? 100) / 100);
2345
+ shape.setAttr('shadowOffset', {
2346
+ x: config['shadowOffsetX'] ?? 0,
2347
+ y: config['shadowOffsetY'] ?? 0
2348
+ });
2349
+ }
2350
+ else {
2351
+ shape.setAttr('shadowOpacity', 0);
2352
+ }
2353
+ shape.filters(filters);
2354
+ if (filters.length > 0 || (config['cornerRadiusEnabled'] && config['cornerRadius'] > 0)) {
2355
+ // In Konva, caching is required for filters to work, and for cornerRadius to clip Image
2356
+ shape.clearCache();
2357
+ shape.cache();
2358
+ }
2359
+ else {
2360
+ shape.clearCache();
2361
+ }
2362
+ this.workspaceLayer?.batchDraw();
2363
+ }
2364
+ destroy() {
2365
+ this.isInitialized.set(false);
2366
+ if (this.isBrowser) {
2367
+ window.removeEventListener('mouseup', this.handleGlobalMouseUp);
2368
+ window.removeEventListener('touchend', this.handleGlobalMouseUp);
2369
+ window.removeEventListener('mousemove', this.handleGlobalMouseMove);
2370
+ window.removeEventListener('touchmove', this.handleGlobalMouseMove);
2371
+ }
2372
+ this.resizeObserver?.disconnect();
2373
+ this.stage?.off('mousedown touchstart');
2374
+ this.stage?.off('mousemove touchmove');
2375
+ this.stage?.off('mouseup touchend');
2376
+ this.hideHover();
2377
+ this.stage?.destroy();
2378
+ this.stage = undefined;
2379
+ this.uiLayer = undefined;
2380
+ this.workspaceLayer = undefined;
2381
+ this.canvasRect = undefined;
2382
+ this.maskOverlay = undefined;
2383
+ this.transformer = undefined;
2384
+ this.selectionRect = undefined;
2385
+ this.hoverRect = undefined;
2386
+ this.selectionBordersGroup = undefined;
2387
+ }
2388
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ImageDesignerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2389
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ImageDesignerService, providedIn: 'root' });
2390
+ }
2391
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ImageDesignerService, decorators: [{
2392
+ type: Injectable,
2393
+ args: [{
2394
+ providedIn: 'root'
2395
+ }]
2396
+ }] });
2397
+
2398
+ function createDefaultPhotosDataSource(http) {
2399
+ return {
2400
+ getItems: (params) => {
2401
+ const url = `https://picsum.photos/v2/list?page=${params.page}&limit=${params.pageSize}`;
2402
+ http.get(url).pipe(map(images => images.map(img => ({
2403
+ id: img.id,
2404
+ name: img.author,
2405
+ width: img.width,
2406
+ height: img.height,
2407
+ url: img.download_url,
2408
+ thumbUrl: `https://picsum.photos/id/${img.id}/${img.width > img.height ? 400 : Math.round(400 * (img.width / img.height))}/${img.height > img.width ? 400 : Math.round(400 * (img.height / img.width))}`
2409
+ })))).subscribe({
2410
+ next: (data) => params.successCallback(data),
2411
+ error: () => params.failCallback()
2412
+ });
2413
+ }
2414
+ };
2415
+ }
2416
+
2417
+ const SVG_PATTERNS = [
2418
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="10" cy="10" r="2" fill="#64748b"/></svg>`,
2419
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="20" r="3" fill="#64748b"/></svg>`,
2420
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect width="4" height="4" x="10" y="10" fill="#64748b"/></svg>`,
2421
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect width="6" height="6" x="20" y="20" fill="#64748b"/></svg>`,
2422
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><line x1="0" y1="0" x2="40" y2="40" stroke="#64748b"/></svg>`,
2423
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><line x1="40" y1="0" x2="0" y2="40" stroke="#64748b"/></svg>`,
2424
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="20" r="10" stroke="#64748b" fill="none"/></svg>`,
2425
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect width="20" height="20" x="10" y="10" stroke="#64748b" fill="none"/></svg>`,
2426
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 20 H40" stroke="#64748b"/></svg>`,
2427
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M20 0 V40" stroke="#64748b"/></svg>`,
2428
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="5" cy="5" r="2" fill="#64748b"/><circle cx="25" cy="25" r="2" fill="#64748b"/></svg>`,
2429
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="0" y="0" width="8" height="8" fill="#64748b"/></svg>`,
2430
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="32" y="32" width="8" height="8" fill="#64748b"/></svg>`,
2431
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="10" cy="30" r="3" fill="#64748b"/></svg>`,
2432
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="30" cy="10" r="3" fill="#64748b"/></svg>`,
2433
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 10 H40" stroke="#64748b"/><path d="M0 30 H40" stroke="#64748b"/></svg>`,
2434
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="10" r="2" fill="#64748b"/><circle cx="20" cy="30" r="2" fill="#64748b"/></svg>`,
2435
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="10" y="10" width="5" height="5" fill="#64748b"/><rect x="25" y="25" width="5" height="5" fill="#64748b"/></svg>`,
2436
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="20" r="5" fill="#64748b"/></svg>`,
2437
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="20,5 25,15 15,15" fill="#64748b"/></svg>`,
2438
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="20,35 25,25 15,25" fill="#64748b"/></svg>`,
2439
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="5,20 15,15 15,25" fill="#64748b"/></svg>`,
2440
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="35,20 25,15 25,25" fill="#64748b"/></svg>`,
2441
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="5" cy="20" r="2" fill="#64748b"/><circle cx="35" cy="20" r="2" fill="#64748b"/></svg>`,
2442
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><line x1="0" y1="20" x2="40" y2="20" stroke="#64748b"/></svg>`,
2443
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><line x1="20" y1="0" x2="20" y2="40" stroke="#64748b"/></svg>`,
2444
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><line x1="0" y1="0" x2="20" y2="20" stroke="#64748b"/></svg>`,
2445
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><line x1="40" y1="0" x2="20" y2="20" stroke="#64748b"/></svg>`,
2446
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><line x1="0" y1="40" x2="20" y2="20" stroke="#64748b"/></svg>`,
2447
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="10" cy="20" r="2" fill="#64748b"/><circle cx="30" cy="20" r="2" fill="#64748b"/></svg>`,
2448
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="10" r="2" fill="#64748b"/><circle cx="20" cy="30" r="2" fill="#64748b"/></svg>`,
2449
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="15" y="15" width="10" height="10" fill="#64748b"/></svg>`,
2450
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="5" cy="5" r="1" fill="#64748b"/><circle cx="35" cy="35" r="1" fill="#64748b"/></svg>`,
2451
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="35" cy="5" r="1" fill="#64748b"/><circle cx="5" cy="35" r="1" fill="#64748b"/></svg>`,
2452
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M5 5 L35 35" stroke="#64748b"/></svg>`,
2453
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M35 5 L5 35" stroke="#64748b"/></svg>`,
2454
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="20" r="1" fill="#64748b"/></svg>`,
2455
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="18" y="18" width="4" height="4" fill="#64748b"/></svg>`,
2456
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="20,10 30,30 10,30" fill="none" stroke="#64748b"/></svg>`,
2457
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="10" cy="10" r="1" fill="#64748b"/><circle cx="30" cy="30" r="1" fill="#64748b"/></svg>`,
2458
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="30" cy="10" r="1" fill="#64748b"/><circle cx="10" cy="30" r="1" fill="#64748b"/></svg>`,
2459
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M10 10 H30 V30 H10 Z" fill="none" stroke="#64748b"/></svg>`,
2460
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="5" r="2" fill="#64748b"/><circle cx="20" cy="35" r="2" fill="#64748b"/></svg>`,
2461
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="5" cy="20" r="2" fill="#64748b"/><circle cx="35" cy="20" r="2" fill="#64748b"/></svg>`,
2462
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="0" y="18" width="40" height="4" fill="#64748b"/></svg>`,
2463
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="18" y="0" width="4" height="40" fill="#64748b"/></svg>`,
2464
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="10" cy="20" r="4" fill="none" stroke="#64748b"/></svg>`,
2465
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="30" cy="20" r="4" fill="none" stroke="#64748b"/></svg>`,
2466
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="20" r="8" fill="none" stroke="#64748b"/></svg>`,
2467
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 20 L20 0 L40 20 L20 40 Z" fill="none" stroke="#64748b"/></svg>`,
2468
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 10 L10 0 L20 10 L30 0 L40 10" stroke="#64748b" fill="none"/></svg>`,
2469
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 30 L10 40 L20 30 L30 40 L40 30" stroke="#64748b" fill="none"/></svg>`,
2470
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="20" r="2" fill="#64748b"/><circle cx="10" cy="10" r="2" fill="#64748b"/><circle cx="30" cy="30" r="2" fill="#64748b"/></svg>`,
2471
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="0" y="0" width="20" height="20" fill="none" stroke="#64748b"/><rect x="20" y="20" width="20" height="20" fill="none" stroke="#64748b"/></svg>`,
2472
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="20" y="0" width="20" height="20" fill="none" stroke="#64748b"/><rect x="0" y="20" width="20" height="20" fill="none" stroke="#64748b"/></svg>`,
2473
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 0 L40 20 L0 40 Z" fill="none" stroke="#64748b"/></svg>`,
2474
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M40 0 L0 20 L40 40 Z" fill="none" stroke="#64748b"/></svg>`,
2475
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="10,0 30,0 40,20 30,40 10,40 0,20" fill="none" stroke="#64748b"/></svg>`,
2476
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 0 L20 20 L40 0" stroke="#64748b" fill="none"/></svg>`,
2477
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 40 L20 20 L40 40" stroke="#64748b" fill="none"/></svg>`,
2478
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="0" cy="0" r="5" fill="#64748b"/><circle cx="40" cy="40" r="5" fill="#64748b"/></svg>`,
2479
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="40" cy="0" r="5" fill="#64748b"/><circle cx="0" cy="40" r="5" fill="#64748b"/></svg>`,
2480
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="20,5 35,35 5,35" fill="none" stroke="#64748b"/></svg>`,
2481
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="5,5 35,5 20,35" fill="none" stroke="#64748b"/></svg>`,
2482
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="5" y="5" width="30" height="30" rx="5" fill="none" stroke="#64748b"/></svg>`,
2483
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="10" y="10" width="20" height="20" rx="10" fill="none" stroke="#64748b"/></svg>`,
2484
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="20" r="12" stroke="#64748b" fill="none"/></svg>`,
2485
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="10" cy="20" r="4" fill="#64748b"/><circle cx="30" cy="20" r="4" fill="#64748b"/></svg>`,
2486
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="10" r="4" fill="#64748b"/><circle cx="20" cy="30" r="4" fill="#64748b"/></svg>`,
2487
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 20 Q10 0 20 20 T40 20" stroke="#64748b" fill="none"/></svg>`,
2488
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 20 Q10 40 20 20 T40 20" stroke="#64748b" fill="none"/></svg>`,
2489
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="15" y="0" width="10" height="40" fill="#64748b"/></svg>`,
2490
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="0" y="15" width="40" height="10" fill="#64748b"/></svg>`,
2491
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="0,20 10,10 20,20 10,30" fill="none" stroke="#64748b"/></svg>`,
2492
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="20,0 30,10 20,20 10,10" fill="none" stroke="#64748b"/></svg>`,
2493
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="5" cy="20" r="2" fill="#64748b"/><circle cx="20" cy="5" r="2" fill="#64748b"/><circle cx="35" cy="20" r="2" fill="#64748b"/><circle cx="20" cy="35" r="2" fill="#64748b"/></svg>`,
2494
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M10 0 L20 10 L30 0 L40 10" stroke="#64748b" fill="none"/></svg>`,
2495
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><path d="M0 30 L10 20 L20 30 L30 20 L40 30" stroke="#64748b" fill="none"/></svg>`,
2496
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="0,0 40,0 20,20" fill="none" stroke="#64748b"/></svg>`,
2497
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="0,40 40,40 20,20" fill="none" stroke="#64748b"/></svg>`,
2498
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="10" cy="10" r="3" fill="#64748b"/><circle cx="30" cy="10" r="3" fill="#64748b"/><circle cx="20" cy="30" r="3" fill="#64748b"/></svg>`,
2499
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="10" r="2" fill="#64748b"/><circle cx="10" cy="30" r="2" fill="#64748b"/><circle cx="30" cy="30" r="2" fill="#64748b"/></svg>`,
2500
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="5" y="5" width="10" height="10" fill="#64748b"/><rect x="25" y="25" width="10" height="10" fill="#64748b"/></svg>`,
2501
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect x="25" y="5" width="10" height="10" fill="#64748b"/><rect x="5" y="25" width="10" height="10" fill="#64748b"/></svg>`,
2502
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="20" r="3" fill="#64748b"/><circle cx="20" cy="5" r="3" fill="#64748b"/><circle cx="5" cy="20" r="3" fill="#64748b"/><circle cx="35" cy="20" r="3" fill="#64748b"/></svg>`,
2503
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="10,20 20,10 30,20 20,30" fill="none" stroke="#64748b"/></svg>`,
2504
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="20" r="6" fill="none" stroke="#64748b"/><circle cx="20" cy="20" r="2" fill="#64748b"/></svg>`,
2505
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="5,20 20,5 35,20 20,35" fill="none" stroke="#64748b"/></svg>`,
2506
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="20" cy="20" r="14" fill="none" stroke="#64748b"/></svg>`,
2507
+ `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><polygon points="0,10 40,10 20,30" fill="none" stroke="#64748b"/></svg>`
2508
+ ];
2509
+
2510
+ const SVG_ELEMENTS = [
2511
+ {
2512
+ name: 'square-filled',
2513
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="15" y="15" width="70" height="70" fill="%2364748b"/></svg>'
2514
+ },
2515
+ {
2516
+ name: 'square-outline',
2517
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="15" y="15" width="70" height="70" fill="none" stroke="%2364748b" stroke-width="8"/></svg>'
2518
+ },
2519
+ {
2520
+ name: 'rectangle-filled',
2521
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="10" y="25" width="80" height="50" fill="%2364748b"/></svg>'
2522
+ },
2523
+ {
2524
+ name: 'rectangle-outline',
2525
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="10" y="25" width="80" height="50" fill="none" stroke="%2364748b" stroke-width="8"/></svg>'
2526
+ },
2527
+ {
2528
+ name: 'arrow-right-1',
2529
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M10 45H65L50 30L90 50L50 70L65 55H10Z"/></svg>'
2530
+ },
2531
+ {
2532
+ name: 'arrow-right-2',
2533
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,40 60,40 60,25 90,50 60,75 60,60 10,60"/></svg>'
2534
+ },
2535
+ {
2536
+ name: 'arrow-right-3',
2537
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,45 55,45 55,30 90,50 55,70 55,55 10,55"/></svg>'
2538
+ },
2539
+ {
2540
+ name: 'arrow-right-4',
2541
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="15,40 65,40 65,25 90,50 65,75 65,60 15,60"/></svg>'
2542
+ },
2543
+ {
2544
+ name: 'arrow-right-5',
2545
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,42 58,42 58,28 90,50 58,72 58,58 10,58"/></svg>'
2546
+ },
2547
+ {
2548
+ name: 'arrow-right-fat',
2549
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,35 55,35 55,20 90,50 55,80 55,65 10,65"/></svg>'
2550
+ },
2551
+ {
2552
+ name: 'arrow-right-wide',
2553
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,38 60,38 60,22 90,50 60,78 60,62 10,62"/></svg>'
2554
+ },
2555
+ {
2556
+ name: 'arrow-right-long',
2557
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="5,45 70,45 70,30 95,50 70,70 70,55 5,55"/></svg>'
2558
+ },
2559
+ {
2560
+ name: 'arrow-right-compact',
2561
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="20,45 60,45 60,35 85,50 60,65 60,55 20,55"/></svg>'
2562
+ },
2563
+ {
2564
+ name: 'arrow-right-block',
2565
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,40 55,40 55,25 85,50 55,75 55,60 10,60"/></svg>'
2566
+ },
2567
+ {
2568
+ name: 'arrow-right',
2569
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M20 45H65L50 30L80 50L50 70L65 55H20Z"/></svg>'
2570
+ },
2571
+ {
2572
+ name: 'arrow-left',
2573
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M80 45H35L50 30L20 50L50 70L35 55H80Z"/></svg>'
2574
+ },
2575
+ {
2576
+ name: 'arrow-up',
2577
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M45 80V35L30 50L50 20L70 50L55 35V80Z"/></svg>'
2578
+ },
2579
+ {
2580
+ name: 'triangle',
2581
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="50,10 90,90 10,90"/></svg>'
2582
+ },
2583
+ {
2584
+ name: 'diamond',
2585
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="50,10 90,50 50,90 10,50"/></svg>'
2586
+ },
2587
+ {
2588
+ name: 'pentagon',
2589
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="50,10 90,40 70,90 30,90 10,40"/></svg>'
2590
+ },
2591
+ {
2592
+ name: 'parallelogram',
2593
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="20,20 90,20 70,80 0,80"/></svg>'
2594
+ },
2595
+ {
2596
+ name: 'star',
2597
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="50,10 61,40 95,40 67,60 78,90 50,70 22,90 33,60 5,40 39,40"/></svg>'
2598
+ },
2599
+ {
2600
+ name: 'hexagon',
2601
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="30,10 70,10 90,50 70,90 30,90 10,50"/></svg>'
2602
+ },
2603
+ {
2604
+ name: 'chevron',
2605
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="20,30 50,60 80,30 80,50 50,80 20,50"/></svg>'
2606
+ },
2607
+ {
2608
+ name: 'arrow',
2609
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,50 60,10 60,35 90,35 90,65 60,65 60,90"/></svg>'
2610
+ },
2611
+ {
2612
+ name: 'cross',
2613
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="40,10 60,10 60,40 90,40 90,60 60,60 60,90 40,90 40,60 10,60 10,40 40,40"/></svg>'
2614
+ },
2615
+ {
2616
+ name: 'trapezoid',
2617
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="20,30 80,30 65,80 35,80"/></svg>'
2618
+ },
2619
+ {
2620
+ name: 'kite',
2621
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="50,10 80,50 50,90 20,50"/></svg>'
2622
+ },
2623
+ {
2624
+ name: 'zigzag',
2625
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,30 40,30 25,55 60,55 45,80 90,80 90,90 10,90"/></svg>'
2626
+ },
2627
+ {
2628
+ name: 'notched-rect',
2629
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,20 70,20 90,40 90,80 10,80"/></svg>'
2630
+ },
2631
+ {
2632
+ name: 'double-triangle',
2633
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="20,80 40,20 60,80"/><polygon fill="%2364748b" points="50,80 70,20 90,80"/></svg>'
2634
+ },
2635
+ {
2636
+ name: 'shield',
2637
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M50 10 L85 25 L80 70 L50 90 L20 70 L15 25 Z"/></svg>'
2638
+ },
2639
+ {
2640
+ name: 'octagon',
2641
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="30,10 70,10 90,30 90,70 70,90 30,90 10,70 10,30"/></svg>'
2642
+ },
2643
+ {
2644
+ name: 'right-triangle',
2645
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,90 10,10 90,90"/></svg>'
2646
+ },
2647
+ {
2648
+ name: 'corner-cut',
2649
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="20,10 80,10 90,20 90,80 80,90 20,90 10,80 10,20"/></svg>'
2650
+ },
2651
+ {
2652
+ name: 'arrow-wide',
2653
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,50 50,10 50,30 90,30 90,70 50,70 50,90"/></svg>'
2654
+ },
2655
+ {
2656
+ name: 'triangle-down',
2657
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,20 90,20 50,90"/></svg>'
2658
+ },
2659
+ {
2660
+ name: 'triangle-left',
2661
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="80,10 20,50 80,90"/></svg>'
2662
+ },
2663
+ {
2664
+ name: 'triangle-right',
2665
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="20,10 80,50 20,90"/></svg>'
2666
+ },
2667
+ {
2668
+ name: 'wide-trapezoid',
2669
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,30 90,30 70,80 30,80"/></svg>'
2670
+ },
2671
+ {
2672
+ name: 'cut-diamond',
2673
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="50,5 90,50 50,95 10,50 30,50"/></svg>'
2674
+ },
2675
+ {
2676
+ name: 'house',
2677
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,50 50,10 90,50 90,90 10,90"/></svg>'
2678
+ },
2679
+ {
2680
+ name: 'tag',
2681
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,40 50,10 90,40 70,90 30,90"/></svg>'
2682
+ },
2683
+ {
2684
+ name: 'double-chevron',
2685
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,30 40,50 10,70 25,70 55,50 25,30"/><polygon fill="%2364748b" points="45,30 75,50 45,70 60,70 90,50 60,30"/></svg>'
2686
+ },
2687
+ {
2688
+ name: 'diamond-wide',
2689
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="50,10 95,50 50,90 5,50"/></svg>'
2690
+ },
2691
+ {
2692
+ name: 'notch-top',
2693
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,20 40,20 50,10 60,20 90,20 90,90 10,90"/></svg>'
2694
+ },
2695
+ {
2696
+ name: 'notch-bottom',
2697
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,10 90,10 90,80 60,80 50,90 40,80 10,80"/></svg>'
2698
+ },
2699
+ {
2700
+ name: 'step-shape',
2701
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,80 10,40 40,40 40,20 90,20 90,80"/></svg>'
2702
+ },
2703
+ {
2704
+ name: 'skew-arrow',
2705
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,60 60,20 60,40 90,40 90,80 60,80 60,90"/></svg>'
2706
+ },
2707
+ {
2708
+ name: 'barbed',
2709
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon fill="%2364748b" points="10,50 30,30 50,50 70,30 90,50 70,70 50,50 30,70"/></svg>'
2710
+ },
2711
+ {
2712
+ name: 'circle',
2713
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle fill="%2364748b" cx="50" cy="50" r="40"/></svg>'
2714
+ },
2715
+ {
2716
+ name: 'circle-ring',
2717
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle fill="%2364748b" cx="50" cy="50" r="40"/><circle fill="white" cx="50" cy="50" r="20"/></svg>'
2718
+ },
2719
+ {
2720
+ name: 'circle-cut-top',
2721
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle fill="%2364748b" cx="50" cy="60" r="35"/></svg>'
2722
+ },
2723
+ {
2724
+ name: 'circle-half',
2725
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M10 50 A40 40 0 0 1 90 50 L90 90 L10 90 Z"/></svg>'
2726
+ },
2727
+ {
2728
+ name: 'circle-quarter',
2729
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M50 50 L90 50 A40 40 0 0 0 50 10 Z"/></svg>'
2730
+ },
2731
+ {
2732
+ name: 'circle-pacman',
2733
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M50 10 A40 40 0 1 1 50 90 L70 50 Z"/></svg>'
2734
+ },
2735
+ {
2736
+ name: 'circle-segment',
2737
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M50 50 L50 10 A40 40 0 0 1 90 50 Z"/></svg>'
2738
+ },
2739
+ {
2740
+ name: 'circle-dot',
2741
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle fill="%2364748b" cx="50" cy="50" r="35"/><circle fill="white" cx="50" cy="50" r="10"/></svg>'
2742
+ },
2743
+ {
2744
+ name: 'circle-double',
2745
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle fill="%2364748b" cx="40" cy="50" r="25"/><circle fill="%2364748b" cx="65" cy="50" r="25"/></svg>'
2746
+ },
2747
+ {
2748
+ name: 'circle-arc',
2749
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M10 50 A40 40 0 0 1 90 50 L70 50 A20 20 0 0 0 30 50 Z"/></svg>'
2750
+ },
2751
+ {
2752
+ name: 'blob-1',
2753
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M32 15C50 5 78 18 85 40C92 65 70 90 45 85C20 80 10 55 15 35C20 25 25 18 32 15Z"/></svg>'
2754
+ },
2755
+ {
2756
+ name: 'blob-2',
2757
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M40 10C65 5 90 25 85 50C80 80 55 95 35 85C15 75 10 50 20 30C25 20 30 15 40 10Z"/></svg>'
2758
+ },
2759
+ {
2760
+ name: 'blob-3',
2761
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M28 20C45 5 75 10 85 35C95 60 75 90 50 88C25 85 10 60 15 40C18 30 22 25 28 20Z"/></svg>'
2762
+ },
2763
+ {
2764
+ name: 'blob-4',
2765
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M35 12C55 2 85 18 88 42C92 70 70 92 45 88C20 85 8 60 15 38C18 28 25 18 35 12Z"/></svg>'
2766
+ },
2767
+ {
2768
+ name: 'blob-5',
2769
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M30 18C52 4 80 15 88 38C95 62 75 90 50 90C25 90 10 65 15 42C18 30 22 25 30 18Z"/></svg>'
2770
+ },
2771
+ {
2772
+ name: 'blob-6',
2773
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M38 14C60 5 85 20 90 45C95 70 70 92 45 88C20 85 5 60 12 38C18 25 25 18 38 14Z"/></svg>'
2774
+ },
2775
+ {
2776
+ name: 'blob-7',
2777
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M25 22C45 8 78 12 88 35C98 60 78 92 50 90C22 88 8 60 12 40C15 32 18 26 25 22Z"/></svg>'
2778
+ },
2779
+ {
2780
+ name: 'blob-8',
2781
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M35 18C58 6 85 18 92 40C98 65 78 90 52 88C25 85 10 60 15 38C18 30 25 22 35 18Z"/></svg>'
2782
+ },
2783
+ {
2784
+ name: 'blob-9',
2785
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M30 12C52 4 82 15 90 38C98 65 75 92 48 90C22 88 8 62 15 38C20 25 22 20 30 12Z"/></svg>'
2786
+ },
2787
+ {
2788
+ name: 'blob-10',
2789
+ data: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path fill="%2364748b" d="M34 16C55 5 80 15 88 36C95 60 78 90 50 92C22 95 8 65 15 42C20 30 25 22 34 16Z"/></svg>'
2790
+ }
2791
+ ];
2792
+
2793
+ const PRESET_CATEGORIES = [
2794
+ {
2795
+ name: 'Instagram',
2796
+ icon: 'fluent:camera-24-regular',
2797
+ presets: [
2798
+ { name: 'Post', width: 1080, height: 1080, icon: 'fluent:image-24-regular' },
2799
+ { name: 'Story', width: 1080, height: 1920, icon: 'fluent:video-clip-24-regular' },
2800
+ { name: 'Ad', width: 1080, height: 1080, icon: 'fluent:megaphone-24-regular' },
2801
+ ]
2802
+ },
2803
+ {
2804
+ name: 'Facebook',
2805
+ icon: 'fluent:people-24-regular',
2806
+ presets: [
2807
+ { name: 'Post (Landscape)', width: 1200, height: 630, icon: 'fluent:image-24-regular' },
2808
+ { name: 'Post (Square)', width: 1080, height: 1080, icon: 'fluent:image-24-regular' },
2809
+ { name: 'Cover', width: 851, height: 315, icon: 'fluent:image-24-regular' },
2810
+ ]
2811
+ },
2812
+ {
2813
+ name: 'Youtube',
2814
+ icon: 'fluent:video-24-regular',
2815
+ presets: [
2816
+ { name: 'Thumbnail', width: 1280, height: 720, icon: 'fluent:image-24-regular' },
2817
+ { name: 'Channel', width: 2560, height: 1440, icon: 'fluent:image-24-regular' },
2818
+ { name: 'Short', width: 1080, height: 1920, icon: 'fluent:video-clip-24-regular' },
2819
+ ]
2820
+ },
2821
+ {
2822
+ name: 'LinkedIn',
2823
+ icon: 'fluent:briefcase-24-regular',
2824
+ presets: [
2825
+ { name: 'Post', width: 1200, height: 627, icon: 'fluent:image-24-regular' },
2826
+ { name: 'Banner', width: 1584, height: 396, icon: 'fluent:image-24-regular' },
2827
+ { name: 'Square', width: 1080, height: 1080, icon: 'fluent:image-24-regular' },
2828
+ ]
2829
+ },
2830
+ {
2831
+ name: 'Twitter',
2832
+ icon: 'fluent:news-24-regular',
2833
+ presets: [
2834
+ { name: 'Post', width: 1600, height: 900, icon: 'fluent:image-24-regular' },
2835
+ { name: 'Header', width: 1500, height: 500, icon: 'fluent:image-24-regular' },
2836
+ { name: 'Square', width: 1080, height: 1080, icon: 'fluent:image-24-regular' },
2837
+ ]
2838
+ },
2839
+ {
2840
+ name: 'Video',
2841
+ icon: 'fluent:video-24-regular',
2842
+ presets: [
2843
+ { name: 'Full HD', width: 1920, height: 1080, icon: 'fluent:video-24-regular' },
2844
+ { name: '4K UHD', width: 3840, height: 2160, icon: 'fluent:video-24-regular' },
2845
+ { name: 'Vertical HD', width: 1080, height: 1920, icon: 'fluent:video-24-regular' },
2846
+ { name: 'Square HD', width: 1080, height: 1080, icon: 'fluent:video-24-regular' },
2847
+ ]
2848
+ }
2849
+ ];
2850
+
2851
+ const IMAGE_DESIGNER = new InjectionToken('IMAGE_DESIGNER');
2852
+
2853
+ class Settings {
2854
+ imageDesigner = inject(IMAGE_DESIGNER, { optional: true });
2855
+ designerService = inject(ImageDesignerService);
2856
+ selectedLayerId = this.designerService.selectedLayerId;
2857
+ layers = this.designerService.layers;
2858
+ fonts = this.designerService.fonts;
2859
+ selectedLayer = computed(() => {
2860
+ const id = this.selectedLayerId();
2861
+ if (!id)
2862
+ return null;
2863
+ return this.layers().find(l => l.id === id);
2864
+ }, ...(ngDevMode ? [{ debugName: "selectedLayer" }] : /* istanbul ignore next */ []));
2865
+ showColorPicker = computed(() => {
2866
+ const layer = this.selectedLayer();
2867
+ if (!layer)
2868
+ return false;
2869
+ return ['text', 'shape', 'pattern'].includes(layer.type);
2870
+ }, ...(ngDevMode ? [{ debugName: "showColorPicker" }] : /* istanbul ignore next */ []));
2871
+ presetColors = signal([
2872
+ '#000000', '#7a7a7a', '#ffffff',
2873
+ '#f44336', '#e91e63', '#9c27b0',
2874
+ '#673ab7', '#3f51b5', '#2196f3',
2875
+ '#03a9f4', '#00bcd4', '#009688',
2876
+ '#4caf50', '#8bc34a', '#cddc39',
2877
+ '#ffeb3b', '#ffc107', '#ff9800',
2878
+ '#ff5722', '#795548', '#607d8b'
2879
+ ], ...(ngDevMode ? [{ debugName: "presetColors" }] : /* istanbul ignore next */ []));
2880
+ fontWeightNames = {
2881
+ 100: 'Thin',
2882
+ 200: 'Extra Light',
2883
+ 300: 'Light',
2884
+ 400: 'Regular',
2885
+ 500: 'Medium',
2886
+ 600: 'Semi Bold',
2887
+ 700: 'Bold',
2888
+ 800: 'Extra Bold',
2889
+ 900: 'Black'
2890
+ };
2891
+ getFontWeightName(value) {
2892
+ const numericValue = value === 'bold' ? 700 : (value === 'normal' ? 400 : (parseInt(value, 10) || 400));
2893
+ return this.fontWeightNames[numericValue] || numericValue.toString();
2894
+ }
2895
+ async updateOpacity(value) {
2896
+ const id = this.selectedLayerId();
2897
+ if (id) {
2898
+ await this.designerService.updateLayer(id, { opacity: value });
2899
+ }
2900
+ }
2901
+ async updateColor(color) {
2902
+ const id = this.selectedLayerId();
2903
+ const layer = this.selectedLayer();
2904
+ if (id && layer) {
2905
+ await this.designerService.updateLayer(id, { fill: color, type: layer.type });
2906
+ }
2907
+ }
2908
+ async updateFont(font) {
2909
+ const id = this.selectedLayerId();
2910
+ if (id) {
2911
+ await this.designerService.loadFont(font);
2912
+ await this.designerService.updateLayer(id, { fontFamily: font });
2913
+ }
2914
+ }
2915
+ async updateFontWeight(value) {
2916
+ const id = this.selectedLayerId();
2917
+ if (id) {
2918
+ await this.designerService.updateLayer(id, { fontWeight: value });
2919
+ }
2920
+ }
2921
+ async updateTextStyle(style) {
2922
+ const id = this.selectedLayerId();
2923
+ if (!id)
2924
+ return;
2925
+ const decorations = [];
2926
+ if (style.includes('underline'))
2927
+ decorations.push('underline');
2928
+ if (style.includes('line-through'))
2929
+ decorations.push('line-through');
2930
+ const config = {
2931
+ fontStyle: style.includes('italic') ? 'italic' : 'normal',
2932
+ textDecoration: decorations.join(' ') || 'none'
2933
+ };
2934
+ await this.designerService.updateLayer(id, config);
2935
+ }
2936
+ async toggleUppercase() {
2937
+ const id = this.selectedLayerId();
2938
+ const layer = this.selectedLayer();
2939
+ if (id && layer) {
2940
+ const currentCase = layer['textCase'];
2941
+ const newCase = currentCase === 'upper' ? 'normal' : 'upper';
2942
+ await this.designerService.updateLayer(id, { textCase: newCase });
2943
+ }
2944
+ }
2945
+ async updateTextAlign(align) {
2946
+ const id = this.selectedLayerId();
2947
+ if (id) {
2948
+ await this.designerService.updateLayer(id, { align });
2949
+ }
2950
+ }
2951
+ async updateLineHeight(value) {
2952
+ const id = this.selectedLayerId();
2953
+ if (id) {
2954
+ await this.designerService.updateLayer(id, { lineHeight: value });
2955
+ }
2956
+ }
2957
+ async updateLetterSpacing(value) {
2958
+ const id = this.selectedLayerId();
2959
+ if (id) {
2960
+ await this.designerService.updateLayer(id, { letterSpacing: value });
2961
+ }
2962
+ }
2963
+ getTextStyle = computed(() => {
2964
+ const layer = this.selectedLayer();
2965
+ if (!layer)
2966
+ return [];
2967
+ const styles = [];
2968
+ if (layer['fontStyle'] === 'italic')
2969
+ styles.push('italic');
2970
+ const decoration = layer['textDecoration'] || '';
2971
+ if (decoration.includes('underline'))
2972
+ styles.push('underline');
2973
+ if (decoration.includes('line-through'))
2974
+ styles.push('line-through');
2975
+ return styles;
2976
+ }, { ...(ngDevMode ? { debugName: "getTextStyle" } : /* istanbul ignore next */ {}), equal: (a, b) => {
2977
+ if (a.length !== b.length)
2978
+ return false;
2979
+ return a.every((v, i) => v === b[i]);
2980
+ } });
2981
+ toggleLock() {
2982
+ const id = this.selectedLayerId();
2983
+ if (id) {
2984
+ this.designerService.toggleLayerLock(id);
2985
+ }
2986
+ }
2987
+ async flipHorizontal() {
2988
+ const id = this.selectedLayerId();
2989
+ if (id) {
2990
+ await this.designerService.flipHorizontal(id);
2991
+ }
2992
+ }
2993
+ async flipVertical() {
2994
+ const id = this.selectedLayerId();
2995
+ if (id) {
2996
+ await this.designerService.flipVertical(id);
2997
+ }
2998
+ }
2999
+ async fitToPage() {
3000
+ const id = this.selectedLayerId();
3001
+ if (id) {
3002
+ await this.designerService.fitToPage(id);
3003
+ }
3004
+ }
3005
+ async fillPage() {
3006
+ const id = this.selectedLayerId();
3007
+ if (id) {
3008
+ await this.designerService.fillPage(id);
3009
+ }
3010
+ }
3011
+ saveSnapshot() {
3012
+ const snapshot = this.designerService.getSnapshot();
3013
+ const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(snapshot));
3014
+ const downloadAnchorNode = document.createElement('a');
3015
+ downloadAnchorNode.setAttribute("href", dataStr);
3016
+ downloadAnchorNode.setAttribute("download", "design-snapshot.json");
3017
+ document.body.appendChild(downloadAnchorNode);
3018
+ downloadAnchorNode.click();
3019
+ downloadAnchorNode.remove();
3020
+ }
3021
+ loadSnapshot(event) {
3022
+ const input = event.target;
3023
+ if (input.files && input.files[0]) {
3024
+ const reader = new FileReader();
3025
+ reader.onload = async (e) => {
3026
+ try {
3027
+ const snapshot = JSON.parse(e.target?.result);
3028
+ await this.designerService.loadSnapshot(snapshot);
3029
+ }
3030
+ catch (error) {
3031
+ console.error('Failed to load snapshot:', error);
3032
+ }
3033
+ };
3034
+ reader.readAsText(input.files[0]);
3035
+ }
3036
+ }
3037
+ openEffects() {
3038
+ this.imageDesigner?.openEffects();
3039
+ }
3040
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: Settings, deps: [], target: i0.ɵɵFactoryTarget.Component });
3041
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: Settings, isStandalone: true, selector: "ngs-settings", ngImport: i0, template: "@let layer = selectedLayer();\n\n@if (layer) {\n <ngs-toolbar\n class=\"h-14 z-10 bg-surface-container-lowest border-b border-border px-5 absolute top-0 left-0 right-0\">\n <!-- Color -->\n @if (showColorPicker()) {\n <ngs-toolbar-item>\n <button [ngsPopoverTriggerFor]=\"colorPopover\"\n position=\"below-center\"\n class=\"size-7 cursor-pointer hover:ring-2 hover:ring-primary rounded-full border border-border\"\n ngsRipple\n [style.backgroundColor]=\"layer['fill'] || '#000000'\">\n </button>\n <ngs-popover #colorPopover>\n <ng-template ngsPopoverContent>\n <div class=\"p-4 w-72\">\n <div class=\"text-xs font-medium uppercase text-muted-foreground mb-4\">\n {{ layer.type === 'text' ? 'Text Color' : 'Fill Color' }}\n </div>\n <ngs-color-switcher [colors]=\"presetColors()\"\n [selectedColor]=\"layer['fill'] || '#000000'\"\n (colorChange)=\"updateColor($event)\"/>\n </div>\n </ng-template>\n </ngs-popover>\n </ngs-toolbar-item>\n <ngs-divider vertical/>\n }\n\n <!-- Opacity -->\n <ngs-toolbar-item>\n <button ngsIconButton\n [ngsPopoverTriggerFor]=\"opacityPopover\"\n position=\"below-center\">\n <ngs-icon name=\"fluent:blur-24-regular\"/>\n </button>\n <ngs-popover #opacityPopover>\n <ng-template ngsPopoverContent>\n <div class=\"p-4 w-60 space-y-3\">\n <div class=\"flex justify-between items-center\">\n <label class=\"text-xs font-medium uppercase text-muted-foreground\">Opacity</label>\n <span class=\"text-xs text-muted-foreground\">{{ (layer['opacity'] ?? 1) * 100 | number:'1.0-0' }}%</span>\n </div>\n <ngs-slider [min]=\"0\" [max]=\"1\" [step]=\"0.01\">\n <input ngsSliderThumb\n [value]=\"layer['opacity'] ?? 1\"\n (valueChange)=\"updateOpacity($event)\"/>\n </ngs-slider>\n </div>\n </ng-template>\n </ngs-popover>\n </ngs-toolbar-item>\n\n <!-- Font Selection for text layers -->\n @if (layer.type === 'text') {\n <ngs-toolbar-item>\n <button ngsButton reverse [ngsMenuTriggerFor]=\"fontMenu\">\n <span class=\"truncate max-w-[100px]\">{{ layer['fontFamily'] }}</span>\n <ngs-icon name=\"fluent:chevron-down-20-regular\" class=\"size-5\"/>\n </button>\n\n <ngs-menu #fontMenu=\"ngsMenu\">\n <div class=\"min-w-[200px]\">\n @for (font of fonts(); track font) {\n <button ngs-menu-item\n [selected]=\"layer['fontFamily'] === font\"\n (click)=\"updateFont(font)\">\n {{ font }}\n </button>\n }\n </div>\n </ngs-menu>\n </ngs-toolbar-item>\n\n <!-- Text Styles -->\n <ngs-toolbar-item>\n <button ngsIconButton\n [ngsPopoverTriggerFor]=\"boldPopover\"\n position=\"below-center\"\n [class.text-primary]=\"layer['fontWeight'] && layer['fontWeight'] !== 'normal' && layer['fontWeight'] !== 400\"\n title=\"Bold\">\n <ngs-icon name=\"fluent:text-bold-24-regular\"/>\n </button>\n <ngs-popover #boldPopover>\n <ng-template ngsPopoverContent>\n <div class=\"p-4 w-60 space-y-3\">\n <div class=\"flex justify-between items-center\">\n <label class=\"text-xs font-medium uppercase text-muted-foreground\">Font Weight</label>\n <span class=\"text-xs text-muted-foreground\">{{ getFontWeightName(layer['fontWeight']) }}</span>\n </div>\n <ngs-slider [min]=\"300\" [max]=\"900\" [step]=\"100\">\n <input ngsSliderThumb\n [value]=\"layer['fontWeight'] === 'bold' ? 700 : (layer['fontWeight'] === 'normal' ? 400 : (layer['fontWeight'] || 400))\"\n (valueChange)=\"updateFontWeight($event)\"/>\n </ngs-slider>\n </div>\n </ng-template>\n </ngs-popover>\n </ngs-toolbar-item>\n\n <!-- Uppercase -->\n <ngs-toolbar-item>\n <button ngsIconButton\n (click)=\"toggleUppercase()\"\n [class.text-primary]=\"layer['textCase'] === 'upper'\"\n title=\"Uppercase\">\n <ngs-icon name=\"fluent:text-case-uppercase-24-regular\"/>\n </button>\n </ngs-toolbar-item>\n\n <ngs-toolbar-item>\n <ngs-button-toggle-group multiple\n hideSelectionIndicator\n [value]=\"getTextStyle()\"\n (valueChange)=\"updateTextStyle($event)\">\n <ngs-button-toggle value=\"italic\" title=\"Italic\">\n <ngs-icon name=\"fluent:text-italic-24-regular\"/>\n </ngs-button-toggle>\n <ngs-button-toggle value=\"underline\" title=\"Underline\">\n <ngs-icon name=\"fluent:text-underline-24-regular\"/>\n </ngs-button-toggle>\n <ngs-button-toggle value=\"line-through\" title=\"Strikethrough\">\n <ngs-icon name=\"fluent:text-strikethrough-24-regular\"/>\n </ngs-button-toggle>\n </ngs-button-toggle-group>\n </ngs-toolbar-item>\n\n <!-- Text Alignment -->\n <ngs-toolbar-item>\n <button ngsIconButton\n [ngsPopoverTriggerFor]=\"alignPopover\"\n position=\"below-center\"\n title=\"Alignment\">\n <ngs-icon [name]=\"'fluent:text-align-' + (layer['align'] || 'left') + '-24-regular'\"/>\n </button>\n <ngs-popover #alignPopover>\n <ng-template ngsPopoverContent>\n <div class=\"p-2 flex gap-1\">\n <button ngsIconButton (click)=\"updateTextAlign('left')\" [class.text-primary]=\"layer['align'] === 'left'\">\n <ngs-icon name=\"fluent:text-align-left-24-regular\"/>\n </button>\n <button ngsIconButton (click)=\"updateTextAlign('center')\" [class.text-primary]=\"layer['align'] === 'center'\">\n <ngs-icon name=\"fluent:text-align-center-24-regular\"/>\n </button>\n <button ngsIconButton (click)=\"updateTextAlign('right')\" [class.text-primary]=\"layer['align'] === 'right'\">\n <ngs-icon name=\"fluent:text-align-right-24-regular\"/>\n </button>\n <button ngsIconButton (click)=\"updateTextAlign('justify')\" [class.text-primary]=\"layer['align'] === 'justify'\">\n <ngs-icon name=\"fluent:text-align-justify-24-regular\"/>\n </button>\n </div>\n </ng-template>\n </ngs-popover>\n </ngs-toolbar-item>\n\n <!-- Spacing (Line Height & Letter Spacing) -->\n <ngs-toolbar-item>\n <button ngsIconButton\n [ngsPopoverTriggerFor]=\"spacingPopover\"\n position=\"below-center\"\n title=\"Spacing\">\n <ngs-icon name=\"fluent:text-line-spacing-24-regular\"/>\n </button>\n <ngs-popover #spacingPopover>\n <ng-template ngsPopoverContent>\n <div class=\"p-4 w-60 space-y-6\">\n <div class=\"space-y-3\">\n <div class=\"flex justify-between items-center\">\n <label class=\"text-xs font-medium uppercase text-muted-foreground\">Line Height</label>\n <span class=\"text-xs text-muted-foreground\">{{ layer['lineHeight'] || 1.2 | number:'1.1-1' }}</span>\n </div>\n <ngs-slider [min]=\"0.5\" [max]=\"3\" [step]=\"0.1\">\n <input ngsSliderThumb\n [value]=\"layer['lineHeight'] || 1.2\"\n (valueChange)=\"updateLineHeight($event)\"/>\n </ngs-slider>\n </div>\n <div class=\"space-y-3\">\n <div class=\"flex justify-between items-center\">\n <label class=\"text-xs font-medium uppercase text-muted-foreground\">Letter Spacing</label>\n <span class=\"text-xs text-muted-foreground\">{{ layer['letterSpacing'] || 0 }}</span>\n </div>\n <ngs-slider [min]=\"-5\" [max]=\"50\" [step]=\"1\">\n <input ngsSliderThumb\n [value]=\"layer['letterSpacing'] || 0\"\n (valueChange)=\"updateLetterSpacing($event)\"/>\n </ngs-slider>\n </div>\n </div>\n </ng-template>\n </ngs-popover>\n </ngs-toolbar-item>\n }\n\n @if (layer.type === 'image') {\n <ngs-toolbar-item>\n <button ngsButton reverse [ngsMenuTriggerFor]=\"flipMenu\">\n <span>Flip</span>\n <ngs-icon name=\"fluent:chevron-down-20-regular\" class=\"size-5\"/>\n </button>\n\n <ngs-menu #flipMenu=\"ngsMenu\">\n <div class=\"min-w-[150px]\">\n <button ngs-menu-item (click)=\"flipHorizontal()\">\n <ngs-icon name=\"fluent:flip-horizontal-24-regular\" class=\"mr-2\"/>\n <span>Flip horizontally</span>\n </button>\n <button ngs-menu-item (click)=\"flipVertical()\">\n <ngs-icon name=\"fluent:flip-vertical-24-regular\" class=\"mr-2\"/>\n <span>Flip vertically</span>\n </button>\n </div>\n </ngs-menu>\n </ngs-toolbar-item>\n\n <ngs-toolbar-item>\n <button ngsButton reverse [ngsMenuTriggerFor]=\"fitMenu\">\n <span>Fit to</span>\n <ngs-icon name=\"fluent:chevron-down-20-regular\" class=\"size-5\"/>\n </button>\n\n <ngs-menu #fitMenu=\"ngsMenu\">\n <div class=\"min-w-[150px]\">\n <button ngs-menu-item (click)=\"fitToPage()\">\n <span>Fit to page</span>\n </button>\n <button ngs-menu-item (click)=\"fillPage()\">\n <span>Fill page</span>\n </button>\n </div>\n </ngs-menu>\n </ngs-toolbar-item>\n }\n\n <ngs-toolbar-item>\n <button ngsButton reverse (click)=\"openEffects()\">\n <span>Effects</span>\n </button>\n </ngs-toolbar-item>\n\n <ngs-toolbar-spacer/>\n\n <!-- <ngs-divider vertical/>-->\n\n <ngs-toolbar-item>\n <button ngsIconButton (click)=\"toggleLock()\">\n <ngs-icon [name]=\"layer['locked'] ? 'fluent:lock-closed-24-regular' : 'fluent:lock-open-24-regular'\"/>\n </button>\n </ngs-toolbar-item>\n\n </ngs-toolbar>\n}\n", styles: [":host{display:contents}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: ColorSwitcher, selector: "ngs-color-switcher", inputs: ["colors", "selectedColor", "disabled"], outputs: ["colorChange"], exportAs: ["ngsColorSwitcher"] }, { kind: "component", type: Slider, selector: "ngs-slider", inputs: ["disabled", "discrete", "showTickMarks", "min", "max", "step", "displayWith"], exportAs: ["ngsSlider"] }, { kind: "directive", type: SliderThumb, selector: "input[ngsSliderThumb]", inputs: ["value"], outputs: ["valueChange"], exportAs: ["ngsSliderThumb"] }, { kind: "component", type: Toolbar, selector: "ngs-toolbar", exportAs: ["ngsToolbar"] }, { kind: "component", type: ToolbarItem, selector: "ngs-toolbar-item", inputs: ["hidden"], outputs: ["hiddenChange"] }, { kind: "component", type: Popover, selector: "ngs-popover", exportAs: ["ngsPopover"] }, { kind: "directive", type: PopoverContent, selector: "[ngsPopoverContent]" }, { kind: "directive", type: PopoverTriggerForDirective, selector: "[ngsPopoverTriggerFor]", inputs: ["ngsPopoverTriggerFor", "ngsPopoverContext", "trigger", "position", "delay", "origin", "closeOnOriginClick", "closeOnOriginMouseLeave", "hasBackdrop"], outputs: ["opened", "closed"], exportAs: ["ngsPopoverTriggerFor"] }, { kind: "component", type: Button, selector: " button[ngsButton], button[ngsIconButton], a[ngsButton], a[ngsIconButton] ", inputs: ["ngsButton", "ngsIconButton", "loading", "disabled", "disabledInteractive", "disableRipple", "reverse", "fullWidth", "hideTextOnMobile"], exportAs: ["ngsButton"] }, { kind: "component", type: Icon, selector: "ngs-icon", inputs: ["name"], exportAs: ["ngsIcon"] }, { kind: "component", type: Divider, selector: "ngs-divider", inputs: ["vertical", "inset", "fixedHeight"], exportAs: ["ngsDivider"] }, { kind: "component", type: Menu, selector: "ngs-menu", inputs: ["role", "classList", "xPosition", "yPosition"], outputs: ["closed"], exportAs: ["ngsMenu"] }, { kind: "component", type: MenuItem, selector: "ngs-menu-item, [ngs-menu-item]", inputs: ["disabled", "role", "selected"], outputs: ["_triggered"], exportAs: ["ngsMenuItem"] }, { kind: "directive", type: MenuTrigger, selector: "[ngsMenuTriggerFor]", inputs: ["ngsMenuTriggerFor", "ngsMenuTriggerData", "ngsMenuDisabled", "xPosition", "yPosition", "ngsMenuTriggerRestoreFocus"], outputs: ["menuOpened", "menuClosed"], exportAs: ["ngsMenuTrigger"] }, { kind: "directive", type: Ripple, selector: "[ngsRipple]", inputs: ["ngsRippleColor", "ngsRippleUnbounded", "ngsRippleCentered", "ngsRippleRadius", "ngsRippleAnimation", "ngsRippleDisabled", "ngsRippleTrigger"], outputs: ["ngsRippleCenteredChange", "ngsRippleDisabledChange", "ngsRippleTriggerChange"], exportAs: ["ngsRipple"] }, { kind: "component", type: ToolbarSpacer, selector: "ngs-toolbar-spacer" }, { kind: "component", type: ButtonToggle, selector: "ngs-button-toggle", inputs: ["id", "value", "name", "checked", "disabled"], outputs: ["change"] }, { kind: "component", type: ButtonToggleGroup, selector: "ngs-button-toggle-group", inputs: ["appearance", "disabled", "multiple", "hideSelectionIndicator", "vertical", "value"], outputs: ["valueChange", "change"], exportAs: ["ngsButtonToggleGroup"] }, { kind: "ngmodule", type: FormsModule }, { kind: "pipe", type: i1.DecimalPipe, name: "number" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3042
+ }
3043
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: Settings, decorators: [{
3044
+ type: Component,
3045
+ args: [{ selector: 'ngs-settings', imports: [
3046
+ CommonModule,
3047
+ ColorSwitcher,
3048
+ Slider,
3049
+ SliderThumb,
3050
+ Toolbar,
3051
+ ToolbarItem,
3052
+ Popover,
3053
+ PopoverContent,
3054
+ PopoverTriggerForDirective,
3055
+ Button,
3056
+ Icon,
3057
+ Divider,
3058
+ DecimalPipe,
3059
+ Menu,
3060
+ MenuItem,
3061
+ MenuTrigger,
3062
+ Ripple,
3063
+ ToolbarSpacer,
3064
+ ButtonToggle,
3065
+ ButtonToggleGroup,
3066
+ FormsModule
3067
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let layer = selectedLayer();\n\n@if (layer) {\n <ngs-toolbar\n class=\"h-14 z-10 bg-surface-container-lowest border-b border-border px-5 absolute top-0 left-0 right-0\">\n <!-- Color -->\n @if (showColorPicker()) {\n <ngs-toolbar-item>\n <button [ngsPopoverTriggerFor]=\"colorPopover\"\n position=\"below-center\"\n class=\"size-7 cursor-pointer hover:ring-2 hover:ring-primary rounded-full border border-border\"\n ngsRipple\n [style.backgroundColor]=\"layer['fill'] || '#000000'\">\n </button>\n <ngs-popover #colorPopover>\n <ng-template ngsPopoverContent>\n <div class=\"p-4 w-72\">\n <div class=\"text-xs font-medium uppercase text-muted-foreground mb-4\">\n {{ layer.type === 'text' ? 'Text Color' : 'Fill Color' }}\n </div>\n <ngs-color-switcher [colors]=\"presetColors()\"\n [selectedColor]=\"layer['fill'] || '#000000'\"\n (colorChange)=\"updateColor($event)\"/>\n </div>\n </ng-template>\n </ngs-popover>\n </ngs-toolbar-item>\n <ngs-divider vertical/>\n }\n\n <!-- Opacity -->\n <ngs-toolbar-item>\n <button ngsIconButton\n [ngsPopoverTriggerFor]=\"opacityPopover\"\n position=\"below-center\">\n <ngs-icon name=\"fluent:blur-24-regular\"/>\n </button>\n <ngs-popover #opacityPopover>\n <ng-template ngsPopoverContent>\n <div class=\"p-4 w-60 space-y-3\">\n <div class=\"flex justify-between items-center\">\n <label class=\"text-xs font-medium uppercase text-muted-foreground\">Opacity</label>\n <span class=\"text-xs text-muted-foreground\">{{ (layer['opacity'] ?? 1) * 100 | number:'1.0-0' }}%</span>\n </div>\n <ngs-slider [min]=\"0\" [max]=\"1\" [step]=\"0.01\">\n <input ngsSliderThumb\n [value]=\"layer['opacity'] ?? 1\"\n (valueChange)=\"updateOpacity($event)\"/>\n </ngs-slider>\n </div>\n </ng-template>\n </ngs-popover>\n </ngs-toolbar-item>\n\n <!-- Font Selection for text layers -->\n @if (layer.type === 'text') {\n <ngs-toolbar-item>\n <button ngsButton reverse [ngsMenuTriggerFor]=\"fontMenu\">\n <span class=\"truncate max-w-[100px]\">{{ layer['fontFamily'] }}</span>\n <ngs-icon name=\"fluent:chevron-down-20-regular\" class=\"size-5\"/>\n </button>\n\n <ngs-menu #fontMenu=\"ngsMenu\">\n <div class=\"min-w-[200px]\">\n @for (font of fonts(); track font) {\n <button ngs-menu-item\n [selected]=\"layer['fontFamily'] === font\"\n (click)=\"updateFont(font)\">\n {{ font }}\n </button>\n }\n </div>\n </ngs-menu>\n </ngs-toolbar-item>\n\n <!-- Text Styles -->\n <ngs-toolbar-item>\n <button ngsIconButton\n [ngsPopoverTriggerFor]=\"boldPopover\"\n position=\"below-center\"\n [class.text-primary]=\"layer['fontWeight'] && layer['fontWeight'] !== 'normal' && layer['fontWeight'] !== 400\"\n title=\"Bold\">\n <ngs-icon name=\"fluent:text-bold-24-regular\"/>\n </button>\n <ngs-popover #boldPopover>\n <ng-template ngsPopoverContent>\n <div class=\"p-4 w-60 space-y-3\">\n <div class=\"flex justify-between items-center\">\n <label class=\"text-xs font-medium uppercase text-muted-foreground\">Font Weight</label>\n <span class=\"text-xs text-muted-foreground\">{{ getFontWeightName(layer['fontWeight']) }}</span>\n </div>\n <ngs-slider [min]=\"300\" [max]=\"900\" [step]=\"100\">\n <input ngsSliderThumb\n [value]=\"layer['fontWeight'] === 'bold' ? 700 : (layer['fontWeight'] === 'normal' ? 400 : (layer['fontWeight'] || 400))\"\n (valueChange)=\"updateFontWeight($event)\"/>\n </ngs-slider>\n </div>\n </ng-template>\n </ngs-popover>\n </ngs-toolbar-item>\n\n <!-- Uppercase -->\n <ngs-toolbar-item>\n <button ngsIconButton\n (click)=\"toggleUppercase()\"\n [class.text-primary]=\"layer['textCase'] === 'upper'\"\n title=\"Uppercase\">\n <ngs-icon name=\"fluent:text-case-uppercase-24-regular\"/>\n </button>\n </ngs-toolbar-item>\n\n <ngs-toolbar-item>\n <ngs-button-toggle-group multiple\n hideSelectionIndicator\n [value]=\"getTextStyle()\"\n (valueChange)=\"updateTextStyle($event)\">\n <ngs-button-toggle value=\"italic\" title=\"Italic\">\n <ngs-icon name=\"fluent:text-italic-24-regular\"/>\n </ngs-button-toggle>\n <ngs-button-toggle value=\"underline\" title=\"Underline\">\n <ngs-icon name=\"fluent:text-underline-24-regular\"/>\n </ngs-button-toggle>\n <ngs-button-toggle value=\"line-through\" title=\"Strikethrough\">\n <ngs-icon name=\"fluent:text-strikethrough-24-regular\"/>\n </ngs-button-toggle>\n </ngs-button-toggle-group>\n </ngs-toolbar-item>\n\n <!-- Text Alignment -->\n <ngs-toolbar-item>\n <button ngsIconButton\n [ngsPopoverTriggerFor]=\"alignPopover\"\n position=\"below-center\"\n title=\"Alignment\">\n <ngs-icon [name]=\"'fluent:text-align-' + (layer['align'] || 'left') + '-24-regular'\"/>\n </button>\n <ngs-popover #alignPopover>\n <ng-template ngsPopoverContent>\n <div class=\"p-2 flex gap-1\">\n <button ngsIconButton (click)=\"updateTextAlign('left')\" [class.text-primary]=\"layer['align'] === 'left'\">\n <ngs-icon name=\"fluent:text-align-left-24-regular\"/>\n </button>\n <button ngsIconButton (click)=\"updateTextAlign('center')\" [class.text-primary]=\"layer['align'] === 'center'\">\n <ngs-icon name=\"fluent:text-align-center-24-regular\"/>\n </button>\n <button ngsIconButton (click)=\"updateTextAlign('right')\" [class.text-primary]=\"layer['align'] === 'right'\">\n <ngs-icon name=\"fluent:text-align-right-24-regular\"/>\n </button>\n <button ngsIconButton (click)=\"updateTextAlign('justify')\" [class.text-primary]=\"layer['align'] === 'justify'\">\n <ngs-icon name=\"fluent:text-align-justify-24-regular\"/>\n </button>\n </div>\n </ng-template>\n </ngs-popover>\n </ngs-toolbar-item>\n\n <!-- Spacing (Line Height & Letter Spacing) -->\n <ngs-toolbar-item>\n <button ngsIconButton\n [ngsPopoverTriggerFor]=\"spacingPopover\"\n position=\"below-center\"\n title=\"Spacing\">\n <ngs-icon name=\"fluent:text-line-spacing-24-regular\"/>\n </button>\n <ngs-popover #spacingPopover>\n <ng-template ngsPopoverContent>\n <div class=\"p-4 w-60 space-y-6\">\n <div class=\"space-y-3\">\n <div class=\"flex justify-between items-center\">\n <label class=\"text-xs font-medium uppercase text-muted-foreground\">Line Height</label>\n <span class=\"text-xs text-muted-foreground\">{{ layer['lineHeight'] || 1.2 | number:'1.1-1' }}</span>\n </div>\n <ngs-slider [min]=\"0.5\" [max]=\"3\" [step]=\"0.1\">\n <input ngsSliderThumb\n [value]=\"layer['lineHeight'] || 1.2\"\n (valueChange)=\"updateLineHeight($event)\"/>\n </ngs-slider>\n </div>\n <div class=\"space-y-3\">\n <div class=\"flex justify-between items-center\">\n <label class=\"text-xs font-medium uppercase text-muted-foreground\">Letter Spacing</label>\n <span class=\"text-xs text-muted-foreground\">{{ layer['letterSpacing'] || 0 }}</span>\n </div>\n <ngs-slider [min]=\"-5\" [max]=\"50\" [step]=\"1\">\n <input ngsSliderThumb\n [value]=\"layer['letterSpacing'] || 0\"\n (valueChange)=\"updateLetterSpacing($event)\"/>\n </ngs-slider>\n </div>\n </div>\n </ng-template>\n </ngs-popover>\n </ngs-toolbar-item>\n }\n\n @if (layer.type === 'image') {\n <ngs-toolbar-item>\n <button ngsButton reverse [ngsMenuTriggerFor]=\"flipMenu\">\n <span>Flip</span>\n <ngs-icon name=\"fluent:chevron-down-20-regular\" class=\"size-5\"/>\n </button>\n\n <ngs-menu #flipMenu=\"ngsMenu\">\n <div class=\"min-w-[150px]\">\n <button ngs-menu-item (click)=\"flipHorizontal()\">\n <ngs-icon name=\"fluent:flip-horizontal-24-regular\" class=\"mr-2\"/>\n <span>Flip horizontally</span>\n </button>\n <button ngs-menu-item (click)=\"flipVertical()\">\n <ngs-icon name=\"fluent:flip-vertical-24-regular\" class=\"mr-2\"/>\n <span>Flip vertically</span>\n </button>\n </div>\n </ngs-menu>\n </ngs-toolbar-item>\n\n <ngs-toolbar-item>\n <button ngsButton reverse [ngsMenuTriggerFor]=\"fitMenu\">\n <span>Fit to</span>\n <ngs-icon name=\"fluent:chevron-down-20-regular\" class=\"size-5\"/>\n </button>\n\n <ngs-menu #fitMenu=\"ngsMenu\">\n <div class=\"min-w-[150px]\">\n <button ngs-menu-item (click)=\"fitToPage()\">\n <span>Fit to page</span>\n </button>\n <button ngs-menu-item (click)=\"fillPage()\">\n <span>Fill page</span>\n </button>\n </div>\n </ngs-menu>\n </ngs-toolbar-item>\n }\n\n <ngs-toolbar-item>\n <button ngsButton reverse (click)=\"openEffects()\">\n <span>Effects</span>\n </button>\n </ngs-toolbar-item>\n\n <ngs-toolbar-spacer/>\n\n <!-- <ngs-divider vertical/>-->\n\n <ngs-toolbar-item>\n <button ngsIconButton (click)=\"toggleLock()\">\n <ngs-icon [name]=\"layer['locked'] ? 'fluent:lock-closed-24-regular' : 'fluent:lock-open-24-regular'\"/>\n </button>\n </ngs-toolbar-item>\n\n </ngs-toolbar>\n}\n", styles: [":host{display:contents}\n"] }]
3068
+ }] });
3069
+
3070
+ class Effects {
3071
+ designerService = inject(ImageDesignerService);
3072
+ imageDesigner = inject(IMAGE_DESIGNER);
3073
+ selectedLayerId = this.designerService.selectedLayerId;
3074
+ layers = this.designerService.layers;
3075
+ selectedLayer = computed(() => {
3076
+ const id = this.selectedLayerId();
3077
+ if (!id)
3078
+ return null;
3079
+ return this.layers().find(l => l.id === id);
3080
+ }, ...(ngDevMode ? [{ debugName: "selectedLayer" }] : /* istanbul ignore next */ []));
3081
+ defaultThumb = 'https://picsum.photos/id/11/200/200';
3082
+ selectedLayerThumb = computed(() => {
3083
+ const id = this.selectedLayerId();
3084
+ if (!id)
3085
+ return this.defaultThumb;
3086
+ return this.designerService.getLayerThumbnail(id) || this.defaultThumb;
3087
+ }, ...(ngDevMode ? [{ debugName: "selectedLayerThumb" }] : /* istanbul ignore next */ []));
3088
+ effects = computed(() => {
3089
+ const id = this.selectedLayerId();
3090
+ const thumb = this.selectedLayerThumb();
3091
+ if (!id) {
3092
+ return [
3093
+ { id: 'none', name: 'Original', thumb },
3094
+ { id: 'grayscale', name: 'Grayscale', thumb },
3095
+ { id: 'sepia', name: 'Sepia', thumb },
3096
+ { id: 'cold', name: 'Cold', thumb },
3097
+ { id: 'warm', name: 'Warm', thumb },
3098
+ ];
3099
+ }
3100
+ return [
3101
+ { id: 'none', name: 'Original', thumb: this.designerService.getLayerThumbnail(id, 'none') || thumb },
3102
+ { id: 'grayscale', name: 'Grayscale', thumb: this.designerService.getLayerThumbnail(id, 'grayscale') || thumb },
3103
+ { id: 'sepia', name: 'Sepia', thumb: this.designerService.getLayerThumbnail(id, 'sepia') || thumb },
3104
+ { id: 'cold', name: 'Cold', thumb: this.designerService.getLayerThumbnail(id, 'cold') || thumb },
3105
+ { id: 'warm', name: 'Warm', thumb: this.designerService.getLayerThumbnail(id, 'warm') || thumb },
3106
+ ];
3107
+ }, ...(ngDevMode ? [{ debugName: "effects" }] : /* istanbul ignore next */ []));
3108
+ adjustments = signal([
3109
+ { id: 'blur', name: 'Blur', min: 0, max: 20, step: 1, default: 0 },
3110
+ // { id: 'brightness', name: 'Brightness', min: -1, max: 1, step: 0.1, default: 0 },
3111
+ // { id: 'temperature', name: 'Temperature', min: -100, max: 100, step: 1, default: 0 },
3112
+ // { id: 'contrast', name: 'Contrast', min: -100, max: 100, step: 1, default: 0 },
3113
+ // { id: 'white', name: 'White', min: -100, max: 100, step: 1, default: 0 },
3114
+ // { id: 'black', name: 'Black', min: -100, max: 100, step: 1, default: 0 },
3115
+ // { id: 'vibrance', name: 'Vibrance', min: -100, max: 100, step: 1, default: 0 },
3116
+ // { id: 'saturation', name: 'Saturation', min: -100, max: 100, step: 1, default: 0 },
3117
+ { id: 'border', name: 'Border', min: 0, max: 50, step: 1, default: 0 },
3118
+ { id: 'cornerRadius', name: 'Corner Radius', min: 0, max: 100, step: 1, default: 0 },
3119
+ { id: 'shadow', name: 'Shadow', isGroup: true, controls: [
3120
+ { id: 'shadowBlur', name: 'Blur', min: 0, max: 100, step: 1, default: 15 },
3121
+ { id: 'shadowOffsetX', name: 'Offset X', min: -100, max: 100, step: 1, default: 0 },
3122
+ { id: 'shadowOffsetY', name: 'Offset Y', min: -100, max: 100, step: 1, default: 0 },
3123
+ { id: 'shadowOpacity', name: 'Opacity', min: 0, max: 100, step: 1, default: 100 },
3124
+ { id: 'shadowColor', name: 'Color', type: 'color', default: '#000000' }
3125
+ ] }
3126
+ ], ...(ngDevMode ? [{ debugName: "adjustments" }] : /* istanbul ignore next */ []));
3127
+ async applyEffect(effectId) {
3128
+ const id = this.selectedLayerId();
3129
+ if (id) {
3130
+ await this.designerService.updateLayer(id, { effect: effectId });
3131
+ }
3132
+ }
3133
+ async resetAll() {
3134
+ const id = this.selectedLayerId();
3135
+ if (id) {
3136
+ const config = { effect: 'none' };
3137
+ this.adjustments().forEach(adj => {
3138
+ config[`${adj.id}Enabled`] = false;
3139
+ if (adj.isGroup) {
3140
+ adj.controls.forEach(ctrl => {
3141
+ config[ctrl.id] = ctrl.default;
3142
+ });
3143
+ }
3144
+ else {
3145
+ config[adj.id] = adj.default;
3146
+ }
3147
+ });
3148
+ await this.designerService.updateLayer(id, config);
3149
+ }
3150
+ }
3151
+ async updateAdjustment(id, value) {
3152
+ const layerId = this.selectedLayerId();
3153
+ if (layerId) {
3154
+ await this.designerService.updateLayer(layerId, { [id]: value });
3155
+ }
3156
+ }
3157
+ async toggleAdjustment(id, enabled) {
3158
+ const layerId = this.selectedLayerId();
3159
+ if (layerId) {
3160
+ const config = { [`${id}Enabled`]: enabled };
3161
+ if (!enabled) {
3162
+ const adj = this.adjustments().find(a => a.id === id);
3163
+ if (adj) {
3164
+ if (adj.isGroup) {
3165
+ adj.controls.forEach(control => {
3166
+ config[control.id] = control.default;
3167
+ });
3168
+ }
3169
+ else {
3170
+ config[id] = adj.default;
3171
+ }
3172
+ }
3173
+ }
3174
+ await this.designerService.updateLayer(layerId, config);
3175
+ }
3176
+ }
3177
+ close() {
3178
+ this.imageDesigner.effectsPortal.set(null);
3179
+ }
3180
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: Effects, deps: [], target: i0.ɵɵFactoryTarget.Component });
3181
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: Effects, isStandalone: true, selector: "ngs-effects", ngImport: i0, template: "<ngs-panel class=\"h-full\">\n <ngs-panel-header class=\"border-b border-border flex items-center ps-4 pe-2\">\n <ngs-toolbar class=\"h-full\">\n <span class=\"font-medium\">Effects</span>\n <ngs-toolbar-spacer/>\n <button ngsButton=\"tonal\" (click)=\"resetAll()\" class=\"me-2 h-8 text-xs px-3\">\n Reset\n </button>\n <button ngsIconButton (click)=\"close()\">\n <ngs-icon name=\"fluent:dismiss-24-regular\"/>\n </button>\n </ngs-toolbar>\n </ngs-panel-header>\n <ngs-panel-content class=\"p-0 overflow-y-auto\">\n <div class=\"p-4\">\n <!-- Preset Effects -->\n <div class=\"flex gap-4 pb-6 scrollbar-hide flex-wrap\">\n @for (effect of effects() || []; track effect.id) {\n <button (click)=\"applyEffect(effect.id)\"\n class=\"flex flex-col items-center gap-2 group min-w-[80px]\">\n <div class=\"w-20 h-20 rounded-xl overflow-hidden border-2 transition-all group-hover:border-primary\"\n [class.border-primary]=\"(selectedLayer()?.['effect'] || 'none') === effect.id\"\n [class.border-transparent]=\"(selectedLayer()?.['effect'] || 'none') !== effect.id\">\n <img [src]=\"effect.thumb\" class=\"w-full h-full object-cover\" [alt]=\"effect.name\">\n </div>\n <span class=\"text-xs font-medium text-muted-foreground group-hover:text-foreground transition-colors\">\n {{ effect.name }}\n </span>\n </button>\n }\n </div>\n\n <!-- Adjustments -->\n <div class=\"flex flex-col gap-6\">\n @for (adj of adjustments() || []; track adj.id) {\n <div class=\"flex flex-col gap-3\">\n <div class=\"flex items-center justify-between\">\n <span class=\"text-sm font-medium\">{{ adj.name }}</span>\n <ngs-slide-toggle [checked]=\"selectedLayer()?.[adj.id + 'Enabled']\"\n (change)=\"toggleAdjustment(adj.id, $event.checked)\"/>\n </div>\n\n @if (selectedLayer()?.[adj.id + 'Enabled']) {\n <div class=\"flex flex-col gap-4\">\n @if (adj.isGroup) {\n @for (control of adj.controls || []; track control.id) {\n <div class=\"flex flex-col gap-2\">\n <div class=\"flex items-center justify-between\">\n <span class=\"text-xs text-muted-foreground\">{{ control.name }}</span>\n @if (control.type === 'color') {\n <button ngs-color-picker-thumbnail\n [color]=\"selectedLayer()?.[control.id] || control.default\"\n [ngsColorPickerTriggerFor]=\"colorPicker\"\n class=\"cursor-pointer border border-border rounded\"></button>\n <ng-template #colorPicker>\n <ngs-color-picker [ngModel]=\"selectedLayer()?.[control.id] || control.default\"\n (ngModelChange)=\"updateAdjustment(control.id, $event)\"/>\n </ng-template>\n } @else {\n <input ngsInput\n type=\"number\"\n class=\"w-16 h-8 text-center text-sm border border-border rounded-md bg-surface-container\"\n [ngModel]=\"selectedLayer()?.[control.id] ?? control.default\"\n (ngModelChange)=\"updateAdjustment(control.id, $event)\">\n }\n </div>\n @if (control.type !== 'color') {\n <ngs-slider [min]=\"control.min\"\n [max]=\"control.max\"\n [step]=\"control.step\">\n <input ngsSliderThumb\n [value]=\"selectedLayer()?.[control.id] ?? control.default\"\n (valueChange)=\"updateAdjustment(control.id, $event)\">\n </ngs-slider>\n }\n </div>\n }\n } @else {\n <div class=\"flex items-center gap-4\">\n @if (adj.id === 'border') {\n <button ngs-color-picker-thumbnail\n [color]=\"selectedLayer()?.['borderColor'] || selectedLayer()?.['fill'] || '#000000'\"\n [ngsColorPickerTriggerFor]=\"borderPicker\"\n class=\"cursor-pointer border border-border rounded\"></button>\n <ng-template #borderPicker>\n <ngs-color-picker [ngModel]=\"selectedLayer()?.['borderColor'] || selectedLayer()?.['fill'] || '#000000'\"\n (ngModelChange)=\"updateAdjustment('borderColor', $event)\"/>\n </ng-template>\n }\n <ngs-slider class=\"flex-1\"\n [min]=\"adj.min\"\n [max]=\"adj.max\"\n [step]=\"adj.step\">\n <input ngsSliderThumb\n [value]=\"selectedLayer()?.[adj.id] ?? adj.default\"\n (valueChange)=\"updateAdjustment(adj.id, $event)\">\n </ngs-slider>\n <input ngsInput\n type=\"number\"\n class=\"w-16 h-8 text-center text-sm border border-border rounded-md bg-surface-container\"\n [ngModel]=\"selectedLayer()?.[adj.id] ?? adj.default\"\n (ngModelChange)=\"updateAdjustment(adj.id, $event)\">\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n </ngs-panel-content>\n</ngs-panel>\n", styles: [":host{display:block;height:100%}:host .scrollbar-hide{-ms-overflow-style:none;scrollbar-width:none}:host .scrollbar-hide::-webkit-scrollbar{display:none}:host ngs-slider{--ngs-slider-track-height: 4px;--ngs-slider-thumb-size: 16px}:host input[type=number]::-webkit-inner-spin-button,:host input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.ngs-color-picker-thumbnail{width:32px;height:32px;min-width:32px;background:var(--ngs-color-picker-thumbnail-bg, #000)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: Panel, selector: "ngs-panel", inputs: ["absolute"], exportAs: ["ngsPanel"] }, { kind: "component", type: PanelHeader, selector: "ngs-panel-header", inputs: ["autoHeight"], exportAs: ["ngsPanelHeader"] }, { kind: "component", type: PanelContent, selector: "ngs-panel-content", exportAs: ["ngsPanelContent"] }, { kind: "component", type: Button, selector: " button[ngsButton], button[ngsIconButton], a[ngsButton], a[ngsIconButton] ", inputs: ["ngsButton", "ngsIconButton", "loading", "disabled", "disabledInteractive", "disableRipple", "reverse", "fullWidth", "hideTextOnMobile"], exportAs: ["ngsButton"] }, { kind: "component", type: Icon, selector: "ngs-icon", inputs: ["name"], exportAs: ["ngsIcon"] }, { kind: "component", type: SlideToggle, selector: "ngs-slide-toggle", inputs: ["id", "name", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "disabled", "disableRipple", "tabIndex", "hideIcon", "color", "checked"], outputs: ["disabledChange", "checkedChange", "change", "toggleChange"], exportAs: ["ngsSlideToggle"] }, { kind: "component", type: Slider, selector: "ngs-slider", inputs: ["disabled", "discrete", "showTickMarks", "min", "max", "step", "displayWith"], exportAs: ["ngsSlider"] }, { kind: "directive", type: SliderThumb, selector: "input[ngsSliderThumb]", inputs: ["value"], outputs: ["valueChange"], exportAs: ["ngsSliderThumb"] }, { kind: "directive", type: Input, selector: "input[ngsInput], textarea[ngsInput]", inputs: ["id", "placeholder", "required", "disabled", "readonly", "errorStateMatcher"], exportAs: ["ngsInput"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: Toolbar, selector: "ngs-toolbar", exportAs: ["ngsToolbar"] }, { kind: "component", type: ToolbarSpacer, selector: "ngs-toolbar-spacer" }, { kind: "component", type: ColorPicker, selector: "ngs-color-picker", inputs: ["color", "disabled", "asDropdown", "showOpacity", "resultFormat"], outputs: ["colorChange", "rawColorChange"], exportAs: ["ngsColorPicker"] }, { kind: "component", type: ColorPickerThumbnail, selector: "ngs-color-picker-thumbnail,[ngs-color-picker-thumbnail]", inputs: ["color"], exportAs: ["ngsColorPickerThumbnail"] }, { kind: "directive", type: ColorPickerTriggerForDirective, selector: "[ngsColorPickerTriggerFor]", inputs: ["ngsColorPickerTriggerFor", "position"], outputs: ["opened", "closed"], exportAs: ["ngsColorPickerTriggerFor"] }] });
3182
+ }
3183
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: Effects, decorators: [{
3184
+ type: Component,
3185
+ args: [{ selector: 'ngs-effects', imports: [
3186
+ CommonModule,
3187
+ Panel,
3188
+ PanelHeader,
3189
+ PanelContent,
3190
+ Button,
3191
+ Icon,
3192
+ SlideToggle,
3193
+ Slider,
3194
+ SliderThumb,
3195
+ Input,
3196
+ FormsModule,
3197
+ Toolbar,
3198
+ ToolbarSpacer,
3199
+ ColorPicker,
3200
+ ColorPickerThumbnail,
3201
+ ColorPickerTriggerForDirective
3202
+ ], template: "<ngs-panel class=\"h-full\">\n <ngs-panel-header class=\"border-b border-border flex items-center ps-4 pe-2\">\n <ngs-toolbar class=\"h-full\">\n <span class=\"font-medium\">Effects</span>\n <ngs-toolbar-spacer/>\n <button ngsButton=\"tonal\" (click)=\"resetAll()\" class=\"me-2 h-8 text-xs px-3\">\n Reset\n </button>\n <button ngsIconButton (click)=\"close()\">\n <ngs-icon name=\"fluent:dismiss-24-regular\"/>\n </button>\n </ngs-toolbar>\n </ngs-panel-header>\n <ngs-panel-content class=\"p-0 overflow-y-auto\">\n <div class=\"p-4\">\n <!-- Preset Effects -->\n <div class=\"flex gap-4 pb-6 scrollbar-hide flex-wrap\">\n @for (effect of effects() || []; track effect.id) {\n <button (click)=\"applyEffect(effect.id)\"\n class=\"flex flex-col items-center gap-2 group min-w-[80px]\">\n <div class=\"w-20 h-20 rounded-xl overflow-hidden border-2 transition-all group-hover:border-primary\"\n [class.border-primary]=\"(selectedLayer()?.['effect'] || 'none') === effect.id\"\n [class.border-transparent]=\"(selectedLayer()?.['effect'] || 'none') !== effect.id\">\n <img [src]=\"effect.thumb\" class=\"w-full h-full object-cover\" [alt]=\"effect.name\">\n </div>\n <span class=\"text-xs font-medium text-muted-foreground group-hover:text-foreground transition-colors\">\n {{ effect.name }}\n </span>\n </button>\n }\n </div>\n\n <!-- Adjustments -->\n <div class=\"flex flex-col gap-6\">\n @for (adj of adjustments() || []; track adj.id) {\n <div class=\"flex flex-col gap-3\">\n <div class=\"flex items-center justify-between\">\n <span class=\"text-sm font-medium\">{{ adj.name }}</span>\n <ngs-slide-toggle [checked]=\"selectedLayer()?.[adj.id + 'Enabled']\"\n (change)=\"toggleAdjustment(adj.id, $event.checked)\"/>\n </div>\n\n @if (selectedLayer()?.[adj.id + 'Enabled']) {\n <div class=\"flex flex-col gap-4\">\n @if (adj.isGroup) {\n @for (control of adj.controls || []; track control.id) {\n <div class=\"flex flex-col gap-2\">\n <div class=\"flex items-center justify-between\">\n <span class=\"text-xs text-muted-foreground\">{{ control.name }}</span>\n @if (control.type === 'color') {\n <button ngs-color-picker-thumbnail\n [color]=\"selectedLayer()?.[control.id] || control.default\"\n [ngsColorPickerTriggerFor]=\"colorPicker\"\n class=\"cursor-pointer border border-border rounded\"></button>\n <ng-template #colorPicker>\n <ngs-color-picker [ngModel]=\"selectedLayer()?.[control.id] || control.default\"\n (ngModelChange)=\"updateAdjustment(control.id, $event)\"/>\n </ng-template>\n } @else {\n <input ngsInput\n type=\"number\"\n class=\"w-16 h-8 text-center text-sm border border-border rounded-md bg-surface-container\"\n [ngModel]=\"selectedLayer()?.[control.id] ?? control.default\"\n (ngModelChange)=\"updateAdjustment(control.id, $event)\">\n }\n </div>\n @if (control.type !== 'color') {\n <ngs-slider [min]=\"control.min\"\n [max]=\"control.max\"\n [step]=\"control.step\">\n <input ngsSliderThumb\n [value]=\"selectedLayer()?.[control.id] ?? control.default\"\n (valueChange)=\"updateAdjustment(control.id, $event)\">\n </ngs-slider>\n }\n </div>\n }\n } @else {\n <div class=\"flex items-center gap-4\">\n @if (adj.id === 'border') {\n <button ngs-color-picker-thumbnail\n [color]=\"selectedLayer()?.['borderColor'] || selectedLayer()?.['fill'] || '#000000'\"\n [ngsColorPickerTriggerFor]=\"borderPicker\"\n class=\"cursor-pointer border border-border rounded\"></button>\n <ng-template #borderPicker>\n <ngs-color-picker [ngModel]=\"selectedLayer()?.['borderColor'] || selectedLayer()?.['fill'] || '#000000'\"\n (ngModelChange)=\"updateAdjustment('borderColor', $event)\"/>\n </ng-template>\n }\n <ngs-slider class=\"flex-1\"\n [min]=\"adj.min\"\n [max]=\"adj.max\"\n [step]=\"adj.step\">\n <input ngsSliderThumb\n [value]=\"selectedLayer()?.[adj.id] ?? adj.default\"\n (valueChange)=\"updateAdjustment(adj.id, $event)\">\n </ngs-slider>\n <input ngsInput\n type=\"number\"\n class=\"w-16 h-8 text-center text-sm border border-border rounded-md bg-surface-container\"\n [ngModel]=\"selectedLayer()?.[adj.id] ?? adj.default\"\n (ngModelChange)=\"updateAdjustment(adj.id, $event)\">\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n </ngs-panel-content>\n</ngs-panel>\n", styles: [":host{display:block;height:100%}:host .scrollbar-hide{-ms-overflow-style:none;scrollbar-width:none}:host .scrollbar-hide::-webkit-scrollbar{display:none}:host ngs-slider{--ngs-slider-track-height: 4px;--ngs-slider-thumb-size: 16px}:host input[type=number]::-webkit-inner-spin-button,:host input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.ngs-color-picker-thumbnail{width:32px;height:32px;min-width:32px;background:var(--ngs-color-picker-thumbnail-bg, #000)}\n"] }]
3203
+ }] });
3204
+
3205
+ class ImageDesigner {
3206
+ http = inject(HttpClient);
3207
+ destroyRef = inject(DestroyRef);
3208
+ designerService = inject(ImageDesignerService);
3209
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
3210
+ canvasContainer = viewChild('canvasContainer', ...(ngDevMode ? [{ debugName: "canvasContainer" }] : /* istanbul ignore next */ []));
3211
+ title = input('', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
3212
+ imageSize = input({
3213
+ width: 700,
3214
+ height: 400
3215
+ }, ...(ngDevMode ? [{ debugName: "imageSize" }] : /* istanbul ignore next */ []));
3216
+ defaultFont = input('Inter', ...(ngDevMode ? [{ debugName: "defaultFont" }] : /* istanbul ignore next */ []));
3217
+ scale = input(1, ...(ngDevMode ? [{ debugName: "scale" }] : /* istanbul ignore next */ []));
3218
+ minScale = input(0.1, ...(ngDevMode ? [{ debugName: "minScale" }] : /* istanbul ignore next */ []));
3219
+ maxScale = input(3, ...(ngDevMode ? [{ debugName: "maxScale" }] : /* istanbul ignore next */ []));
3220
+ showGuidelines = input(true, ...(ngDevMode ? [{ debugName: "showGuidelines" }] : /* istanbul ignore next */ []));
3221
+ snapToShapes = input(true, ...(ngDevMode ? [{ debugName: "snapToShapes" }] : /* istanbul ignore next */ []));
3222
+ snapToStageCenter = input(true, ...(ngDevMode ? [{ debugName: "snapToStageCenter" }] : /* istanbul ignore next */ []));
3223
+ snapToStageBorders = input(true, ...(ngDevMode ? [{ debugName: "snapToStageBorders" }] : /* istanbul ignore next */ []));
3224
+ guidelineColor = input('blue', ...(ngDevMode ? [{ debugName: "guidelineColor" }] : /* istanbul ignore next */ []));
3225
+ snapRange = input(5, ...(ngDevMode ? [{ debugName: "snapRange" }] : /* istanbul ignore next */ []));
3226
+ showDownloadButton = input(true, { ...(ngDevMode ? { debugName: "showDownloadButton" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3227
+ uploadFn = input(...(ngDevMode ? [undefined, { debugName: "uploadFn" }] : /* istanbul ignore next */ []));
3228
+ snapshot = input(...(ngDevMode ? [undefined, { debugName: "snapshot" }] : /* istanbul ignore next */ []));
3229
+ snapshotChanged = output();
3230
+ photosDataSource = input(...(ngDevMode ? [undefined, { debugName: "photosDataSource" }] : /* istanbul ignore next */ []));
3231
+ assetsDataSource = input(...(ngDevMode ? [undefined, { debugName: "assetsDataSource" }] : /* istanbul ignore next */ []));
3232
+ historyLimit = input(50, { ...(ngDevMode ? { debugName: "historyLimit" } : /* istanbul ignore next */ {}), transform: numberAttribute });
3233
+ activeItemId = signal('text', ...(ngDevMode ? [{ debugName: "activeItemId" }] : /* istanbul ignore next */ []));
3234
+ elements = signal(SVG_ELEMENTS, ...(ngDevMode ? [{ debugName: "elements" }] : /* istanbul ignore next */ []));
3235
+ photos = signal([], ...(ngDevMode ? [{ debugName: "photos" }] : /* istanbul ignore next */ []));
3236
+ assets = signal([], ...(ngDevMode ? [{ debugName: "assets" }] : /* istanbul ignore next */ []));
3237
+ photosFilter = signal('', ...(ngDevMode ? [{ debugName: "photosFilter" }] : /* istanbul ignore next */ []));
3238
+ assetsFilter = signal('', ...(ngDevMode ? [{ debugName: "assetsFilter" }] : /* istanbul ignore next */ []));
3239
+ uploadedImages = signal([], ...(ngDevMode ? [{ debugName: "uploadedImages" }] : /* istanbul ignore next */ []));
3240
+ isUploading = signal(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : /* istanbul ignore next */ []));
3241
+ uploadPreview = signal(null, ...(ngDevMode ? [{ debugName: "uploadPreview" }] : /* istanbul ignore next */ []));
3242
+ photosPage = signal(1, ...(ngDevMode ? [{ debugName: "photosPage" }] : /* istanbul ignore next */ []));
3243
+ assetsPage = signal(1, ...(ngDevMode ? [{ debugName: "assetsPage" }] : /* istanbul ignore next */ []));
3244
+ isLoadingPhotos = signal(false, ...(ngDevMode ? [{ debugName: "isLoadingPhotos" }] : /* istanbul ignore next */ []));
3245
+ isLoadingAssets = signal(false, ...(ngDevMode ? [{ debugName: "isLoadingAssets" }] : /* istanbul ignore next */ []));
3246
+ hasMoreAssets = signal(true, ...(ngDevMode ? [{ debugName: "hasMoreAssets" }] : /* istanbul ignore next */ []));
3247
+ hasMorePhotos = signal(true, ...(ngDevMode ? [{ debugName: "hasMorePhotos" }] : /* istanbul ignore next */ []));
3248
+ resizeWidth = signal(0, ...(ngDevMode ? [{ debugName: "resizeWidth" }] : /* istanbul ignore next */ []));
3249
+ resizeHeight = signal(0, ...(ngDevMode ? [{ debugName: "resizeHeight" }] : /* istanbul ignore next */ []));
3250
+ resizeUnit = signal('px', ...(ngDevMode ? [{ debugName: "resizeUnit" }] : /* istanbul ignore next */ []));
3251
+ presetCategories = signal(PRESET_CATEGORIES, ...(ngDevMode ? [{ debugName: "presetCategories" }] : /* istanbul ignore next */ []));
3252
+ presetColors = signal([
3253
+ // Basic & Neutral
3254
+ '#000000', '#7a7a7a', '#ffffff',
3255
+ // Reds & Pinks
3256
+ '#f44336', '#d32f2f', '#ff5252',
3257
+ '#e91e63', '#c2185b', '#ff4081',
3258
+ // Purples & Deep Purples
3259
+ '#9c27b0', '#7b1fa2', '#e040fb',
3260
+ '#673ab7', '#512da8', '#7c4dff',
3261
+ // Blues
3262
+ '#3f51b5', '#303f9f', '#536dfe',
3263
+ '#2196f3', '#1976d2', '#448aff',
3264
+ '#03a9f4', '#0288d1', '#40c4ff',
3265
+ // Teals & Cyans
3266
+ '#00bcd4', '#0097a7', '#18ffff',
3267
+ '#009688', '#00796b', '#64ffda',
3268
+ // Greens
3269
+ '#4caf50', '#388e3c', '#00c853',
3270
+ '#8bc34a', '#689f38', '#b2ff59',
3271
+ '#cddc39', '#afb42b', '#eeff41',
3272
+ // Yellows & Oranges
3273
+ '#ffeb3b', '#fbc02d', '#ffff00',
3274
+ '#ffc107', '#ffa000', '#ffc400',
3275
+ '#ff9800', '#f57c00', '#ff9100',
3276
+ '#ff5722', '#e64a19', '#ff3d00',
3277
+ // Browns
3278
+ '#795548', '#5d4037', '#8d6e63',
3279
+ // Blue Grays & Grays
3280
+ '#607d8b', '#455a64', '#78909c',
3281
+ ], ...(ngDevMode ? [{ debugName: "presetColors" }] : /* istanbul ignore next */ []));
3282
+ background = signal([], ...(ngDevMode ? [{ debugName: "background" }] : /* istanbul ignore next */ []));
3283
+ resize = signal([], ...(ngDevMode ? [{ debugName: "resize" }] : /* istanbul ignore next */ []));
3284
+ displayScale = this.designerService.scale;
3285
+ zoomPercentage = computed(() => (this.displayScale() * 100).toFixed(0), ...(ngDevMode ? [{ debugName: "zoomPercentage" }] : /* istanbul ignore next */ []));
3286
+ layersFromService = this.designerService.layers;
3287
+ selectedLayerId = this.designerService.selectedLayerId;
3288
+ presetPatterns = signal([
3289
+ ...SVG_PATTERNS.map(svg => `data:image/svg+xml;base64,${btoa(svg)}`),
3290
+ ], ...(ngDevMode ? [{ debugName: "presetPatterns" }] : /* istanbul ignore next */ []));
3291
+ presetGradients = computed(() => {
3292
+ const width = this.resizeWidth();
3293
+ const height = this.resizeHeight();
3294
+ const isLandscape = width >= height;
3295
+ const baseColors = [
3296
+ // --- VIBRANT & ENERGETIC ---
3297
+ ['#ff9a9e', '#fecfef'], ['#f093fb', '#f5576c'], ['#4facfe', '#00f2fe'],
3298
+ ['#43e97b', '#38f8d4'], ['#fa709a', '#fee140'], ['#30cfd0', '#330867'],
3299
+ ['#ff0844', '#ffb199'], ['#ff8177', '#ff867a'], ['#f83600', '#f9d423'],
3300
+ ['#b721ff', '#21d4fd'], ['#00dbde', '#fc00ff'], ['#8ec5fc', '#e0c3fc'],
3301
+ ['#6a11cb', '#2575fc'], ['#09203f', '#537895'], ['#00c6fb', '#005bea'],
3302
+ ['#ffecd2', '#fcb69f'], ['#ff758c', '#ff7eb3'], ['#868f96', '#596164'],
3303
+ ['#0ba360', '#3cba92'], ['#13547a', '#80d0c7'], ['#6a85b6', '#bac8e0'],
3304
+ ['#434343', '#000000'], ['#92fe9d', '#00c9ff'], ['#f40076', '#df98fa'],
3305
+ ['#f067b4', '#81ffef'], ['#ff4b1f', '#ff9068'], ['#16a085', '#f4d03f'],
3306
+ ['#00d2ff', '#3a7bd5'], ['#f7971e', '#ffd200'], ['#f12711', '#f5af19'],
3307
+ ['#12c2e9', '#c471ed'], ['#f64f59', '#12c2e9'], ['#74ebd5', '#acb6e5'],
3308
+ ['#ff9966', '#ff5e62'], ['#00b09b', '#96c93d'], ['#654ea3', '#eaafc8'],
3309
+ ['#4e54c8', '#8f94fb'], ['#ff0099', '#493240'], ['#8e2de2', '#4a00e0'],
3310
+ ['#3a1c71', '#d76d77'], ['#1f4037', '#99f2c8'], ['#56ab2f', '#a8e063'],
3311
+ ['#f11712', '#0099f7'], ['#11998e', '#38ef7d'], ['#fc466b', '#3f5efb'],
3312
+ ['#c94b4b', '#4b134f'], ['#00b4db', '#0083b0'], ['#7b4397', '#dc2430'],
3313
+ ['#1e3c72', '#2a5298'], ['#2c3e50', '#fd746c'], ['#f00000', '#dc281e'],
3314
+ ['#ff512f', '#dd2476'], ['#ff5f6d', '#ffc371'], ['#114357', '#f29492'],
3315
+ ['#40e0d0', '#ff8c00'], ['#ff0080', '#ff8c00'], ['#000046', '#1cb5e0'],
3316
+ ['#642b73', '#c6426e'], ['#cb2d3e', '#ef473a'], ['#5614b0', '#dbd65d'],
3317
+ ['#00c3ff', '#ffff1c'], ['#1d2b64', '#f8cdda'], ['#1a2a6c', '#b21f1f'],
3318
+ ['#cc2b5e', '#753a88'], ['#2193b0', '#6dd5ed'], ['#ee9ca7', '#ffdde1'],
3319
+ ['#e65100', '#fdb813'], ['#360033', '#0b8793'], ['#141e30', '#243b55'],
3320
+ ['#2c3e50', '#4ca1af'], ['#ff512f', '#f06292'], ['#70e1f5', '#ffd194'],
3321
+ ['#1c92d2', '#f2fcfe'], ['#3ca55c', '#b5ac49'], ['#4b6cb7', '#182848'],
3322
+ ['#00bf8f', '#001510'], ['#517fa4', '#243949'], ['#1e130c', '#9a8478'],
3323
+ ['#000000', '#533440'], ['#304352', '#d7d2cc'], ['#83a4d4', '#b6fbff'],
3324
+ ['#4568dc', '#b06ab3'], ['#ef3b36', '#ffffff'], ['#000000', '#434343'],
3325
+ ['#0f2027', '#203a43'], ['#373b44', '#4286f4'], ['#8e9eab', '#eef2f3']
3326
+ ];
3327
+ return baseColors.map(([c1, c2]) => ({
3328
+ x0: 0,
3329
+ y0: 0,
3330
+ x1: isLandscape ? width : 0,
3331
+ y1: isLandscape ? 0 : height,
3332
+ colorStops: [0, c1, 1, c2],
3333
+ css: `linear-gradient(${isLandscape ? '90deg' : '180deg'}, ${c1}, ${c2})`
3334
+ }));
3335
+ }, ...(ngDevMode ? [{ debugName: "presetGradients" }] : /* istanbul ignore next */ []));
3336
+ settingsPortal = signal(null, ...(ngDevMode ? [{ debugName: "settingsPortal" }] : /* istanbul ignore next */ []));
3337
+ effectsPortal = signal(null, ...(ngDevMode ? [{ debugName: "effectsPortal" }] : /* istanbul ignore next */ []));
3338
+ openEffects() {
3339
+ this.effectsPortal.set(new ComponentPortal(Effects));
3340
+ }
3341
+ constructor() {
3342
+ this.resizeWidth.set(this.imageSize().width);
3343
+ this.resizeHeight.set(this.imageSize().height);
3344
+ effect(() => {
3345
+ const size = this.imageSize();
3346
+ untracked(() => {
3347
+ this.resizeWidth.set(size.width);
3348
+ this.resizeHeight.set(size.height);
3349
+ });
3350
+ });
3351
+ this.designerService.change$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
3352
+ this.snapshotChanged.emit(this.designerService.getSnapshot());
3353
+ });
3354
+ effect(() => {
3355
+ this.designerService.setScale(this.scale());
3356
+ });
3357
+ effect(() => {
3358
+ this.designerService.setMinMaxScale(this.minScale(), this.maxScale());
3359
+ });
3360
+ effect(() => {
3361
+ this.designerService.updateSnapSettings({
3362
+ showGuidelines: this.showGuidelines(),
3363
+ snapToShapes: this.snapToShapes(),
3364
+ snapToStageCenter: this.snapToStageCenter(),
3365
+ snapToStageBorders: this.snapToStageBorders(),
3366
+ guidelineColor: this.guidelineColor(),
3367
+ snapRange: this.snapRange()
3368
+ });
3369
+ });
3370
+ effect(() => {
3371
+ const selectedId = this.selectedLayerId();
3372
+ if (selectedId) {
3373
+ this.settingsPortal.set(new ComponentPortal(Settings));
3374
+ }
3375
+ else {
3376
+ this.settingsPortal.set(null);
3377
+ this.effectsPortal.set(null);
3378
+ }
3379
+ });
3380
+ effect(() => {
3381
+ if (this.activeItemId() === 'photos' && this.photos().length === 0 && !this.isLoadingPhotos() && this.hasMorePhotos()) {
3382
+ this.loadPhotos();
3383
+ }
3384
+ });
3385
+ effect(() => {
3386
+ if ((this.activeItemId() === 'assets' || this.activeItemId() === 'upload') && this.assets().length === 0 && !this.isLoadingAssets() && this.hasMoreAssets()) {
3387
+ this.loadAssets();
3388
+ }
3389
+ });
3390
+ effect(() => {
3391
+ this.photosFilter();
3392
+ untracked(() => {
3393
+ if (this.activeItemId() === 'photos') {
3394
+ this.photos.set([]);
3395
+ this.photosPage.set(1);
3396
+ this.hasMorePhotos.set(true);
3397
+ this.loadPhotos();
3398
+ }
3399
+ });
3400
+ });
3401
+ effect(() => {
3402
+ this.assetsFilter();
3403
+ untracked(() => {
3404
+ if (this.activeItemId() === 'assets' || this.activeItemId() === 'upload') {
3405
+ this.assets.set([]);
3406
+ this.assetsPage.set(1);
3407
+ this.hasMoreAssets.set(true);
3408
+ this.loadAssets();
3409
+ }
3410
+ });
3411
+ });
3412
+ effect(() => {
3413
+ const snapshot = this.snapshot();
3414
+ if (snapshot && this.designerService.isInitialized()) {
3415
+ const nextVersion = snapshot.version ?? 0;
3416
+ const currentVersion = this.designerService.currentSnapshotVersion;
3417
+ if (nextVersion !== currentVersion) {
3418
+ console.log(`Snapshot version: ${nextVersion}, Designer version: ${currentVersion}`);
3419
+ console.log(`Snapshot version changed, loading new snapshot`);
3420
+ this.designerService.loadSnapshot(snapshot, true);
3421
+ }
3422
+ }
3423
+ });
3424
+ }
3425
+ ngOnInit() {
3426
+ this.designerService.historyLimit = this.historyLimit();
3427
+ if (this.assets().length > 0) {
3428
+ this.uploadedImages.set([...this.assets()]);
3429
+ }
3430
+ }
3431
+ onFileSelected(event) {
3432
+ if (event.files.length > 0) {
3433
+ const file = event.files[0];
3434
+ const uploadFn = this.uploadFn();
3435
+ if (uploadFn) {
3436
+ const result = uploadFn(file);
3437
+ this.isUploading.set(true);
3438
+ const handleSuccess = async (result) => {
3439
+ let photo;
3440
+ if (typeof result === 'string') {
3441
+ const dimensions = await this.getImageDimensions(result);
3442
+ photo = {
3443
+ id: Math.random().toString(36).substring(7),
3444
+ url: result,
3445
+ width: dimensions.width,
3446
+ height: dimensions.height
3447
+ };
3448
+ }
3449
+ else {
3450
+ photo = result;
3451
+ }
3452
+ this.uploadedImages.update(images => [photo, ...images]);
3453
+ this.isUploading.set(false);
3454
+ };
3455
+ const handleError = () => {
3456
+ this.isUploading.set(false);
3457
+ };
3458
+ if (isObservable(result)) {
3459
+ result.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
3460
+ next: handleSuccess,
3461
+ error: handleError
3462
+ });
3463
+ }
3464
+ else if (result instanceof Promise) {
3465
+ result.then(handleSuccess).catch(handleError);
3466
+ }
3467
+ else {
3468
+ handleSuccess(result);
3469
+ }
3470
+ }
3471
+ else {
3472
+ this.fakeUpload(file);
3473
+ }
3474
+ }
3475
+ }
3476
+ fakeUpload(file) {
3477
+ const reader = new FileReader();
3478
+ reader.onload = async (e) => {
3479
+ const url = e.target.result;
3480
+ const dimensions = await this.getImageDimensions(url);
3481
+ const photo = {
3482
+ id: Math.random().toString(36).substring(7),
3483
+ name: file.name,
3484
+ url: url,
3485
+ width: dimensions.width,
3486
+ height: dimensions.height
3487
+ };
3488
+ this.uploadedImages.update(images => [photo, ...images]);
3489
+ };
3490
+ reader.readAsDataURL(file);
3491
+ }
3492
+ getImageDimensions(url) {
3493
+ return new Promise(resolve => {
3494
+ const img = new Image();
3495
+ img.crossOrigin = 'anonymous';
3496
+ img.onload = () => {
3497
+ resolve({ width: img.width, height: img.height });
3498
+ };
3499
+ img.onerror = () => {
3500
+ resolve({ width: 0, height: 0 });
3501
+ };
3502
+ img.src = url;
3503
+ });
3504
+ }
3505
+ async addUploadedImage(photo, x = 0, y = 0) {
3506
+ const canvasWidth = this.resizeWidth();
3507
+ const canvasHeight = this.resizeHeight();
3508
+ const url = photo.url;
3509
+ let width = canvasWidth;
3510
+ let height = canvasWidth * (photo.height / photo.width);
3511
+ if (height > canvasHeight) {
3512
+ height = canvasHeight;
3513
+ width = height * (photo.width / photo.height);
3514
+ }
3515
+ const id = await this.designerService.addLayer({
3516
+ type: 'image',
3517
+ src: url,
3518
+ name: 'Uploaded Image',
3519
+ width,
3520
+ height,
3521
+ x,
3522
+ y
3523
+ });
3524
+ if (id) {
3525
+ this.selectLayer(id);
3526
+ }
3527
+ }
3528
+ loadAssets() {
3529
+ const dataSource = this.assetsDataSource();
3530
+ if (!dataSource || this.isLoadingAssets() || !this.hasMoreAssets()) {
3531
+ return;
3532
+ }
3533
+ this.isLoadingAssets.set(true);
3534
+ const pageSize = 30;
3535
+ const page = this.assetsPage();
3536
+ const startRow = (page - 1) * pageSize;
3537
+ const endRow = page * pageSize;
3538
+ dataSource.getItems({
3539
+ startRow,
3540
+ endRow,
3541
+ page,
3542
+ pageSize,
3543
+ filterModel: this.assetsFilter(),
3544
+ successCallback: (data, lastRow) => {
3545
+ this.assets.update(assets => [...assets, ...data]);
3546
+ if (data.length === 0) {
3547
+ this.hasMoreAssets.set(false);
3548
+ }
3549
+ this.isLoadingAssets.set(false);
3550
+ },
3551
+ failCallback: () => {
3552
+ this.isLoadingAssets.set(false);
3553
+ }
3554
+ });
3555
+ }
3556
+ onAssetsScroll(event) {
3557
+ const element = event.target;
3558
+ if (element.scrollHeight - element.scrollTop <= element.clientHeight + 50) {
3559
+ if (!this.isLoadingAssets() && this.hasMoreAssets()) {
3560
+ this.assetsPage.update(p => p + 1);
3561
+ this.loadAssets();
3562
+ }
3563
+ }
3564
+ }
3565
+ loadPhotos() {
3566
+ if (this.isLoadingPhotos() || !this.hasMorePhotos())
3567
+ return;
3568
+ this.isLoadingPhotos.set(true);
3569
+ const ds = this.photosDataSource() || this.defaultPhotosDataSource;
3570
+ const page = this.photosPage();
3571
+ const pageSize = 30;
3572
+ ds.getItems({
3573
+ startRow: (page - 1) * pageSize,
3574
+ endRow: page * pageSize,
3575
+ page: page,
3576
+ pageSize: pageSize,
3577
+ filterModel: this.photosFilter(),
3578
+ successCallback: (data, lastRow) => {
3579
+ this.photos.update(photos => [...photos, ...data]);
3580
+ if (data.length === 0) {
3581
+ this.hasMorePhotos.set(false);
3582
+ }
3583
+ this.isLoadingPhotos.set(false);
3584
+ },
3585
+ failCallback: () => {
3586
+ this.isLoadingPhotos.set(false);
3587
+ }
3588
+ });
3589
+ }
3590
+ defaultPhotosDataSource = createDefaultPhotosDataSource(this.http);
3591
+ onPhotosScroll(event) {
3592
+ const target = event.target;
3593
+ if (target && target.scrollHeight - target.scrollTop <= target.clientHeight + 100) {
3594
+ if (!this.isLoadingPhotos() && this.hasMorePhotos()) {
3595
+ this.photosPage.update(p => p + 1);
3596
+ this.loadPhotos();
3597
+ }
3598
+ }
3599
+ }
3600
+ async addImage(photo, x = 0, y = 0) {
3601
+ const canvasWidth = this.resizeWidth();
3602
+ const canvasHeight = this.resizeHeight();
3603
+ let width = canvasWidth;
3604
+ let height = canvasWidth * (photo.height / photo.width);
3605
+ if (height > canvasHeight) {
3606
+ height = canvasHeight;
3607
+ width = height * (photo.width / photo.height);
3608
+ }
3609
+ const id = await this.designerService.addLayer({
3610
+ type: 'image',
3611
+ src: photo.url,
3612
+ name: photo.name,
3613
+ width,
3614
+ height,
3615
+ x,
3616
+ y
3617
+ });
3618
+ if (id) {
3619
+ this.selectLayer(id);
3620
+ }
3621
+ }
3622
+ ngAfterViewInit() {
3623
+ if (!this.isBrowser) {
3624
+ return;
3625
+ }
3626
+ const container = this.canvasContainer()?.nativeElement;
3627
+ if (container) {
3628
+ console.log('Initializing ImageDesignerService in requestAnimationFrame');
3629
+ requestAnimationFrame(() => {
3630
+ if (!this.canvasContainer()) {
3631
+ console.warn('Canvas container disappeared before initialization');
3632
+ return;
3633
+ }
3634
+ const currentContainer = this.canvasContainer().nativeElement;
3635
+ // Final check before init, if dimensions are 0, wait a bit more
3636
+ if (currentContainer.offsetWidth === 0 || currentContainer.offsetHeight === 0) {
3637
+ console.warn('Container dimensions are still 0, retrying init');
3638
+ setTimeout(() => this.ngAfterViewInit(), 250);
3639
+ return;
3640
+ }
3641
+ console.log('Initializing ImageDesignerService with container:', currentContainer);
3642
+ console.log('Container dimensions in rAF:', currentContainer.offsetWidth, 'x', currentContainer.offsetHeight);
3643
+ this.designerService.init(currentContainer, this.imageSize(), this.defaultFont(), this.scale(), this.minScale(), this.maxScale()).then(() => {
3644
+ console.log('ImageDesignerService initialization completed');
3645
+ });
3646
+ });
3647
+ }
3648
+ else {
3649
+ console.warn('Canvas container not found during ngAfterViewInit');
3650
+ }
3651
+ }
3652
+ ngOnDestroy() {
3653
+ this.designerService.destroy();
3654
+ }
3655
+ toggleAside() {
3656
+ this.activeItemId.set(this.activeItemId() ? null : 'text');
3657
+ }
3658
+ zoomIn() {
3659
+ this.designerService.zoomIn();
3660
+ }
3661
+ zoomOut() {
3662
+ this.designerService.zoomOut();
3663
+ }
3664
+ undo() {
3665
+ this.designerService.undo();
3666
+ }
3667
+ redo() {
3668
+ this.designerService.redo();
3669
+ }
3670
+ resetZoom() {
3671
+ this.designerService.setScale(1);
3672
+ }
3673
+ onWheel(event) {
3674
+ if (event.metaKey || event.ctrlKey) {
3675
+ // preventDefault is not allowed during event replay (e.g. during hydration).
3676
+ // EventPhase.REPLAY is 101 in Angular.
3677
+ if (event.eventPhase !== 101) {
3678
+ event.preventDefault();
3679
+ }
3680
+ if (event.deltaY < 0) {
3681
+ this.zoomIn();
3682
+ }
3683
+ else {
3684
+ this.zoomOut();
3685
+ }
3686
+ }
3687
+ }
3688
+ onKeyDown(event) {
3689
+ const activeElement = document.activeElement;
3690
+ const isInput = activeElement instanceof HTMLInputElement ||
3691
+ activeElement instanceof HTMLTextAreaElement ||
3692
+ (activeElement instanceof HTMLElement && activeElement.isContentEditable);
3693
+ if (isInput) {
3694
+ return;
3695
+ }
3696
+ if (event.key === 'ArrowLeft' || event.key === 'ArrowRight' || event.key === 'ArrowUp' || event.key === 'ArrowDown') {
3697
+ event.preventDefault();
3698
+ const step = event.shiftKey ? 10 : 1;
3699
+ if (event.key === 'ArrowLeft') {
3700
+ this.designerService.moveSelectedLayers(-step, 0);
3701
+ }
3702
+ else if (event.key === 'ArrowRight') {
3703
+ this.designerService.moveSelectedLayers(step, 0);
3704
+ }
3705
+ else if (event.key === 'ArrowUp') {
3706
+ this.designerService.moveSelectedLayers(0, -step);
3707
+ }
3708
+ else if (event.key === 'ArrowDown') {
3709
+ this.designerService.moveSelectedLayers(0, step);
3710
+ }
3711
+ }
3712
+ if (event.key === 'Delete' || event.key === 'Backspace') {
3713
+ this.designerService.deleteSelectedLayers();
3714
+ }
3715
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'z') {
3716
+ event.preventDefault();
3717
+ if (event.shiftKey) {
3718
+ this.redo();
3719
+ }
3720
+ else {
3721
+ this.undo();
3722
+ }
3723
+ }
3724
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'y') {
3725
+ event.preventDefault();
3726
+ this.redo();
3727
+ }
3728
+ }
3729
+ selectLayer(id) {
3730
+ this.designerService.selectLayer(id);
3731
+ }
3732
+ toggleLayerVisibility(id, event) {
3733
+ event.stopPropagation();
3734
+ this.designerService.toggleLayerVisibility(id);
3735
+ }
3736
+ toggleLayerLock(id, event) {
3737
+ event.stopPropagation();
3738
+ this.designerService.toggleLayerLock(id);
3739
+ }
3740
+ deleteLayer(id, event) {
3741
+ event.stopPropagation();
3742
+ this.designerService.deleteLayer(id);
3743
+ }
3744
+ onDragOver(event) {
3745
+ if (event.dataTransfer?.types.includes('application/x-ngs-text-type') ||
3746
+ event.dataTransfer?.types.includes('application/x-ngs-image-data')) {
3747
+ event.preventDefault();
3748
+ event.dataTransfer.dropEffect = 'copy';
3749
+ }
3750
+ }
3751
+ onTextDragStart(event, type) {
3752
+ event.dataTransfer?.setData('application/x-ngs-text-type', type);
3753
+ if (event.target instanceof HTMLElement) {
3754
+ const el = event.target;
3755
+ el.classList.add('is-dragging');
3756
+ setTimeout(() => el.classList.remove('is-dragging'), 0);
3757
+ }
3758
+ }
3759
+ onImageDragStart(event, photo) {
3760
+ const data = 'url' in photo
3761
+ ? { url: photo.url, name: photo.name, width: photo.width, height: photo.height, type: 'image' }
3762
+ : { url: photo.data, name: photo.name, width: 100, height: 100, type: 'shape' };
3763
+ event.dataTransfer?.setData('application/x-ngs-image-data', JSON.stringify(data));
3764
+ if (event.target instanceof HTMLElement) {
3765
+ const el = event.target;
3766
+ el.classList.add('is-dragging');
3767
+ setTimeout(() => el.classList.remove('is-dragging'), 0);
3768
+ }
3769
+ }
3770
+ onDrop(event) {
3771
+ const textType = event.dataTransfer?.getData('application/x-ngs-text-type');
3772
+ const imageDataStr = event.dataTransfer?.getData('application/x-ngs-image-data');
3773
+ if (!textType && !imageDataStr)
3774
+ return;
3775
+ event.preventDefault();
3776
+ const container = this.canvasContainer()?.nativeElement;
3777
+ if (!container)
3778
+ return;
3779
+ const rect = container.getBoundingClientRect();
3780
+ const scale = this.designerService.scale();
3781
+ // Calculate position relative to the stage center
3782
+ const canvasRect = this.designerService['canvasRect'];
3783
+ if (!canvasRect)
3784
+ return;
3785
+ const centerX = canvasRect.x();
3786
+ const centerY = canvasRect.y();
3787
+ const x = (event.clientX - rect.left - centerX) / scale;
3788
+ const y = (event.clientY - rect.top - centerY) / scale;
3789
+ if (textType) {
3790
+ switch (textType) {
3791
+ case 'header':
3792
+ this.addHeader(x, y);
3793
+ break;
3794
+ case 'subheader':
3795
+ this.addSubheader(x, y);
3796
+ break;
3797
+ case 'body':
3798
+ this.addBodyText(x, y);
3799
+ break;
3800
+ }
3801
+ }
3802
+ else if (imageDataStr) {
3803
+ try {
3804
+ const data = JSON.parse(imageDataStr);
3805
+ if (data.type === 'image') {
3806
+ this.addImage({
3807
+ url: data.url,
3808
+ name: data.name,
3809
+ width: data.width,
3810
+ height: data.height,
3811
+ id: ''
3812
+ }, x, y);
3813
+ }
3814
+ else if (data.type === 'shape') {
3815
+ this.addShape({
3816
+ name: data.name,
3817
+ data: data.url
3818
+ }, x, y);
3819
+ }
3820
+ }
3821
+ catch (e) {
3822
+ console.error('Failed to parse image data', e);
3823
+ }
3824
+ }
3825
+ }
3826
+ drop(event) {
3827
+ if (event.previousIndex !== event.currentIndex) {
3828
+ this.designerService.reorderLayers(event.previousIndex, event.currentIndex);
3829
+ }
3830
+ }
3831
+ download() {
3832
+ this.designerService.downloadImage();
3833
+ }
3834
+ getBase64Image() {
3835
+ return this.designerService.getBase64Image();
3836
+ }
3837
+ getLayerIcon(type) {
3838
+ const iconMap = {
3839
+ 'text': 'fluent:text-field-24-regular',
3840
+ 'image': 'fluent:image-24-regular',
3841
+ 'pattern': 'fluent:grid-dots-24-regular',
3842
+ };
3843
+ return iconMap[type || ''] || 'fluent:shape-subtract-24-regular';
3844
+ }
3845
+ selectedFontSize = signal(24, ...(ngDevMode ? [{ debugName: "selectedFontSize" }] : /* istanbul ignore next */ []));
3846
+ selectedFontWeight = signal(400, ...(ngDevMode ? [{ debugName: "selectedFontWeight" }] : /* istanbul ignore next */ []));
3847
+ async addHeader(x = 0, y = 0) {
3848
+ const id = await this.designerService.addLayer({
3849
+ type: 'text',
3850
+ text: 'Add a heading',
3851
+ fontSize: 40,
3852
+ fontWeight: 'bold',
3853
+ fontFamily: 'Inter',
3854
+ name: 'Heading',
3855
+ align: 'center',
3856
+ x,
3857
+ y
3858
+ });
3859
+ if (id) {
3860
+ this.selectLayer(id);
3861
+ }
3862
+ }
3863
+ async addSubheader(x = 0, y = 0) {
3864
+ const id = await this.designerService.addLayer({
3865
+ type: 'text',
3866
+ text: 'Add a subheading',
3867
+ fontSize: 24,
3868
+ fontWeight: '600',
3869
+ fontFamily: 'Inter',
3870
+ name: 'Subheading',
3871
+ align: 'center',
3872
+ x,
3873
+ y
3874
+ });
3875
+ if (id) {
3876
+ this.selectLayer(id);
3877
+ }
3878
+ }
3879
+ async addBodyText(x = 0, y = 0) {
3880
+ const id = await this.designerService.addLayer({
3881
+ type: 'text',
3882
+ text: 'Add body text',
3883
+ fontSize: 16,
3884
+ fontFamily: 'Inter',
3885
+ name: 'Body text',
3886
+ align: 'center',
3887
+ x,
3888
+ y
3889
+ });
3890
+ if (id) {
3891
+ this.selectLayer(id);
3892
+ }
3893
+ }
3894
+ async addShape(shape, x = 0, y = 0) {
3895
+ const id = await this.designerService.addLayer({
3896
+ type: 'shape',
3897
+ name: shape.name,
3898
+ data: shape.data,
3899
+ width: 100,
3900
+ height: 100,
3901
+ fill: '#64748b',
3902
+ x,
3903
+ y
3904
+ });
3905
+ if (id) {
3906
+ this.selectLayer(id);
3907
+ }
3908
+ }
3909
+ setGradient(gradient) {
3910
+ this.designerService.setCanvasBackground({
3911
+ x0: gradient.x0,
3912
+ y0: gradient.y0,
3913
+ x1: gradient.x1,
3914
+ y1: gradient.y1,
3915
+ colorStops: gradient.colorStops
3916
+ });
3917
+ }
3918
+ setColor(color) {
3919
+ this.designerService.setCanvasBackground(color);
3920
+ }
3921
+ async addPattern(url) {
3922
+ const id = await this.designerService.addLayer({
3923
+ type: 'pattern',
3924
+ name: 'Pattern',
3925
+ patternImage: url,
3926
+ width: this.resizeWidth(),
3927
+ height: this.resizeHeight(),
3928
+ x: 0,
3929
+ y: 0
3930
+ });
3931
+ if (id) {
3932
+ this.selectLayer(id);
3933
+ }
3934
+ }
3935
+ applyResize() {
3936
+ this.designerService.updateSize(this.canvasContainer().nativeElement, {
3937
+ width: this.resizeWidth(),
3938
+ height: this.resizeHeight()
3939
+ }, true, true);
3940
+ }
3941
+ selectPreset(preset) {
3942
+ this.resizeWidth.set(preset.width);
3943
+ this.resizeHeight.set(preset.height);
3944
+ this.applyResize();
3945
+ }
3946
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ImageDesigner, deps: [], target: i0.ɵɵFactoryTarget.Component });
3947
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: ImageDesigner, isStandalone: true, selector: "ngs-image-designer", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, imageSize: { classPropertyName: "imageSize", publicName: "imageSize", isSignal: true, isRequired: false, transformFunction: null }, defaultFont: { classPropertyName: "defaultFont", publicName: "defaultFont", isSignal: true, isRequired: false, transformFunction: null }, scale: { classPropertyName: "scale", publicName: "scale", isSignal: true, isRequired: false, transformFunction: null }, minScale: { classPropertyName: "minScale", publicName: "minScale", isSignal: true, isRequired: false, transformFunction: null }, maxScale: { classPropertyName: "maxScale", publicName: "maxScale", isSignal: true, isRequired: false, transformFunction: null }, showGuidelines: { classPropertyName: "showGuidelines", publicName: "showGuidelines", isSignal: true, isRequired: false, transformFunction: null }, snapToShapes: { classPropertyName: "snapToShapes", publicName: "snapToShapes", isSignal: true, isRequired: false, transformFunction: null }, snapToStageCenter: { classPropertyName: "snapToStageCenter", publicName: "snapToStageCenter", isSignal: true, isRequired: false, transformFunction: null }, snapToStageBorders: { classPropertyName: "snapToStageBorders", publicName: "snapToStageBorders", isSignal: true, isRequired: false, transformFunction: null }, guidelineColor: { classPropertyName: "guidelineColor", publicName: "guidelineColor", isSignal: true, isRequired: false, transformFunction: null }, snapRange: { classPropertyName: "snapRange", publicName: "snapRange", isSignal: true, isRequired: false, transformFunction: null }, showDownloadButton: { classPropertyName: "showDownloadButton", publicName: "showDownloadButton", isSignal: true, isRequired: false, transformFunction: null }, uploadFn: { classPropertyName: "uploadFn", publicName: "uploadFn", isSignal: true, isRequired: false, transformFunction: null }, snapshot: { classPropertyName: "snapshot", publicName: "snapshot", isSignal: true, isRequired: false, transformFunction: null }, photosDataSource: { classPropertyName: "photosDataSource", publicName: "photosDataSource", isSignal: true, isRequired: false, transformFunction: null }, assetsDataSource: { classPropertyName: "assetsDataSource", publicName: "assetsDataSource", isSignal: true, isRequired: false, transformFunction: null }, historyLimit: { classPropertyName: "historyLimit", publicName: "historyLimit", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { snapshotChanged: "snapshotChanged" }, host: { listeners: { "keydown": "onKeyDown($event)" }, classAttribute: "ngs-image-designer" }, providers: [
3948
+ ImageDesignerService,
3949
+ {
3950
+ provide: IMAGE_DESIGNER,
3951
+ useExisting: forwardRef(() => ImageDesigner)
3952
+ }
3953
+ ], viewQueries: [{ propertyName: "canvasContainer", first: true, predicate: ["canvasContainer"], descendants: true, isSignal: true }], exportAs: ["ngsImageDesigner"], ngImport: i0, template: "<ngs-panel class=\"h-full\">\n <ngs-panel-sidebar class=\"border-r border-border h-full\">\n <ngs-tab-panel hideContentIfTabNotSelected [activeItemId]=\"activeItemId()\" class=\"h-full\">\n <ngs-tab-panel-content>\n <ngs-tab-panel-nav>\n <ngs-tab-panel-item for=\"text\" (click)=\"activeItemId.set('text')\">\n <ngs-icon name=\"fluent:text-field-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Text</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"photos\" (click)=\"activeItemId.set('photos')\">\n <ngs-icon name=\"fluent:image-copy-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Photos</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"elements\" (click)=\"activeItemId.set('elements')\">\n <ngs-icon name=\"fluent:shape-subtract-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Elements</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"upload\" (click)=\"activeItemId.set('upload')\">\n <ngs-icon name=\"fluent:arrow-upload-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Upload</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"background\" (click)=\"activeItemId.set('background')\">\n <ngs-icon name=\"fluent:grid-dots-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Background</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"layers\" (click)=\"activeItemId.set('layers')\">\n <ngs-icon name=\"fluent:layer-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Layers</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"resize\" (click)=\"activeItemId.set('resize')\">\n <ngs-icon name=\"fluent:resize-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Resize</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n </ngs-tab-panel-nav>\n </ngs-tab-panel-content>\n <ngs-tab-panel-aside class=\"border-s border-border\">\n <ng-template ngsTabPanelAsideContent=\"text\">\n <ngs-panel>\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">Text</ngs-panel-header>\n <ngs-panel-content class=\"p-4 flex flex-col gap-4\">\n <div class=\"grid grid-cols-1 gap-4 mt-2\">\n <button (click)=\"addHeader()\"\n draggable=\"true\"\n (dragstart)=\"onTextDragStart($event, 'header')\"\n class=\"w-full text-left p-4 border border-border rounded-lg hover:bg-surface-container transition-colors\">\n <div class=\"text-3xl font-bold pointer-events-none\">Add a heading</div>\n </button>\n <button (click)=\"addSubheader()\"\n draggable=\"true\"\n (dragstart)=\"onTextDragStart($event, 'subheader')\"\n class=\"w-full text-left p-3 border border-border rounded-lg hover:bg-surface-container transition-colors\">\n <div class=\"text-xl font-semibold pointer-events-none\">Add a subheading</div>\n </button>\n <button (click)=\"addBodyText()\"\n draggable=\"true\"\n (dragstart)=\"onTextDragStart($event, 'body')\"\n class=\"w-full text-left p-2 border border-border rounded-lg hover:bg-surface-container transition-colors\">\n <div class=\"text-base pointer-events-none\">Add body text</div>\n </button>\n </div>\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"photos\">\n <ngs-panel class=\"h-full flex flex-col\">\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">\n<!-- <ngs-form-field class=\"w-full\" subscriptHiddenIfEmpty>-->\n<!-- <ngs-icon name=\"fluent:search-24-regular\" ngsIconPrefix/>-->\n<!-- <input type=\"text\"-->\n<!-- ngsInput-->\n<!-- placeholder=\"Search photos\"-->\n<!-- [ngModel]=\"photosFilter()\"-->\n<!-- (ngModelChange)=\"photosFilter.set($event)\"/>-->\n<!-- </ngs-form-field>-->\n Photos\n </ngs-panel-header>\n <ngs-panel-content class=\"relative flex-1 p-0 overflow-hidden\">\n <ngs-scrollbar-area (scrolled)=\"onPhotosScroll($event)\" class=\"h-full\">\n <div class=\"p-4\">\n <div class=\"masonry-grid\">\n @for (photo of photos(); track photo.id) {\n <button (click)=\"addImage(photo)\"\n draggable=\"true\"\n (dragstart)=\"onImageDragStart($event, photo)\"\n [style.aspect-ratio]=\"photo.width + '/' + photo.height\"\n class=\"border border-border rounded-lg overflow-hidden hover:ring-2 hover:ring-primary transition-all bg-surface-container\">\n <img [src]=\"photo.thumbUrl || photo.url\"\n [alt]=\"photo.name\"\n class=\"w-full h-auto block pointer-events-none\"/>\n </button>\n }\n </div>\n @if (isLoadingPhotos()) {\n <div class=\"flex justify-center p-4\">\n <ngs-progress-spinner diameter=\"32\"/>\n </div>\n }\n @if (photos().length === 0 && !isLoadingPhotos()) {\n <div class=\"text-center p-4 text-sm text-neutral-500\">\n No photos found\n </div>\n }\n </div>\n </ngs-scrollbar-area>\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"elements\">\n <ngs-panel>\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">Elements</ngs-panel-header>\n <ngs-panel-content class=\"p-4 overflow-y-auto\">\n <div class=\"grid grid-cols-3 gap-3\">\n @for (element of elements(); track element.name) {\n <button (click)=\"addShape(element)\"\n draggable=\"true\"\n (dragstart)=\"onImageDragStart($event, element)\"\n class=\"aspect-square p-2 border border-border rounded-lg hover:bg-surface-container\n transition-colors flex items-center justify-center\">\n <img [src]=\"element.data\" [alt]=\"element.name\"\n class=\"max-w-full max-h-full object-contain pointer-events-none\"/>\n </button>\n }\n </div>\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"upload\">\n <ngs-panel class=\"h-full flex flex-col\">\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">\n <ngs-toolbar>\n <div>Upload</div>\n <ngs-toolbar-spacer/>\n <button ngsButton=\"filled\"\n ngsUploadTrigger\n [accept]=\"'image/*'\"\n (fileSelected)=\"onFileSelected($event)\"\n [loading]=\"isUploading()\">\n <ngs-icon name=\"fluent:arrow-upload-24-regular\"/>\n Add Image\n </button>\n </ngs-toolbar>\n </ngs-panel-header>\n <ngs-panel-content>\n @if (uploadedImages().length > 0 || assets().length > 0) {\n <ngs-scrollbar-area (scrolled)=\"onAssetsScroll($event)\">\n <div class=\"p-4 flex flex-col gap-4 min-h-full\">\n @if (isUploading() || uploadPreview()) {\n <div class=\"relative aspect-video border border-border rounded-lg overflow-hidden bg-surface-container\">\n @if (uploadPreview()) {\n <img [src]=\"uploadPreview()\" class=\"w-full h-full object-contain\" alt=\"Preview\"/>\n }\n @if (isUploading()) {\n <div class=\"absolute inset-0 bg-black/20 flex items-center justify-center backdrop-blur-[2px]\">\n <div class=\"flex flex-col items-center gap-2\">\n <ngs-progress-spinner diameter=\"32\" color=\"white\"/>\n <span class=\"text-xs font-medium text-white\">Uploading...</span>\n </div>\n </div>\n }\n </div>\n }\n\n @if (uploadedImages().length > 0) {\n <div class=\"flex flex-col gap-2\">\n <div class=\"text-sm font-semibold\">Your Uploads</div>\n <div class=\"masonry-grid\">\n @for (photo of uploadedImages(); track $index) {\n <button (click)=\"addUploadedImage(photo)\"\n draggable=\"true\"\n (dragstart)=\"onImageDragStart($event, photo)\"\n [style.aspect-ratio]=\"photo.width + '/' + photo.height\"\n class=\"border border-border rounded-lg overflow-hidden hover:ring-2\n hover:ring-primary transition-all bg-surface-container\">\n <img [src]=\"photo.url\" class=\"w-full h-auto block pointer-events-none\" alt=\"Uploaded image\"/>\n </button>\n }\n </div>\n </div>\n }\n\n @if (assets().length > 0) {\n <div class=\"flex flex-col gap-2\">\n <div class=\"text-sm font-semibold\">Assets</div>\n <div class=\"masonry-grid\">\n @for (asset of assets(); track asset.id) {\n <button (click)=\"addImage(asset)\"\n draggable=\"true\"\n (dragstart)=\"onImageDragStart($event, asset)\"\n [style.aspect-ratio]=\"asset.width + '/' + asset.height\"\n class=\"border border-border rounded-lg overflow-hidden hover:ring-2\n hover:ring-primary transition-all bg-surface-container\">\n <img [src]=\"asset.thumbUrl || asset.url\"\n [alt]=\"asset.name\"\n class=\"w-full h-auto block pointer-events-none\"/>\n </button>\n }\n </div>\n @if (isLoadingAssets()) {\n <div class=\"flex justify-center p-4\">\n <ngs-progress-spinner diameter=\"32\"/>\n </div>\n }\n </div>\n }\n </div>\n </ngs-scrollbar-area>\n }\n\n @if (uploadedImages().length === 0 && assets().length === 0 && !isLoadingAssets()) {\n <div class=\"h-full flex items-center justify-center\">\n <div class=\"text-sm text-neutral-500\">No images uploaded yet.</div>\n </div>\n }\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"background\">\n <ngs-panel>\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">Background</ngs-panel-header>\n <ngs-panel-content>\n <ngs-tab-group animationDuration=\"0\" class=\"h-full\">\n <ngs-tab label=\"Color\">\n <div class=\"absolute inset-0\">\n <ngs-scrollbar-area>\n <div class=\"p-4\">\n <ngs-accordion [multi]=\"false\">\n <ngs-expansion-panel [expanded]=\"true\">\n <ngs-expansion-panel-header>\n <ngs-expansion-panel-title>Colors</ngs-expansion-panel-title>\n </ngs-expansion-panel-header>\n <div class=\"py-2\">\n <ngs-color-switcher [colors]=\"presetColors()\" (colorChange)=\"setColor($event)\"/>\n </div>\n </ngs-expansion-panel>\n <ngs-expansion-panel>\n <ngs-expansion-panel-header>\n <ngs-expansion-panel-title>Gradients</ngs-expansion-panel-title>\n </ngs-expansion-panel-header>\n <div class=\"grid grid-cols-3 gap-3 py-2\">\n @for (gradient of presetGradients(); track $index) {\n <button (click)=\"setGradient(gradient)\"\n [style.background]=\"gradient.css\"\n class=\"aspect-[16/6.75] w-full rounded-lg border border-border hover:ring-2 hover:ring-primary transition-all\">\n </button>\n }\n </div>\n </ngs-expansion-panel>\n </ngs-accordion>\n </div>\n </ngs-scrollbar-area>\n </div>\n </ngs-tab>\n <ngs-tab label=\"Patterns\">\n <div class=\"absolute inset-0\">\n <ngs-scrollbar-area>\n <div class=\"p-4\">\n <div class=\"grid grid-cols-3 gap-3\">\n @for (pattern of presetPatterns(); track $index) {\n <button (click)=\"addPattern(pattern)\"\n class=\"aspect-square w-full rounded-lg border border-border hover:ring-2 hover:ring-primary transition-all overflow-hidden\">\n <img [src]=\"pattern\" class=\"w-full h-full object-cover\" alt=\"pattern\">\n </button>\n }\n </div>\n </div>\n </ngs-scrollbar-area>\n </div>\n </ngs-tab>\n </ngs-tab-group>\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"layers\">\n <ngs-panel>\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">Layers</ngs-panel-header>\n <ngs-panel-content>\n @if (layersFromService().length > 0) {\n <ngs-list cdkDropList (cdkDropListDropped)=\"drop($event)\">\n @for (layer of layersFromService(); track layer.id) {\n <ngs-list-item cdkDrag\n cdkDragLockAxis=\"y\"\n [cdkDragStartDelay]=\"100\"\n (click)=\"selectLayer(layer.id!)\"\n [class.is-active]=\"selectedLayerId() === layer.id\"\n class=\"relative group bg-surface\">\n <ngs-icon [name]=\"getLayerIcon(layer.type)\" ngsListItemIcon/>\n <div ngsListItemLine>\n {{ layer.name || layer.type }}\n <!-- {{ layer.text || '' }}-->\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 flex items-center\">\n <button ngsIconButton (click)=\"toggleLayerVisibility(layer.id!, $event)\">\n <ngs-icon\n [name]=\"layer.visible === false ? 'fluent:eye-off-24-regular' : 'fluent:eye-24-regular'\"/>\n </button>\n <button ngsIconButton (click)=\"toggleLayerLock(layer.id!, $event)\">\n <ngs-icon\n [name]=\"layer.locked ? 'fluent:lock-closed-24-regular' : 'fluent:lock-open-24-regular'\"\n class=\"size-5\"/>\n </button>\n <button ngsIconButton (click)=\"deleteLayer(layer.id!, $event)\" [disabled]=\"layer.locked\">\n <ngs-icon name=\"fluent:delete-24-regular\" class=\"size-5\"/>\n </button>\n </div>\n\n <div *cdkDragPlaceholder\n class=\"min-h-[var(--ngs-list-item-min-height)] bg-surface-container\"></div>\n </ngs-list-item>\n }\n </ngs-list>\n } @else {\n <div class=\"text-sm p-4 h-full flex items-center justify-center\">No layers available</div>\n }\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"resize\">\n <ngs-panel>\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">Resize</ngs-panel-header>\n <ngs-panel-content>\n <ngs-scrollbar-area>\n <div class=\"p-4 flex flex-col gap-4\">\n <div class=\"flex flex-col gap-4\">\n <ngs-form-field subscriptHiddenIfEmpty>\n <ngs-label>Width (px)</ngs-label>\n <input ngsInput type=\"number\" [(ngModel)]=\"resizeWidth\">\n </ngs-form-field>\n\n <ngs-form-field subscriptHiddenIfEmpty>\n <ngs-label>Height (px)</ngs-label>\n <input ngsInput type=\"number\" [(ngModel)]=\"resizeHeight\">\n </ngs-form-field>\n\n <ngs-form-field subscriptHiddenIfEmpty>\n <ngs-label>Units</ngs-label>\n <ngs-select [(ngModel)]=\"resizeUnit\">\n <ngs-option value=\"px\">px</ngs-option>\n </ngs-select>\n </ngs-form-field>\n\n <button ngsButton=\"filled\" fullWidth (click)=\"applyResize()\">Resize</button>\n </div>\n\n <div class=\"flex flex-col gap-6 mt-2\">\n @for (category of presetCategories(); track category.name) {\n <div class=\"flex flex-col gap-3\">\n <div class=\"flex items-center gap-2 font-semibold text-sm\">\n <ngs-icon [name]=\"category.icon\" class=\"size-5\"/>\n {{ category.name }}\n </div>\n\n <div class=\"grid grid-cols-3 gap-3\">\n @for (preset of category.presets; track preset.name) {\n <button (click)=\"selectPreset(preset)\"\n class=\"flex flex-col items-center gap-1 p-2 rounded-lg border border-border hover:bg-surface-container transition-colors text-center\">\n <ngs-icon [name]=\"preset.icon\" class=\"size-6 mb-1\"/>\n <span class=\"text-xs font-medium truncate w-full\">{{ preset.name }}</span>\n <span class=\"text-[10px] text-muted-foreground whitespace-nowrap\">{{ preset.width }}\u00D7{{ preset.height }} px</span>\n </button>\n }\n </div>\n </div>\n }\n </div>\n </div>\n </ngs-scrollbar-area>\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n\n @if (effectsPortal()) {\n <div class=\"absolute inset-0 z-20 bg-surface-container-lowest\">\n <ng-container [cdkPortalOutlet]=\"effectsPortal()\"/>\n </div>\n }\n </ngs-tab-panel-aside>\n </ngs-tab-panel>\n </ngs-panel-sidebar>\n <ngs-panel-content class=\"h-full\">\n <ngs-panel class=\"h-full\">\n <ngs-panel-header>\n <ngs-toolbar class=\"h-full px-3 border-b border-border\">\n <button ngsIconButton (click)=\"toggleAside()\">\n <ngs-icon name=\"fluent:navigation-24-regular\"/>\n </button>\n <ngs-toolbar-title>{{ title() }}</ngs-toolbar-title>\n <ngs-toolbar-spacer/>\n <div class=\"flex items-center gap-2\">\n <button ngsIconButton (click)=\"zoomOut()\">\n <ngs-icon name=\"fluent:zoom-out-24-regular\"/>\n </button>\n <span class=\"text-sm font-medium w-12 text-center\">{{ zoomPercentage() }}%</span>\n <button ngsIconButton (click)=\"zoomIn()\">\n <ngs-icon name=\"fluent:zoom-in-24-regular\"/>\n </button>\n </div>\n <ngs-toolbar-spacer/>\n <div class=\"flex\">\n <button ngsIconButton (click)=\"undo()\" [disabled]=\"!designerService.canUndo()\">\n <ngs-icon name=\"fluent:arrow-hook-up-left-24-regular\"/>\n </button>\n <button ngsIconButton (click)=\"redo()\" [disabled]=\"!designerService.canRedo()\">\n <ngs-icon name=\"fluent:arrow-hook-up-right-24-regular\"/>\n </button>\n </div>\n @if (showDownloadButton()) {\n <ngs-divider vertical/>\n <button ngsButton=\"filled\" (click)=\"download()\" class=\"mr-2\">Download</button>\n }\n </ngs-toolbar>\n </ngs-panel-header>\n <ngs-panel-content class=\"bg-surface-container overflow-hidden h-full relative\" (wheel)=\"onWheel($event)\"\n (mousedown)=\"canvasContainer.focus()\"\n (dragover)=\"onDragOver($event)\"\n (drop)=\"onDrop($event)\">\n @if (settingsPortal()) {\n <ng-container [cdkPortalOutlet]=\"settingsPortal()\"/>\n }\n <div #canvasContainer class=\"w-full h-full outline-none\" tabindex=\"0\" (contextmenu)=\"$event.preventDefault()\"></div>\n </ngs-panel-content>\n </ngs-panel>\n </ngs-panel-content>\n</ngs-panel>\n", styles: [":host{display:block;width:100%;height:100%}:host ngs-tab-panel{--ngs-tab-panel-aside-width: calc(var(--spacing, .25rem) * 90);--ngs-tab-panel-nav-padding: calc(var(--spacing, .25rem) * 3) 0}:host ngs-list{--ngs-list-padding: 0;--ngs-list-item-radius: 0}:host ngs-tab-group{--ngs-tab-label-padding: 16px 16px}:host .masonry-grid{column-count:2;column-gap:12px}:host .masonry-grid>*{break-inside:avoid;margin-bottom:12px;display:block;width:100%}:host button.is-dragging{background:transparent!important;border-color:transparent!important;box-shadow:none!important;transition:none!important;outline:none!important}:host button.is-dragging:hover{background:transparent!important;border-color:transparent!important;box-shadow:none!important}:host button.is-dragging *{color:currentColor!important}\n/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "component", type: Panel, selector: "ngs-panel", inputs: ["absolute"], exportAs: ["ngsPanel"] }, { kind: "component", type: PanelSidebar, selector: "ngs-panel-sidebar" }, { kind: "component", type: PanelContent, selector: "ngs-panel-content", exportAs: ["ngsPanelContent"] }, { kind: "component", type: PanelHeader, selector: "ngs-panel-header", inputs: ["autoHeight"], exportAs: ["ngsPanelHeader"] }, { kind: "component", type: Toolbar, selector: "ngs-toolbar", exportAs: ["ngsToolbar"] }, { kind: "component", type: Button, selector: " button[ngsButton], button[ngsIconButton], a[ngsButton], a[ngsIconButton] ", inputs: ["ngsButton", "ngsIconButton", "loading", "disabled", "disabledInteractive", "disableRipple", "reverse", "fullWidth", "hideTextOnMobile"], exportAs: ["ngsButton"] }, { kind: "component", type: ToolbarSpacer, selector: "ngs-toolbar-spacer" }, { kind: "component", type: Divider, selector: "ngs-divider", inputs: ["vertical", "inset", "fixedHeight"], exportAs: ["ngsDivider"] }, { kind: "component", type: Icon, selector: "ngs-icon", inputs: ["name"], exportAs: ["ngsIcon"] }, { kind: "component", type: ToolbarTitle, selector: "ngs-toolbar-title" }, { kind: "component", type: TabPanel, selector: "ngs-tab-panel", inputs: ["hideContentIfTabNotSelected", "activeItemId", "compact"], outputs: ["itemIdChanged"], exportAs: ["ngsTabPanel"] }, { kind: "component", type: TabPanelAside, selector: "ngs-tab-panel-aside", exportAs: ["ngsTabPanelAside"] }, { kind: "directive", type: TabPanelAsideContentDirective, selector: "[ngsTabPanelAsideContent]", inputs: ["ngsTabPanelAsideContent"], exportAs: ["ngsTabPanelAsideContent"] }, { kind: "component", type: TabPanelContent, selector: "ngs-tab-panel-content", exportAs: ["ngsTabPanelContent"] }, { kind: "component", type: TabPanelItem, selector: "ngs-tab-panel-item", inputs: ["for"], exportAs: ["ngsTabPanelItem"] }, { kind: "directive", type: TabPanelItemIconDirective, selector: "[ngsTabPanelItemIcon]", exportAs: ["ngsTabPanelItemIcon"] }, { kind: "component", type: TabPanelItemText, selector: "ngs-tab-panel-item-text", exportAs: ["ngsTabPanelItemText"] }, { kind: "component", type: TabPanelNav, selector: "ngs-tab-panel-nav", exportAs: ["ngsTabPanelNav"] }, { kind: "component", type: List, selector: "ngs-list", inputs: ["disabled", "disableRipple"], exportAs: ["ngsList"] }, { kind: "component", type: ListItem, selector: "ngs-list-item, a[ngs-list-item], button[ngs-list-item]", inputs: ["disabled", "lines"], exportAs: ["ngsListItem"] }, { kind: "directive", type: ListItemLine, selector: "[ngsListItemLine], [ngsLine]" }, { kind: "directive", type: CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "component", type: TabGroup, selector: "ngs-tab-group", inputs: ["selectedIndex", "headerPosition", "preserveContent", "ngs-stretch-tabs", "ngs-align-tabs", "disableRipple", "animationDuration", "animate.enter", "animate.leave"], outputs: ["selectedIndexChange", "selectedTabChange", "focusChange"] }, { kind: "component", type: Tab, selector: "ngs-tab", inputs: ["label", "aria-label", "aria-labelledby", "disabled"], exportAs: ["ngsTab"] }, { kind: "component", type: Accordion, selector: "ngs-accordion", inputs: ["multi", "hideToggle"], exportAs: ["ngsAccordion"] }, { kind: "component", type: ExpansionPanel, selector: "ngs-expansion-panel", inputs: ["disabled", "expanded", "hideToggle"], outputs: ["expandedChange", "opened", "closed"], exportAs: ["ngsExpansionPanel"] }, { kind: "component", type: ExpansionPanelHeader, selector: "ngs-expansion-panel-header", inputs: ["hideToggle"] }, { kind: "component", type: ExpansionPanelTitle, selector: "ngs-expansion-panel-title", exportAs: ["ngsExpansionPanelTitle"] }, { kind: "component", type: FormField, selector: "ngs-form-field", inputs: ["subscriptHiddenIfEmpty"], exportAs: ["ngsFormField"] }, { kind: "component", type: Label, selector: "ngs-label" }, { kind: "directive", type: Input, selector: "input[ngsInput], textarea[ngsInput]", inputs: ["id", "placeholder", "required", "disabled", "readonly", "errorStateMatcher"], exportAs: ["ngsInput"] }, { kind: "component", type: Select, selector: "ngs-select", inputs: ["id", "placeholder", "disabled", "required", "multiple", "hideCheckIcon", "ariaLabel", "tabIndex", "aria-describedby", "value"], outputs: ["selectionChange", "opened", "closed", "valueChange"], exportAs: ["ngsSelect"] }, { kind: "component", type: Option, selector: "ngs-option", inputs: ["value", "disabled", "selected"], outputs: ["onSelectionChange"], exportAs: ["ngsOption"] }, { kind: "component", type: ColorSwitcher, selector: "ngs-color-switcher", inputs: ["colors", "selectedColor", "disabled"], outputs: ["colorChange"], exportAs: ["ngsColorSwitcher"] }, { kind: "component", type: ScrollbarArea, selector: "ngs-scrollbar-area", inputs: ["scrollbarWidth", "autoHide", "absolute"], outputs: ["scrolled"], exportAs: ["ngsScrollbarArea"] }, { kind: "directive", type: CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }, { kind: "directive", type: UploadTriggerDirective, selector: "[ngsUploadTrigger]", inputs: ["accept", "multiple"], outputs: ["fileSelected"], exportAs: ["ngsUploadTrigger"] }, { kind: "component", type: ProgressSpinner, selector: "ngs-progress-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["ngsProgressSpinner"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
3954
+ }
3955
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ImageDesigner, decorators: [{
3956
+ type: Component,
3957
+ args: [{ selector: 'ngs-image-designer', exportAs: 'ngsImageDesigner', imports: [
3958
+ Panel,
3959
+ PanelSidebar,
3960
+ PanelContent,
3961
+ PanelHeader,
3962
+ Toolbar,
3963
+ Button,
3964
+ ToolbarSpacer,
3965
+ Divider,
3966
+ Icon,
3967
+ ToolbarTitle,
3968
+ TabPanel,
3969
+ TabPanelAside,
3970
+ TabPanelAsideContentDirective,
3971
+ TabPanelContent,
3972
+ TabPanelItem,
3973
+ TabPanelItemIconDirective,
3974
+ TabPanelItemText,
3975
+ TabPanelNav,
3976
+ List,
3977
+ ListItem,
3978
+ ListItemLine,
3979
+ CdkDropList,
3980
+ CdkDrag,
3981
+ CdkDragPlaceholder,
3982
+ TabGroup,
3983
+ Tab,
3984
+ Accordion,
3985
+ ExpansionPanel,
3986
+ ExpansionPanelHeader,
3987
+ ExpansionPanelTitle,
3988
+ FormField,
3989
+ Label,
3990
+ Input,
3991
+ Select,
3992
+ Option,
3993
+ ColorSwitcher,
3994
+ ScrollbarArea,
3995
+ CdkPortalOutlet,
3996
+ UploadTriggerDirective,
3997
+ ProgressSpinner,
3998
+ FormsModule,
3999
+ ], providers: [
4000
+ ImageDesignerService,
4001
+ {
4002
+ provide: IMAGE_DESIGNER,
4003
+ useExisting: forwardRef(() => ImageDesigner)
4004
+ }
4005
+ ], host: {
4006
+ 'class': 'ngs-image-designer',
4007
+ '(keydown)': 'onKeyDown($event)'
4008
+ }, template: "<ngs-panel class=\"h-full\">\n <ngs-panel-sidebar class=\"border-r border-border h-full\">\n <ngs-tab-panel hideContentIfTabNotSelected [activeItemId]=\"activeItemId()\" class=\"h-full\">\n <ngs-tab-panel-content>\n <ngs-tab-panel-nav>\n <ngs-tab-panel-item for=\"text\" (click)=\"activeItemId.set('text')\">\n <ngs-icon name=\"fluent:text-field-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Text</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"photos\" (click)=\"activeItemId.set('photos')\">\n <ngs-icon name=\"fluent:image-copy-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Photos</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"elements\" (click)=\"activeItemId.set('elements')\">\n <ngs-icon name=\"fluent:shape-subtract-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Elements</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"upload\" (click)=\"activeItemId.set('upload')\">\n <ngs-icon name=\"fluent:arrow-upload-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Upload</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"background\" (click)=\"activeItemId.set('background')\">\n <ngs-icon name=\"fluent:grid-dots-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Background</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"layers\" (click)=\"activeItemId.set('layers')\">\n <ngs-icon name=\"fluent:layer-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Layers</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n <ngs-tab-panel-item for=\"resize\" (click)=\"activeItemId.set('resize')\">\n <ngs-icon name=\"fluent:resize-24-regular\" ngsTabPanelItemIcon/>\n <ngs-tab-panel-item-text>Resize</ngs-tab-panel-item-text>\n </ngs-tab-panel-item>\n </ngs-tab-panel-nav>\n </ngs-tab-panel-content>\n <ngs-tab-panel-aside class=\"border-s border-border\">\n <ng-template ngsTabPanelAsideContent=\"text\">\n <ngs-panel>\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">Text</ngs-panel-header>\n <ngs-panel-content class=\"p-4 flex flex-col gap-4\">\n <div class=\"grid grid-cols-1 gap-4 mt-2\">\n <button (click)=\"addHeader()\"\n draggable=\"true\"\n (dragstart)=\"onTextDragStart($event, 'header')\"\n class=\"w-full text-left p-4 border border-border rounded-lg hover:bg-surface-container transition-colors\">\n <div class=\"text-3xl font-bold pointer-events-none\">Add a heading</div>\n </button>\n <button (click)=\"addSubheader()\"\n draggable=\"true\"\n (dragstart)=\"onTextDragStart($event, 'subheader')\"\n class=\"w-full text-left p-3 border border-border rounded-lg hover:bg-surface-container transition-colors\">\n <div class=\"text-xl font-semibold pointer-events-none\">Add a subheading</div>\n </button>\n <button (click)=\"addBodyText()\"\n draggable=\"true\"\n (dragstart)=\"onTextDragStart($event, 'body')\"\n class=\"w-full text-left p-2 border border-border rounded-lg hover:bg-surface-container transition-colors\">\n <div class=\"text-base pointer-events-none\">Add body text</div>\n </button>\n </div>\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"photos\">\n <ngs-panel class=\"h-full flex flex-col\">\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">\n<!-- <ngs-form-field class=\"w-full\" subscriptHiddenIfEmpty>-->\n<!-- <ngs-icon name=\"fluent:search-24-regular\" ngsIconPrefix/>-->\n<!-- <input type=\"text\"-->\n<!-- ngsInput-->\n<!-- placeholder=\"Search photos\"-->\n<!-- [ngModel]=\"photosFilter()\"-->\n<!-- (ngModelChange)=\"photosFilter.set($event)\"/>-->\n<!-- </ngs-form-field>-->\n Photos\n </ngs-panel-header>\n <ngs-panel-content class=\"relative flex-1 p-0 overflow-hidden\">\n <ngs-scrollbar-area (scrolled)=\"onPhotosScroll($event)\" class=\"h-full\">\n <div class=\"p-4\">\n <div class=\"masonry-grid\">\n @for (photo of photos(); track photo.id) {\n <button (click)=\"addImage(photo)\"\n draggable=\"true\"\n (dragstart)=\"onImageDragStart($event, photo)\"\n [style.aspect-ratio]=\"photo.width + '/' + photo.height\"\n class=\"border border-border rounded-lg overflow-hidden hover:ring-2 hover:ring-primary transition-all bg-surface-container\">\n <img [src]=\"photo.thumbUrl || photo.url\"\n [alt]=\"photo.name\"\n class=\"w-full h-auto block pointer-events-none\"/>\n </button>\n }\n </div>\n @if (isLoadingPhotos()) {\n <div class=\"flex justify-center p-4\">\n <ngs-progress-spinner diameter=\"32\"/>\n </div>\n }\n @if (photos().length === 0 && !isLoadingPhotos()) {\n <div class=\"text-center p-4 text-sm text-neutral-500\">\n No photos found\n </div>\n }\n </div>\n </ngs-scrollbar-area>\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"elements\">\n <ngs-panel>\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">Elements</ngs-panel-header>\n <ngs-panel-content class=\"p-4 overflow-y-auto\">\n <div class=\"grid grid-cols-3 gap-3\">\n @for (element of elements(); track element.name) {\n <button (click)=\"addShape(element)\"\n draggable=\"true\"\n (dragstart)=\"onImageDragStart($event, element)\"\n class=\"aspect-square p-2 border border-border rounded-lg hover:bg-surface-container\n transition-colors flex items-center justify-center\">\n <img [src]=\"element.data\" [alt]=\"element.name\"\n class=\"max-w-full max-h-full object-contain pointer-events-none\"/>\n </button>\n }\n </div>\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"upload\">\n <ngs-panel class=\"h-full flex flex-col\">\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">\n <ngs-toolbar>\n <div>Upload</div>\n <ngs-toolbar-spacer/>\n <button ngsButton=\"filled\"\n ngsUploadTrigger\n [accept]=\"'image/*'\"\n (fileSelected)=\"onFileSelected($event)\"\n [loading]=\"isUploading()\">\n <ngs-icon name=\"fluent:arrow-upload-24-regular\"/>\n Add Image\n </button>\n </ngs-toolbar>\n </ngs-panel-header>\n <ngs-panel-content>\n @if (uploadedImages().length > 0 || assets().length > 0) {\n <ngs-scrollbar-area (scrolled)=\"onAssetsScroll($event)\">\n <div class=\"p-4 flex flex-col gap-4 min-h-full\">\n @if (isUploading() || uploadPreview()) {\n <div class=\"relative aspect-video border border-border rounded-lg overflow-hidden bg-surface-container\">\n @if (uploadPreview()) {\n <img [src]=\"uploadPreview()\" class=\"w-full h-full object-contain\" alt=\"Preview\"/>\n }\n @if (isUploading()) {\n <div class=\"absolute inset-0 bg-black/20 flex items-center justify-center backdrop-blur-[2px]\">\n <div class=\"flex flex-col items-center gap-2\">\n <ngs-progress-spinner diameter=\"32\" color=\"white\"/>\n <span class=\"text-xs font-medium text-white\">Uploading...</span>\n </div>\n </div>\n }\n </div>\n }\n\n @if (uploadedImages().length > 0) {\n <div class=\"flex flex-col gap-2\">\n <div class=\"text-sm font-semibold\">Your Uploads</div>\n <div class=\"masonry-grid\">\n @for (photo of uploadedImages(); track $index) {\n <button (click)=\"addUploadedImage(photo)\"\n draggable=\"true\"\n (dragstart)=\"onImageDragStart($event, photo)\"\n [style.aspect-ratio]=\"photo.width + '/' + photo.height\"\n class=\"border border-border rounded-lg overflow-hidden hover:ring-2\n hover:ring-primary transition-all bg-surface-container\">\n <img [src]=\"photo.url\" class=\"w-full h-auto block pointer-events-none\" alt=\"Uploaded image\"/>\n </button>\n }\n </div>\n </div>\n }\n\n @if (assets().length > 0) {\n <div class=\"flex flex-col gap-2\">\n <div class=\"text-sm font-semibold\">Assets</div>\n <div class=\"masonry-grid\">\n @for (asset of assets(); track asset.id) {\n <button (click)=\"addImage(asset)\"\n draggable=\"true\"\n (dragstart)=\"onImageDragStart($event, asset)\"\n [style.aspect-ratio]=\"asset.width + '/' + asset.height\"\n class=\"border border-border rounded-lg overflow-hidden hover:ring-2\n hover:ring-primary transition-all bg-surface-container\">\n <img [src]=\"asset.thumbUrl || asset.url\"\n [alt]=\"asset.name\"\n class=\"w-full h-auto block pointer-events-none\"/>\n </button>\n }\n </div>\n @if (isLoadingAssets()) {\n <div class=\"flex justify-center p-4\">\n <ngs-progress-spinner diameter=\"32\"/>\n </div>\n }\n </div>\n }\n </div>\n </ngs-scrollbar-area>\n }\n\n @if (uploadedImages().length === 0 && assets().length === 0 && !isLoadingAssets()) {\n <div class=\"h-full flex items-center justify-center\">\n <div class=\"text-sm text-neutral-500\">No images uploaded yet.</div>\n </div>\n }\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"background\">\n <ngs-panel>\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">Background</ngs-panel-header>\n <ngs-panel-content>\n <ngs-tab-group animationDuration=\"0\" class=\"h-full\">\n <ngs-tab label=\"Color\">\n <div class=\"absolute inset-0\">\n <ngs-scrollbar-area>\n <div class=\"p-4\">\n <ngs-accordion [multi]=\"false\">\n <ngs-expansion-panel [expanded]=\"true\">\n <ngs-expansion-panel-header>\n <ngs-expansion-panel-title>Colors</ngs-expansion-panel-title>\n </ngs-expansion-panel-header>\n <div class=\"py-2\">\n <ngs-color-switcher [colors]=\"presetColors()\" (colorChange)=\"setColor($event)\"/>\n </div>\n </ngs-expansion-panel>\n <ngs-expansion-panel>\n <ngs-expansion-panel-header>\n <ngs-expansion-panel-title>Gradients</ngs-expansion-panel-title>\n </ngs-expansion-panel-header>\n <div class=\"grid grid-cols-3 gap-3 py-2\">\n @for (gradient of presetGradients(); track $index) {\n <button (click)=\"setGradient(gradient)\"\n [style.background]=\"gradient.css\"\n class=\"aspect-[16/6.75] w-full rounded-lg border border-border hover:ring-2 hover:ring-primary transition-all\">\n </button>\n }\n </div>\n </ngs-expansion-panel>\n </ngs-accordion>\n </div>\n </ngs-scrollbar-area>\n </div>\n </ngs-tab>\n <ngs-tab label=\"Patterns\">\n <div class=\"absolute inset-0\">\n <ngs-scrollbar-area>\n <div class=\"p-4\">\n <div class=\"grid grid-cols-3 gap-3\">\n @for (pattern of presetPatterns(); track $index) {\n <button (click)=\"addPattern(pattern)\"\n class=\"aspect-square w-full rounded-lg border border-border hover:ring-2 hover:ring-primary transition-all overflow-hidden\">\n <img [src]=\"pattern\" class=\"w-full h-full object-cover\" alt=\"pattern\">\n </button>\n }\n </div>\n </div>\n </ngs-scrollbar-area>\n </div>\n </ngs-tab>\n </ngs-tab-group>\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"layers\">\n <ngs-panel>\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">Layers</ngs-panel-header>\n <ngs-panel-content>\n @if (layersFromService().length > 0) {\n <ngs-list cdkDropList (cdkDropListDropped)=\"drop($event)\">\n @for (layer of layersFromService(); track layer.id) {\n <ngs-list-item cdkDrag\n cdkDragLockAxis=\"y\"\n [cdkDragStartDelay]=\"100\"\n (click)=\"selectLayer(layer.id!)\"\n [class.is-active]=\"selectedLayerId() === layer.id\"\n class=\"relative group bg-surface\">\n <ngs-icon [name]=\"getLayerIcon(layer.type)\" ngsListItemIcon/>\n <div ngsListItemLine>\n {{ layer.name || layer.type }}\n <!-- {{ layer.text || '' }}-->\n </div>\n <div class=\"absolute right-2 top-1/2 -translate-y-1/2 flex items-center\">\n <button ngsIconButton (click)=\"toggleLayerVisibility(layer.id!, $event)\">\n <ngs-icon\n [name]=\"layer.visible === false ? 'fluent:eye-off-24-regular' : 'fluent:eye-24-regular'\"/>\n </button>\n <button ngsIconButton (click)=\"toggleLayerLock(layer.id!, $event)\">\n <ngs-icon\n [name]=\"layer.locked ? 'fluent:lock-closed-24-regular' : 'fluent:lock-open-24-regular'\"\n class=\"size-5\"/>\n </button>\n <button ngsIconButton (click)=\"deleteLayer(layer.id!, $event)\" [disabled]=\"layer.locked\">\n <ngs-icon name=\"fluent:delete-24-regular\" class=\"size-5\"/>\n </button>\n </div>\n\n <div *cdkDragPlaceholder\n class=\"min-h-[var(--ngs-list-item-min-height)] bg-surface-container\"></div>\n </ngs-list-item>\n }\n </ngs-list>\n } @else {\n <div class=\"text-sm p-4 h-full flex items-center justify-center\">No layers available</div>\n }\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n <ng-template ngsTabPanelAsideContent=\"resize\">\n <ngs-panel>\n <ngs-panel-header class=\"border-b border-b-border flex items-center px-4\">Resize</ngs-panel-header>\n <ngs-panel-content>\n <ngs-scrollbar-area>\n <div class=\"p-4 flex flex-col gap-4\">\n <div class=\"flex flex-col gap-4\">\n <ngs-form-field subscriptHiddenIfEmpty>\n <ngs-label>Width (px)</ngs-label>\n <input ngsInput type=\"number\" [(ngModel)]=\"resizeWidth\">\n </ngs-form-field>\n\n <ngs-form-field subscriptHiddenIfEmpty>\n <ngs-label>Height (px)</ngs-label>\n <input ngsInput type=\"number\" [(ngModel)]=\"resizeHeight\">\n </ngs-form-field>\n\n <ngs-form-field subscriptHiddenIfEmpty>\n <ngs-label>Units</ngs-label>\n <ngs-select [(ngModel)]=\"resizeUnit\">\n <ngs-option value=\"px\">px</ngs-option>\n </ngs-select>\n </ngs-form-field>\n\n <button ngsButton=\"filled\" fullWidth (click)=\"applyResize()\">Resize</button>\n </div>\n\n <div class=\"flex flex-col gap-6 mt-2\">\n @for (category of presetCategories(); track category.name) {\n <div class=\"flex flex-col gap-3\">\n <div class=\"flex items-center gap-2 font-semibold text-sm\">\n <ngs-icon [name]=\"category.icon\" class=\"size-5\"/>\n {{ category.name }}\n </div>\n\n <div class=\"grid grid-cols-3 gap-3\">\n @for (preset of category.presets; track preset.name) {\n <button (click)=\"selectPreset(preset)\"\n class=\"flex flex-col items-center gap-1 p-2 rounded-lg border border-border hover:bg-surface-container transition-colors text-center\">\n <ngs-icon [name]=\"preset.icon\" class=\"size-6 mb-1\"/>\n <span class=\"text-xs font-medium truncate w-full\">{{ preset.name }}</span>\n <span class=\"text-[10px] text-muted-foreground whitespace-nowrap\">{{ preset.width }}\u00D7{{ preset.height }} px</span>\n </button>\n }\n </div>\n </div>\n }\n </div>\n </div>\n </ngs-scrollbar-area>\n </ngs-panel-content>\n </ngs-panel>\n </ng-template>\n\n @if (effectsPortal()) {\n <div class=\"absolute inset-0 z-20 bg-surface-container-lowest\">\n <ng-container [cdkPortalOutlet]=\"effectsPortal()\"/>\n </div>\n }\n </ngs-tab-panel-aside>\n </ngs-tab-panel>\n </ngs-panel-sidebar>\n <ngs-panel-content class=\"h-full\">\n <ngs-panel class=\"h-full\">\n <ngs-panel-header>\n <ngs-toolbar class=\"h-full px-3 border-b border-border\">\n <button ngsIconButton (click)=\"toggleAside()\">\n <ngs-icon name=\"fluent:navigation-24-regular\"/>\n </button>\n <ngs-toolbar-title>{{ title() }}</ngs-toolbar-title>\n <ngs-toolbar-spacer/>\n <div class=\"flex items-center gap-2\">\n <button ngsIconButton (click)=\"zoomOut()\">\n <ngs-icon name=\"fluent:zoom-out-24-regular\"/>\n </button>\n <span class=\"text-sm font-medium w-12 text-center\">{{ zoomPercentage() }}%</span>\n <button ngsIconButton (click)=\"zoomIn()\">\n <ngs-icon name=\"fluent:zoom-in-24-regular\"/>\n </button>\n </div>\n <ngs-toolbar-spacer/>\n <div class=\"flex\">\n <button ngsIconButton (click)=\"undo()\" [disabled]=\"!designerService.canUndo()\">\n <ngs-icon name=\"fluent:arrow-hook-up-left-24-regular\"/>\n </button>\n <button ngsIconButton (click)=\"redo()\" [disabled]=\"!designerService.canRedo()\">\n <ngs-icon name=\"fluent:arrow-hook-up-right-24-regular\"/>\n </button>\n </div>\n @if (showDownloadButton()) {\n <ngs-divider vertical/>\n <button ngsButton=\"filled\" (click)=\"download()\" class=\"mr-2\">Download</button>\n }\n </ngs-toolbar>\n </ngs-panel-header>\n <ngs-panel-content class=\"bg-surface-container overflow-hidden h-full relative\" (wheel)=\"onWheel($event)\"\n (mousedown)=\"canvasContainer.focus()\"\n (dragover)=\"onDragOver($event)\"\n (drop)=\"onDrop($event)\">\n @if (settingsPortal()) {\n <ng-container [cdkPortalOutlet]=\"settingsPortal()\"/>\n }\n <div #canvasContainer class=\"w-full h-full outline-none\" tabindex=\"0\" (contextmenu)=\"$event.preventDefault()\"></div>\n </ngs-panel-content>\n </ngs-panel>\n </ngs-panel-content>\n</ngs-panel>\n", styles: [":host{display:block;width:100%;height:100%}:host ngs-tab-panel{--ngs-tab-panel-aside-width: calc(var(--spacing, .25rem) * 90);--ngs-tab-panel-nav-padding: calc(var(--spacing, .25rem) * 3) 0}:host ngs-list{--ngs-list-padding: 0;--ngs-list-item-radius: 0}:host ngs-tab-group{--ngs-tab-label-padding: 16px 16px}:host .masonry-grid{column-count:2;column-gap:12px}:host .masonry-grid>*{break-inside:avoid;margin-bottom:12px;display:block;width:100%}:host button.is-dragging{background:transparent!important;border-color:transparent!important;box-shadow:none!important;transition:none!important;outline:none!important}:host button.is-dragging:hover{background:transparent!important;border-color:transparent!important;box-shadow:none!important}:host button.is-dragging *{color:currentColor!important}\n/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */\n"] }]
4009
+ }], ctorParameters: () => [], propDecorators: { canvasContainer: [{ type: i0.ViewChild, args: ['canvasContainer', { isSignal: true }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], imageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "imageSize", required: false }] }], defaultFont: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultFont", required: false }] }], scale: [{ type: i0.Input, args: [{ isSignal: true, alias: "scale", required: false }] }], minScale: [{ type: i0.Input, args: [{ isSignal: true, alias: "minScale", required: false }] }], maxScale: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxScale", required: false }] }], showGuidelines: [{ type: i0.Input, args: [{ isSignal: true, alias: "showGuidelines", required: false }] }], snapToShapes: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapToShapes", required: false }] }], snapToStageCenter: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapToStageCenter", required: false }] }], snapToStageBorders: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapToStageBorders", required: false }] }], guidelineColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "guidelineColor", required: false }] }], snapRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapRange", required: false }] }], showDownloadButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDownloadButton", required: false }] }], uploadFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadFn", required: false }] }], snapshot: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapshot", required: false }] }], snapshotChanged: [{ type: i0.Output, args: ["snapshotChanged"] }], photosDataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "photosDataSource", required: false }] }], assetsDataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "assetsDataSource", required: false }] }], historyLimit: [{ type: i0.Input, args: [{ isSignal: true, alias: "historyLimit", required: false }] }] } });
4010
+
4011
+ /**
4012
+ * Generated bundle index. Do not edit.
4013
+ */
4014
+
4015
+ export { IMAGE_DESIGNER, ImageDesigner, ImageDesignerService, SVG_ELEMENTS, SVG_PATTERNS, Settings, createDefaultPhotosDataSource };
4016
+ //# sourceMappingURL=ngstarter-ui-components-image-designer.mjs.map