@widgetic/editor 3.11.4 → 4.0.2

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 (497) hide show
  1. package/README.md +245 -0
  2. package/dist/assets/fonts.json +331 -0
  3. package/dist/assets/icons/ic-browser/audio.svg +7 -0
  4. package/dist/assets/icons/ic-browser/document.svg +11 -0
  5. package/dist/assets/icons/ic-browser/error.svg +5 -0
  6. package/dist/assets/icons/ic-browser/folder.svg +28 -0
  7. package/dist/assets/icons/ic-browser/player-pause.svg +1 -0
  8. package/dist/assets/icons/ic-browser/player-play.svg +1 -0
  9. package/dist/assets/icons/ic-browser/trash.svg +1 -0
  10. package/dist/assets/icons/ic-fixed-position/background-deselected.svg +3 -0
  11. package/dist/assets/icons/ic-fixed-position/background-selected.svg +3 -0
  12. package/dist/assets/icons/ic-fixed-position/bottom-left-background-deselected.svg +3 -0
  13. package/dist/assets/icons/ic-fixed-position/bottom-left-background-selected.svg +3 -0
  14. package/dist/assets/icons/ic-fixed-position/bottom-left-icon.svg +4 -0
  15. package/dist/assets/icons/ic-fixed-position/bottom-right-background-deselected.svg +3 -0
  16. package/dist/assets/icons/ic-fixed-position/bottom-right-background-selected.svg +3 -0
  17. package/dist/assets/icons/ic-fixed-position/bottom-right-icon.svg +4 -0
  18. package/dist/assets/icons/ic-fixed-position/center-bottom-icon.svg +5 -0
  19. package/dist/assets/icons/ic-fixed-position/center-center-icon.svg +3 -0
  20. package/dist/assets/icons/ic-fixed-position/center-left-icon.svg +5 -0
  21. package/dist/assets/icons/ic-fixed-position/center-right-icon.svg +5 -0
  22. package/dist/assets/icons/ic-fixed-position/center-top-icon.svg +5 -0
  23. package/dist/assets/icons/ic-fixed-position/top-left-background-deselected.svg +3 -0
  24. package/dist/assets/icons/ic-fixed-position/top-left-background-selected.svg +3 -0
  25. package/dist/assets/icons/ic-fixed-position/top-left-icon.svg +4 -0
  26. package/dist/assets/icons/ic-fixed-position/top-right-background-deselected.svg +3 -0
  27. package/dist/assets/icons/ic-fixed-position/top-right-background-selected.svg +3 -0
  28. package/dist/assets/icons/ic-fixed-position/top-right-icon.svg +4 -0
  29. package/dist/assets/icons/trash.svg +1 -0
  30. package/dist/components/MockWidget.svelte +108 -0
  31. package/dist/components/MockWidget.svelte.d.ts +22 -0
  32. package/dist/components/WidgetPreview.svelte +852 -0
  33. package/dist/components/WidgetPreview.svelte.d.ts +100 -0
  34. package/dist/components/color-select/ColorSelect.svelte +258 -0
  35. package/dist/components/color-select/ColorSelect.svelte.d.ts +42 -0
  36. package/dist/components/color-select/color.d.ts +25 -0
  37. package/dist/components/color-select/color.js +21 -0
  38. package/dist/components/color-select/constants.d.ts +5 -0
  39. package/dist/components/color-select/constants.js +5 -0
  40. package/dist/components/color-select/render.d.ts +5 -0
  41. package/dist/components/color-select/render.js +77 -0
  42. package/dist/components/color-select/shaders.d.ts +3 -0
  43. package/dist/components/color-select/shaders.js +8 -0
  44. package/dist/components/editors/EmbedEditor.svelte +700 -0
  45. package/dist/components/editors/EmbedEditor.svelte.d.ts +50 -0
  46. package/dist/components/editors/PropsEditor.svelte +4009 -0
  47. package/dist/components/editors/PropsEditor.svelte.d.ts +62 -0
  48. package/dist/components/editors/props-editor.css +73 -0
  49. package/dist/components/ic/AngleInputController.svelte +276 -0
  50. package/dist/components/ic/AngleInputController.svelte.d.ts +26 -0
  51. package/dist/components/ic/BrowserInputController.svelte +731 -0
  52. package/dist/components/ic/BrowserInputController.svelte.d.ts +39 -0
  53. package/dist/components/ic/CheckboxInputController.svelte +203 -0
  54. package/dist/components/ic/CheckboxInputController.svelte.d.ts +41 -0
  55. package/dist/components/ic/ColorPickerInputController.svelte +1584 -0
  56. package/dist/components/ic/ColorPickerInputController.svelte.d.ts +50 -0
  57. package/dist/components/ic/DateFieldInputController.svelte +453 -0
  58. package/dist/components/ic/DateFieldInputController.svelte.d.ts +35 -0
  59. package/dist/components/ic/DatePickerInputController.svelte +364 -0
  60. package/dist/components/ic/DatePickerInputController.svelte.d.ts +28 -0
  61. package/dist/components/ic/DropdownInputController.svelte +226 -0
  62. package/dist/components/ic/DropdownInputController.svelte.d.ts +43 -0
  63. package/dist/components/ic/FixedPositionInputController.svelte +353 -0
  64. package/dist/components/ic/FixedPositionInputController.svelte.d.ts +23 -0
  65. package/dist/components/ic/FontInputController.svelte +2098 -0
  66. package/dist/components/ic/FontInputController.svelte.d.ts +47 -0
  67. package/dist/components/ic/InputControllerHeader.svelte +147 -0
  68. package/dist/components/ic/InputControllerHeader.svelte.d.ts +23 -0
  69. package/dist/components/ic/NumberControls.svelte +299 -0
  70. package/dist/components/ic/NumberControls.svelte.d.ts +30 -0
  71. package/dist/components/ic/NumberInputController.svelte +397 -0
  72. package/dist/components/ic/NumberInputController.svelte.d.ts +30 -0
  73. package/dist/components/ic/PositionInputController.svelte +252 -0
  74. package/dist/components/ic/PositionInputController.svelte.d.ts +37 -0
  75. package/dist/components/ic/RadioGroupInputController.svelte +162 -0
  76. package/dist/components/ic/RadioGroupInputController.svelte.d.ts +38 -0
  77. package/dist/components/ic/RangeCalendarInputController.svelte +262 -0
  78. package/dist/components/ic/RangeCalendarInputController.svelte.d.ts +32 -0
  79. package/dist/components/ic/RoundedCornersInputController.svelte +469 -0
  80. package/dist/components/ic/RoundedCornersInputController.svelte.d.ts +37 -0
  81. package/dist/components/ic/SliderInputController.svelte +411 -0
  82. package/dist/components/ic/SliderInputController.svelte.d.ts +46 -0
  83. package/dist/components/ic/SliderRangeInputController.svelte +155 -0
  84. package/dist/components/ic/SliderRangeInputController.svelte.d.ts +33 -0
  85. package/dist/components/ic/SwitchInputController.svelte +107 -0
  86. package/dist/components/ic/SwitchInputController.svelte.d.ts +24 -0
  87. package/dist/components/ic/TagsInputController.svelte +125 -0
  88. package/dist/components/ic/TagsInputController.svelte.d.ts +27 -0
  89. package/dist/components/ic/TextAreaInputController.svelte +1932 -0
  90. package/dist/components/ic/TextAreaInputController.svelte.d.ts +14 -0
  91. package/dist/components/ic/TextAreaPreviewInputController.svelte +478 -0
  92. package/dist/components/ic/TextAreaPreviewInputController.svelte.d.ts +28 -0
  93. package/dist/components/ic/TextInputController.svelte +70 -0
  94. package/dist/components/ic/TextInputController.svelte.d.ts +30 -0
  95. package/dist/components/ic/TimeInputController.svelte +434 -0
  96. package/dist/components/ic/TimeInputController.svelte.d.ts +25 -0
  97. package/dist/components/ic/VectorInputController.svelte +1826 -0
  98. package/dist/components/ic/VectorInputController.svelte.d.ts +47 -0
  99. package/dist/components/ic/index.d.ts +39 -0
  100. package/dist/components/ic/index.js +24 -0
  101. package/dist/components/ic/shared/CategoryTabs.svelte +40 -0
  102. package/dist/components/ic/shared/CategoryTabs.svelte.d.ts +25 -0
  103. package/dist/components/ic/shared/ItemGrid.svelte +181 -0
  104. package/dist/components/ic/shared/ItemGrid.svelte.d.ts +35 -0
  105. package/dist/components/ic/shared/RecentItemsSection.svelte +260 -0
  106. package/dist/components/ic/shared/RecentItemsSection.svelte.d.ts +35 -0
  107. package/dist/components/ic/shared/index.d.ts +8 -0
  108. package/dist/components/ic/shared/index.js +9 -0
  109. package/dist/components/icons/addnew.svelte +21 -0
  110. package/dist/components/icons/addnew.svelte.d.ts +20 -0
  111. package/dist/components/icons/drag.svelte +29 -0
  112. package/dist/components/icons/drag.svelte.d.ts +20 -0
  113. package/dist/components/icons/drag.svg +1 -0
  114. package/dist/components/icons/locked.svelte +4 -0
  115. package/dist/components/icons/locked.svelte.d.ts +26 -0
  116. package/dist/components/icons/locked.svg +4 -0
  117. package/dist/components/icons/ok.svelte +20 -0
  118. package/dist/components/icons/ok.svelte.d.ts +20 -0
  119. package/dist/components/icons/ok.svg +11 -0
  120. package/dist/components/icons/unlocked.svelte +4 -0
  121. package/dist/components/icons/unlocked.svelte.d.ts +26 -0
  122. package/dist/components/icons/unlocked.svg +4 -0
  123. package/dist/components/icons/x.svelte +14 -0
  124. package/dist/components/icons/x.svelte.d.ts +20 -0
  125. package/dist/components/icons/x.svg +5 -0
  126. package/dist/components/ui/arrows/OpacitySliderArrows.svelte +43 -0
  127. package/dist/components/ui/arrows/OpacitySliderArrows.svelte.d.ts +23 -0
  128. package/dist/components/ui/checkerboard-background/CheckerboardBackground.svelte +15 -0
  129. package/dist/components/ui/checkerboard-background/CheckerboardBackground.svelte.d.ts +20 -0
  130. package/dist/config.d.ts +11 -0
  131. package/dist/config.js +31 -0
  132. package/dist/index.d.ts +5 -0
  133. package/dist/index.js +9 -0
  134. package/dist/stores/userSession.d.ts +7 -0
  135. package/dist/stores/userSession.js +28 -0
  136. package/dist/utils/FontManager.d.ts +278 -0
  137. package/dist/utils/FontManager.js +1302 -0
  138. package/dist/utils/MediaManager.d.ts +35 -0
  139. package/dist/utils/MediaManager.js +59 -0
  140. package/dist/utils/VectorManager.d.ts +190 -0
  141. package/dist/utils/VectorManager.js +754 -0
  142. package/dist/utils/colorAutocomplete.d.ts +10 -0
  143. package/dist/utils/colorAutocomplete.js +28 -0
  144. package/dist/utils/colorNames.d.ts +3 -0
  145. package/dist/utils/colorNames.js +152 -0
  146. package/dist/utils/colorTransparency.d.ts +52 -0
  147. package/dist/utils/colorTransparency.js +114 -0
  148. package/dist/utils/fontUtils.d.ts +34 -0
  149. package/dist/utils/fontUtils.js +132 -0
  150. package/dist/utils/passiveEvents.js +20 -0
  151. package/dist/utils/recentColors.d.ts +10 -0
  152. package/dist/utils/recentColors.js +63 -0
  153. package/dist/utils/skipFonts.d.ts +3 -0
  154. package/dist/utils/skipFonts.js +49 -0
  155. package/dist/utils/vectorUtils.d.ts +34 -0
  156. package/dist/utils/vectorUtils.js +71 -0
  157. package/dist/utils.d.ts +11 -0
  158. package/dist/utils.js +38 -0
  159. package/package.json +134 -74
  160. package/.babelrc +0 -10
  161. package/.bowerrc +0 -4
  162. package/.nvmrc +0 -1
  163. package/Gruntfile.coffee +0 -69
  164. package/bower.json +0 -28
  165. package/css/browser/entries/activateable.styl +0 -29
  166. package/css/browser/entries/audio.styl +0 -39
  167. package/css/browser/entries/folder.styl +0 -79
  168. package/css/browser/entries/image.styl +0 -26
  169. package/css/browser/entries/index.styl +0 -66
  170. package/css/browser/entries/rss.styl +0 -11
  171. package/css/browser/entries/video.styl +0 -11
  172. package/css/browser/index.styl +0 -86
  173. package/css/browser/intent/index.styl +0 -61
  174. package/css/browser/nav/index.styl +0 -73
  175. package/css/browser/social-browser.styl +0 -23
  176. package/css/controls/clock.styl +0 -110
  177. package/css/controls/config.styl +0 -6
  178. package/css/controls/control-audio-details.styl +0 -63
  179. package/css/controls/control-audio.styl +0 -28
  180. package/css/controls/control-browser.styl +0 -48
  181. package/css/controls/control-color.styl +0 -423
  182. package/css/controls/control-date_time.styl +0 -191
  183. package/css/controls/control-dropdown.styl +0 -79
  184. package/css/controls/control-font.styl +0 -147
  185. package/css/controls/control-image-details.styl +0 -35
  186. package/css/controls/control-image.styl +0 -43
  187. package/css/controls/control-order-picker.styl +0 -29
  188. package/css/controls/control-position.styl +0 -390
  189. package/css/controls/control-range.styl +0 -68
  190. package/css/controls/control-scale.styl +0 -30
  191. package/css/controls/control-slider.styl +0 -38
  192. package/css/controls/control-spinner.styl +0 -39
  193. package/css/controls/control-textarea.styl +0 -52
  194. package/css/controls/control-toggle.styl +0 -34
  195. package/css/controls/control-url.styl +0 -32
  196. package/css/controls/control-video-details.styl +0 -85
  197. package/css/controls/control-video.styl +0 -43
  198. package/css/controls/general.styl +0 -98
  199. package/css/controls/index.styl +0 -25
  200. package/css/editor/colors.styl +0 -7
  201. package/css/editor/content/directinput.styl +0 -47
  202. package/css/editor/content/index.styl +0 -111
  203. package/css/editor/content/message.styl +0 -53
  204. package/css/editor/content/tabs.styl +0 -75
  205. package/css/editor/copied-from-old-style.styl +0 -197
  206. package/css/editor/details/bulk-editor.styl +0 -104
  207. package/css/editor/details/index.styl +0 -21
  208. package/css/editor/details/item.styl +0 -153
  209. package/css/editor/embed.styl +0 -106
  210. package/css/editor/footer.styl +0 -180
  211. package/css/editor/full-editor.styl +0 -16
  212. package/css/editor/index.styl +0 -124
  213. package/css/editor/login-popup.styl +0 -13
  214. package/css/editor/preview.styl +0 -257
  215. package/css/editor/skin/editor/index.styl +0 -54
  216. package/css/editor/skin/editor/tabs.styl +0 -111
  217. package/css/editor/skin/index.styl +0 -138
  218. package/css/editor/skin/popup.styl +0 -152
  219. package/css/editor/skin/presets/dropdown.styl +0 -61
  220. package/css/editor/skin/presets/index.styl +0 -86
  221. package/css/editor/skin/presets/list.styl +0 -244
  222. package/css/editor/tooltips.styl +0 -85
  223. package/css/fonts.styl +0 -47
  224. package/css/icons/BlogvioEditor.dev.svg +0 -112
  225. package/css/icons.styl +0 -16
  226. package/css/img/arrow-right-rounded-v-small.png +0 -0
  227. package/css/img/browser-folder-separator.png +0 -0
  228. package/css/img/calendar-input-icons.png +0 -0
  229. package/css/img/check.png +0 -0
  230. package/css/img/close.png +0 -0
  231. package/css/img/composition/edit-icon.png +0 -0
  232. package/css/img/content/audio-icon.png +0 -0
  233. package/css/img/content/folder-icon.png +0 -0
  234. package/css/img/content/messages/error-background.png +0 -0
  235. package/css/img/content/messages/error-icon.png +0 -0
  236. package/css/img/content/messages/notification-background.png +0 -0
  237. package/css/img/content/messages/notification-icon.png +0 -0
  238. package/css/img/content/no-files-audio.png +0 -0
  239. package/css/img/content/no-files-image.png +0 -0
  240. package/css/img/content/no-files-video.png +0 -0
  241. package/css/img/content/no-files.png +0 -0
  242. package/css/img/content/rss-icon.png +0 -0
  243. package/css/img/content/selected.png +0 -0
  244. package/css/img/content/video-icon.png +0 -0
  245. package/css/img/content/with-control-text.png +0 -0
  246. package/css/img/control-dropdown-arrow.png +0 -0
  247. package/css/img/control-image-valid.png +0 -0
  248. package/css/img/control-position-icon.png +0 -0
  249. package/css/img/control-scale-lock-icon.png +0 -0
  250. package/css/img/control-slider-arrows-hover.png +0 -0
  251. package/css/img/control-slider-arrows.png +0 -0
  252. package/css/img/control-spinner-buttons-hover.png +0 -0
  253. package/css/img/control-spinner-buttons.png +0 -0
  254. package/css/img/control-textarea-bold-active.png +0 -0
  255. package/css/img/control-textarea-bold.png +0 -0
  256. package/css/img/control-textarea-italic-active.png +0 -0
  257. package/css/img/control-textarea-italic.png +0 -0
  258. package/css/img/control-textarea-underline-active.png +0 -0
  259. package/css/img/control-textarea-underline.png +0 -0
  260. package/css/img/custom-checkbox-sprite.png +0 -0
  261. package/css/img/details/item-collapse-icon.png +0 -0
  262. package/css/img/details/item-expand-icon.png +0 -0
  263. package/css/img/details/item-sort-icon.png +0 -0
  264. package/css/img/done-arrow.png +0 -0
  265. package/css/img/fb-logo-icon-w.png +0 -0
  266. package/css/img/grid-icons.png +0 -0
  267. package/css/img/icon-reference.png +0 -0
  268. package/css/img/indicator.png +0 -0
  269. package/css/img/loader.gif +0 -0
  270. package/css/img/loaders/audio-white.png +0 -0
  271. package/css/img/loaders/audio.png +0 -0
  272. package/css/img/loaders/photo-white.png +0 -0
  273. package/css/img/loaders/photo.png +0 -0
  274. package/css/img/loaders/util-white.png +0 -0
  275. package/css/img/loaders/util.png +0 -0
  276. package/css/img/loaders/video-white.png +0 -0
  277. package/css/img/loaders/video.png +0 -0
  278. package/css/img/maximize.png +0 -0
  279. package/css/img/minimize.png +0 -0
  280. package/css/img/next-btn.png +0 -0
  281. package/css/img/search-icon-sprite.png +0 -0
  282. package/css/img/services/blogvio.png +0 -0
  283. package/css/img/services/drive.png +0 -0
  284. package/css/img/services/dropbox-login.png +0 -0
  285. package/css/img/services/dropbox.png +0 -0
  286. package/css/img/services/facebook-login.png +0 -0
  287. package/css/img/services/facebook.png +0 -0
  288. package/css/img/services/flickr-login.png +0 -0
  289. package/css/img/services/flickr.png +0 -0
  290. package/css/img/services/instagram-login.png +0 -0
  291. package/css/img/services/instagram.png +0 -0
  292. package/css/img/services/text.png +0 -0
  293. package/css/img/services/uploads.png +0 -0
  294. package/css/img/services/uploads.svg +0 -689
  295. package/css/img/services/webaddress-bw.png +0 -0
  296. package/css/img/services/webaddress.png +0 -0
  297. package/css/img/services/webadress.png +0 -0
  298. package/css/img/services/websearch.png +0 -0
  299. package/css/img/services/wix.png +0 -0
  300. package/css/img/skin/item-active-indicator.png +0 -0
  301. package/css/img/skin/item-hover-icon.png +0 -0
  302. package/css/img/skin/overwrite.png +0 -0
  303. package/css/img/skin/popup-name-icon.png +0 -0
  304. package/css/img/skin/popup-save.png +0 -0
  305. package/css/img/skin/preset.png +0 -0
  306. package/css/img/skin/presets-arrow-purple.png +0 -0
  307. package/css/img/tw-icon-w.png +0 -0
  308. package/css/img/warning.png +0 -0
  309. package/css/img/white-rounded-check.png +0 -0
  310. package/css/index.styl +0 -9
  311. package/css/mixins.styl +0 -62
  312. package/css/scrollable.styl +0 -101
  313. package/karma.conf.js +0 -73
  314. package/lib/control.js +0 -1
  315. package/lib/controls.js +0 -1
  316. package/lib/core.editor.js +0 -4
  317. package/lib/core.js +0 -1
  318. package/lib/core.vendor.js +0 -3
  319. package/lib/dev/control.js +0 -845
  320. package/lib/dev/controls.js +0 -7774
  321. package/lib/dev/core.editor.js +0 -6439
  322. package/lib/dev/core.js +0 -1486
  323. package/lib/dev/core.vendor.js +0 -10708
  324. package/lib/dev/editor.css +0 -166
  325. package/lib/dev/editor.js +0 -29710
  326. package/lib/dev/editor.vendor.js +0 -15119
  327. package/lib/dev/jquery.nanoscroller.js.map +0 -10
  328. package/lib/editor.css +0 -167
  329. package/lib/editor.js +0 -49
  330. package/lib/editor.vendor.js +0 -11
  331. package/src/api/index.coffee +0 -23
  332. package/src/api/mixin.coffee +0 -89
  333. package/src/browser/controller/entry/audio.coffee +0 -38
  334. package/src/browser/controller/entry/button.coffee +0 -16
  335. package/src/browser/controller/entry/file.coffee +0 -35
  336. package/src/browser/controller/entry/folder.coffee +0 -14
  337. package/src/browser/controller/entry/image.coffee +0 -24
  338. package/src/browser/controller/entry/index.coffee +0 -18
  339. package/src/browser/controller/entry/rss.coffee +0 -5
  340. package/src/browser/controller/entry/video.coffee +0 -4
  341. package/src/browser/controller/index.coffee +0 -289
  342. package/src/browser/controller/instagram.coffee +0 -129
  343. package/src/browser/controller/navbar.coffee +0 -48
  344. package/src/browser/controller/social.coffee +0 -213
  345. package/src/browser/model/entry.coffee +0 -18
  346. package/src/browser/model/index.coffee +0 -34
  347. package/src/core/controller/font-loader.coffee +0 -65
  348. package/src/core/controller/lazyload.coffee +0 -14
  349. package/src/core/controller/overlay.coffee +0 -146
  350. package/src/core/controller/popup.coffee +0 -36
  351. package/src/core/controller/scrollable.coffee +0 -68
  352. package/src/core/controls/audio.coffee +0 -56
  353. package/src/core/controls/browser.coffee +0 -271
  354. package/src/core/controls/color.coffee +0 -121
  355. package/src/core/controls/date-time/calendar/index.coffee +0 -99
  356. package/src/core/controls/date-time/calendar/months.coffee +0 -52
  357. package/src/core/controls/date-time/calendar/years.coffee +0 -58
  358. package/src/core/controls/date-time/clock.coffee +0 -62
  359. package/src/core/controls/date-time/helper.coffee +0 -31
  360. package/src/core/controls/date-time/index.coffee +0 -62
  361. package/src/core/controls/date-time/popup.coffee +0 -56
  362. package/src/core/controls/details/audio.coffee +0 -33
  363. package/src/core/controls/details/image.coffee +0 -19
  364. package/src/core/controls/details/video.coffee +0 -14
  365. package/src/core/controls/dropdown.coffee +0 -100
  366. package/src/core/controls/font/font-item.coffee +0 -22
  367. package/src/core/controls/font/font-list.coffee +0 -108
  368. package/src/core/controls/font/fonts-popup.coffee +0 -91
  369. package/src/core/controls/font/index.coffee +0 -66
  370. package/src/core/controls/image.coffee +0 -37
  371. package/src/core/controls/index.coffee +0 -88
  372. package/src/core/controls/orderPicker.coffee +0 -93
  373. package/src/core/controls/position/grid-panel.coffee +0 -75
  374. package/src/core/controls/position/index.coffee +0 -98
  375. package/src/core/controls/position/panel.coffee +0 -68
  376. package/src/core/controls/position/pixel-panel.coffee +0 -54
  377. package/src/core/controls/position/popup.coffee +0 -63
  378. package/src/core/controls/position/reference-panel.coffee +0 -26
  379. package/src/core/controls/range.coffee +0 -191
  380. package/src/core/controls/rss.coffee +0 -16
  381. package/src/core/controls/scale.coffee +0 -78
  382. package/src/core/controls/slider.coffee +0 -122
  383. package/src/core/controls/stepper.coffee +0 -97
  384. package/src/core/controls/text.coffee +0 -54
  385. package/src/core/controls/textarea.coffee +0 -78
  386. package/src/core/controls/toggle.coffee +0 -29
  387. package/src/core/controls/url.coffee +0 -88
  388. package/src/core/controls/video.coffee +0 -9
  389. package/src/core/embed/api.js +0 -178
  390. package/src/core/embed/controller.js +0 -331
  391. package/src/core/models/composition.coffee +0 -178
  392. package/src/core/models/content.coffee +0 -79
  393. package/src/core/models/skin.coffee +0 -91
  394. package/src/core/models/user.coffee +0 -6
  395. package/src/core/models/widget.coffee +0 -88
  396. package/src/core/router/index.coffee +0 -44
  397. package/src/core/services/container/index.js +0 -18
  398. package/src/core/services/container/instance.js +0 -3
  399. package/src/core/services/content.js +0 -80
  400. package/src/core/services/instagram.coffee +0 -47
  401. package/src/core/services/mixin/events.js +0 -17
  402. package/src/core/services/service.js +0 -1
  403. package/src/core/services/sounds.js +0 -81
  404. package/src/core/services/uploads.js +0 -299
  405. package/src/core/utils/popup.coffee +0 -67
  406. package/src/core/utils/should-show.coffee +0 -12
  407. package/src/core/views/audio-details.pug +0 -9
  408. package/src/core/views/audio.pug +0 -7
  409. package/src/core/views/browser.pug +0 -4
  410. package/src/core/views/color.pug +0 -8
  411. package/src/core/views/date-time/calendar.pug +0 -3
  412. package/src/core/views/date-time/clock.pug +0 -12
  413. package/src/core/views/date-time/grid.pug +0 -6
  414. package/src/core/views/date-time/input.pug +0 -4
  415. package/src/core/views/date-time/table.pug +0 -6
  416. package/src/core/views/dropdown.pug +0 -9
  417. package/src/core/views/font/font-item.pug +0 -3
  418. package/src/core/views/font/fonts-list.pug +0 -7
  419. package/src/core/views/font/fonts-popup.pug +0 -5
  420. package/src/core/views/font/index.pug +0 -7
  421. package/src/core/views/image-details.pug +0 -6
  422. package/src/core/views/image.pug +0 -5
  423. package/src/core/views/input-layout.pug +0 -15
  424. package/src/core/views/order-items.pug +0 -7
  425. package/src/core/views/order-picker.pug +0 -7
  426. package/src/core/views/pixelpanel.pug +0 -69
  427. package/src/core/views/position.pug +0 -10
  428. package/src/core/views/range.pug +0 -9
  429. package/src/core/views/scale.pug +0 -7
  430. package/src/core/views/slider.pug +0 -6
  431. package/src/core/views/stepper.pug +0 -6
  432. package/src/core/views/text.pug +0 -4
  433. package/src/core/views/textarea.pug +0 -8
  434. package/src/core/views/toggle.pug +0 -5
  435. package/src/core/views/video-details.pug +0 -7
  436. package/src/core/views/video.pug +0 -4
  437. package/src/core/widget/index.coffee +0 -66
  438. package/src/editor/content/browser/dropzone.html +0 -268
  439. package/src/editor/content/browser/file.html +0 -124
  440. package/src/editor/content/browser/scroll-pane.html +0 -32
  441. package/src/editor/content/browser/service.html +0 -53
  442. package/src/editor/content/browser/upload.coffee +0 -44
  443. package/src/editor/content/browser/upload.html +0 -439
  444. package/src/editor/content/index.coffee +0 -294
  445. package/src/editor/content/input/field.coffee +0 -45
  446. package/src/editor/content/input/index.coffee +0 -115
  447. package/src/editor/content/service/base/photoitem.coffee +0 -49
  448. package/src/editor/content/service/cache.coffee +0 -20
  449. package/src/editor/content/service/default.coffee +0 -59
  450. package/src/editor/content/service/dropbox.coffee +0 -71
  451. package/src/editor/content/service/facebook.coffee +0 -143
  452. package/src/editor/content/service/flickr.coffee +0 -85
  453. package/src/editor/content/service/google-drive-sdk.coffee +0 -18
  454. package/src/editor/content/service/google-drive.coffee +0 -144
  455. package/src/editor/content/service/instagram-social.coffee +0 -84
  456. package/src/editor/content/service/instagram.coffee +0 -168
  457. package/src/editor/content/service/share-popup.coffee +0 -15
  458. package/src/editor/content/service/soundcloud.coffee +0 -163
  459. package/src/editor/content/tab.coffee +0 -27
  460. package/src/editor/details/bulk-editor/attributes-panel.coffee +0 -11
  461. package/src/editor/details/bulk-editor/index.coffee +0 -86
  462. package/src/editor/details/index.coffee +0 -156
  463. package/src/editor/details/item/attributes.coffee +0 -45
  464. package/src/editor/details/item/index.coffee +0 -82
  465. package/src/editor/editor.coffee +0 -274
  466. package/src/editor/footer/breadcrumb.coffee +0 -17
  467. package/src/editor/footer/index.coffee +0 -36
  468. package/src/editor/index.coffee +0 -79
  469. package/src/editor/listeners/embed-listener.coffee +0 -143
  470. package/src/editor/login-popup.coffee +0 -29
  471. package/src/editor/notification-controller.coffee +0 -35
  472. package/src/editor/popup.coffee +0 -68
  473. package/src/editor/remote-preview.coffee +0 -68
  474. package/src/editor/save/index.coffee +0 -62
  475. package/src/editor/skin/delete.coffee +0 -33
  476. package/src/editor/skin/editor/index.coffee +0 -188
  477. package/src/editor/skin/editor/panel.coffee +0 -37
  478. package/src/editor/skin/index.coffee +0 -126
  479. package/src/editor/skin/save.coffee +0 -58
  480. package/src/editor/skin/selector/index.coffee +0 -59
  481. package/src/editor/skin/selector/item.coffee +0 -123
  482. package/src/editor/skin/selector/list.coffee +0 -97
  483. package/src/editor/views/embed.pug +0 -10
  484. package/src/editor/views/loginbutton.pug +0 -8
  485. package/src/editor/views/popup.pug +0 -17
  486. package/src/editor/views/presets.pug +0 -8
  487. package/src/editor/views/presetslist-header.pug +0 -8
  488. package/src/editor/views/skinitem.pug +0 -13
  489. package/src/modules/control.js +0 -3
  490. package/src/modules/controls.js +0 -29
  491. package/src/modules/core.js +0 -19
  492. package/src/modules/editor.js +0 -80
  493. package/test/config.js +0 -3
  494. package/test/core/models/skin-spec.js +0 -209
  495. package/webpack.config.js +0 -46
  496. package/webpack.config.prod.js +0 -9
  497. /package/{src/core/views/date-time/popup.pug → dist/utils/passiveEvents.d.ts} +0 -0
@@ -0,0 +1,2098 @@
1
+ <!--
2
+ * FontInputController Component
3
+ * A dual-input controller for font family and weight selection with Google Fonts integration.
4
+ *
5
+ * Features:
6
+ * - Font family selection with preview
7
+ * - Font weight selection based on available variants
8
+ * - Real-time search with highlighting
9
+ * - Category filtering (Serif, Sans Serif, Script, Other)
10
+ * - Recently used fonts tracking (up to 3 fonts)
11
+ * - Performance-optimized font loading
12
+ * - Custom font support via file browser
13
+ *
14
+ * Performance optimizations:
15
+ * - Lazy loads fonts only when visible in viewport
16
+ * - Delays font loading until scrolling stops (1 second delay)
17
+ * - Preloads default font on mount
18
+ * - Uses font-display: swap for better loading UX
19
+ -->
20
+
21
+ <script lang="ts">
22
+ // Svelte imports
23
+ import { onMount, onDestroy, tick } from 'svelte';
24
+ const browser = typeof window !== 'undefined';
25
+
26
+ // Shadcn Compound Components
27
+ import { Popover, Command, EmptyState } from '@widgetic/design-system/components';
28
+
29
+ // Tabler Icons
30
+ import { Check, IconCloudUpload, IconCaretDownFilled, IconArrowDown, IconArrowUp } from '@widgetic/design-system/icons';
31
+
32
+ // Asset imports
33
+ import icTrash from '../../assets/icons/trash.svg?url';
34
+ import fontsJsonUrl from '../../assets/fonts.json?url';
35
+
36
+ // Config imports
37
+ import { config as appConfig } from '../../config';
38
+ import {
39
+ fontUtils,
40
+ fontCategories,
41
+ type FontItem,
42
+ type RecentFont,
43
+ recentFontsStore
44
+ } from '../../utils/fontUtils';
45
+ import type { FontData } from '../../utils/FontManager';
46
+ import FontManager, { mapGoogleVariantIdsToWeightNames } from '../../utils/FontManager';
47
+
48
+ import { shouldSkipFont } from '../../utils/skipFonts';
49
+
50
+ import { InputControllerHeader } from './index';
51
+
52
+ // Import shared components
53
+ import { SearchInput } from '@widgetic/design-system/components';
54
+ import CategoryTabs from './shared/CategoryTabs.svelte';
55
+ import { RecentItemsSection, ItemGrid } from './shared';
56
+
57
+
58
+ const FONT_FILTER_KEY = (id: string) => `font-specific-filter-state-${id}`;
59
+ const FONT_WEIGHT_KEY = (id: string) => `font-weight-state-${id}`;
60
+ const MAX_RECENT_FONTS = 3;
61
+
62
+ // Then continue with the rest of the declarations
63
+ type FilterType = typeof fontCategories.types[number];
64
+ let currentFilter: FilterType = 'All'; // Default to 'All'
65
+ let lastActiveFilter: FilterType = 'All'; // Add this declaration
66
+ let filterOptions: FilterType[] = [...fontCategories.types];
67
+
68
+ /**
69
+ * Component Props
70
+ * @prop {string} label - Display label for the font selector
71
+ * @prop {string} value - Currently selected font family
72
+ * @prop {Function} onChange - Callback when font selection changes
73
+ * @prop {InputControllerConfig} config - Configuration options
74
+ * @prop {string} description - Optional help text
75
+ * @prop {string} controllerId - Unique identifier for this controller
76
+ * @prop {any} openDropdowns - Store tracking open dropdown states
77
+ * @prop {Function} toggleDropdown - Function to toggle dropdown state
78
+ * @prop {Function} onFocus - Optional focus handler
79
+ */
80
+ export let label: string;
81
+ export let value: { family: string; weight: string; category: string } | string | null = null;
82
+ export let onChange: ((newValue: { family: string; weight: string; category: string }) => void) | undefined = undefined;
83
+ export let description: string | undefined = undefined;
84
+ export let controllerId: string;
85
+ export let openDropdowns: any;
86
+ export let toggleDropdown: (id: string) => void;
87
+
88
+ // Initialize timeouts
89
+ let scrollTimeout: ReturnType<typeof setTimeout> | null = null;
90
+
91
+
92
+ // Font weight state - remove localStorage check on init
93
+ let selectedWeight = `${controllerId}-Regular`;
94
+ let availableWeights: string[] = ['Regular', 'Medium', 'SemiBold', 'Bold'];
95
+
96
+ // Font search and filter state
97
+ let allFonts: FontItem[] = [];
98
+ let recentFonts: FontItem[] = [];
99
+ let filteredFonts: FontData[] = [];
100
+ let searchTerm = '';
101
+ let initialFontNumber = FontManager.initialFontNumber;
102
+ const SEARCH_BATCH_SIZE = FontManager.BATCH_SIZE;
103
+
104
+ // Font loading optimization
105
+ let fontListContainer: HTMLElement;
106
+ let fontObserver = new IntersectionObserver(() => {});
107
+
108
+ // Initialize FontManager instance
109
+ let fontManagerInstance: FontManager;
110
+ let buttonWidth = 0;
111
+
112
+ // Add these variables for search
113
+ let activeSearchIndex = -1;
114
+
115
+ // Add loading state
116
+ let isLoadingBatch = false;
117
+ let isInitialLoading = true; // Start with loading state true
118
+
119
+ // Add new prop for controlling font list behavior
120
+ export let hideRecentFromList: boolean = false;
121
+ export let hideTrashIcon: boolean = true;
122
+
123
+ function coerceFontValue(raw: typeof value): { family: string; weight: string; category: string } | null {
124
+ if (!raw) return null;
125
+ if (typeof raw === 'object' && raw.family) {
126
+ return {
127
+ family: raw.family,
128
+ weight: raw.weight || 'Regular',
129
+ category: raw.category || 'Sans Serif',
130
+ };
131
+ }
132
+ if (typeof raw === 'string' && raw.trim()) {
133
+ const family = raw.split(',')[0].replace(/['"]/g, '').trim();
134
+ if (!family) return null;
135
+ return { family, weight: 'Regular', category: 'Sans Serif' };
136
+ }
137
+ return null;
138
+ }
139
+
140
+ $: resolvedFont = coerceFontValue(value);
141
+
142
+ // Add scroll state tracking
143
+ let isScrolled = false;
144
+
145
+ // Add a timeout to track long-running initial loads
146
+ let initialLoadTimeout: ReturnType<typeof setTimeout> | null = null;
147
+
148
+ // Function to update scroll state
149
+ function updateScrollState() {
150
+ if (fontListContainer) {
151
+ const wasScrolled = isScrolled;
152
+ isScrolled = fontListContainer.scrollTop > 10;
153
+
154
+ // If we're transitioning from scrolled to not scrolled, remove transition
155
+ if (wasScrolled && !isScrolled) {
156
+ const stickyHeader = document.querySelector(`#font-header-${controllerId}`);
157
+ if (stickyHeader) {
158
+ stickyHeader.classList.remove('transition-border');
159
+ // Force a reflow to ensure the transition class removal takes effect
160
+ void (stickyHeader as HTMLElement).offsetWidth;
161
+ }
162
+ } else if (!wasScrolled && isScrolled) {
163
+ // Add transition class when starting to scroll
164
+ const stickyHeader = document.querySelector(`#font-header-${controllerId}`);
165
+ if (stickyHeader) {
166
+ stickyHeader.classList.add('transition-border');
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ // Subscribe to store changes
173
+ $: {
174
+ recentFonts = $recentFontsStore
175
+ .filter(font => !shouldSkipFont(font.family))
176
+ .map(font => ({
177
+ family: font.family,
178
+ category: normalizeCategory(font.category) as FontItem['category'],
179
+ isCustom: font.isCustom
180
+ }));
181
+
182
+ // Update filtered fonts when recent fonts change
183
+ if (hideRecentFromList) {
184
+ updateFilteredFonts();
185
+ }
186
+ }
187
+
188
+ // Add debounce function at the top of the script section
189
+ function debounce<T extends (...args: any[]) => Promise<void>>(
190
+ func: T,
191
+ wait: number
192
+ ): (...args: Parameters<T>) => void {
193
+ let timeout: ReturnType<typeof setTimeout> | null = null;
194
+ return function(this: any, ...args: Parameters<T>): void {
195
+ const context = this;
196
+ if (timeout) clearTimeout(timeout);
197
+ timeout = setTimeout(() => {
198
+ timeout = null;
199
+ func.apply(context, args);
200
+ }, wait);
201
+ };
202
+ }
203
+
204
+ // Add a variable to store the debounced search function
205
+ let debouncedSearch: (event: Event) => void;
206
+
207
+ onMount(async () => {
208
+ try {
209
+ if (browser) {
210
+ const coerced = coerceFontValue(value);
211
+ if (coerced?.weight) {
212
+ selectedWeight = `${controllerId}-${coerced.weight}`;
213
+ }
214
+ if (typeof value === 'string' && coerced && onChange) {
215
+ onChange(coerced);
216
+ }
217
+
218
+ // Load saved filter
219
+ const savedFilter = localStorage.getItem(FONT_FILTER_KEY(controllerId));
220
+ if (savedFilter) {
221
+ currentFilter = savedFilter as FilterType;
222
+ }
223
+
224
+ // Load saved weight
225
+ const savedWeight = localStorage.getItem(FONT_WEIGHT_KEY(controllerId));
226
+ if (savedWeight) {
227
+ selectedWeight = savedWeight;
228
+ }
229
+ }
230
+
231
+ // console.log('🚀 [FontInput] Initializing controller:', {
232
+ // controllerId,
233
+ // initialFilter: currentFilter
234
+ // });
235
+
236
+ // Initialize the debounced search function early
237
+ debouncedSearch = debounce(async (event: Event) => {
238
+ await handleSearch(event);
239
+ }, 300); // 300ms debounce time
240
+
241
+ // Start font manager initialization in parallel
242
+ const fontManagerPromise = FontManager.getInstance(appConfig.GOOGLE_FONTS_API_KEY);
243
+
244
+ // Load and verify recent fonts while waiting for font manager
245
+ const loadedRecentFonts = fontUtils.getRecentFonts();
246
+ // console.log('📝 [FontInput] Loading recent fonts:', loadedRecentFonts);
247
+
248
+ // Filter and validate recent fonts
249
+ const validRecentFonts = loadedRecentFonts
250
+ .filter(font => !shouldSkipFont(font.family))
251
+ .slice(0, MAX_RECENT_FONTS);
252
+
253
+ // Update store with validated fonts
254
+ recentFontsStore.set(validRecentFonts);
255
+
256
+ // Update local state
257
+ recentFonts = validRecentFonts.map(font => ({
258
+ family: font.family,
259
+ category: normalizeCategory(font.category) as FontItem['category'],
260
+ isCustom: font.isCustom
261
+ }));
262
+
263
+ // console.log('✅ [FontInput] Recent fonts initialized:', {
264
+ // loaded: loadedRecentFonts.length,
265
+ // valid: validRecentFonts.length,
266
+ // fonts: validRecentFonts.map(f => f.family)
267
+ // });
268
+
269
+ // Now wait for font manager to be ready
270
+ fontManagerInstance = await fontManagerPromise;
271
+
272
+ // Try to get preloaded fonts immediately
273
+ let preloadedFonts: FontData[] = [];
274
+
275
+ // Check if we can get preloaded fonts for the current category
276
+ if (currentFilter !== 'All' && currentFilter !== 'Mine') {
277
+ // console.log(`🔍 [FontInput:${controllerId}] Getting preloaded fonts for category:`, currentFilter);
278
+ preloadedFonts = fontManagerInstance.getPreloadedFonts(currentFilter as FontItem['category']);
279
+
280
+ if (preloadedFonts.length > 0) {
281
+ // console.log(`✅ [FontInput:${controllerId}] Using preloaded fonts for initial display:`, {
282
+ // category: currentFilter,
283
+ // count: preloadedFonts.length,
284
+ // fonts: preloadedFonts.slice(0, 3).map(f => f.family)
285
+ // });
286
+
287
+ // Filter out fonts that should be skipped
288
+ const filteredPreloadedFonts = preloadedFonts.filter(font => !shouldSkipFont(font.family));
289
+
290
+ // Update allFonts with preloaded fonts
291
+ allFonts = filteredPreloadedFonts;
292
+
293
+ // Update filtered fonts immediately
294
+ updateFilteredFonts();
295
+
296
+ // Only set loading to false if we have fonts to display
297
+ if (filteredFonts.length > 0) {
298
+ isInitialLoading = false;
299
+ // console.log(`✅ [FontInput:${controllerId}] Initial loading complete with preloaded fonts:`, {
300
+ // filteredFonts: filteredFonts.length,
301
+ // category: currentFilter
302
+ // });
303
+
304
+ // Clear the timeout since we've loaded successfully
305
+ if (initialLoadTimeout) {
306
+ clearTimeout(initialLoadTimeout);
307
+ initialLoadTimeout = null;
308
+ }
309
+ }
310
+ } else {
311
+ // console.log(`⚠️ [FontInput:${controllerId}] No preloaded fonts available for category:`, currentFilter);
312
+ }
313
+ } else if (currentFilter === 'All') {
314
+ // console.log(`🔍 [FontInput:${controllerId}] Getting initial batch for All category`);
315
+ // For All category, get the first batch
316
+ preloadedFonts = fontManagerInstance.getNextBatch(controllerId, 'All');
317
+
318
+ if (preloadedFonts.length > 0) {
319
+ // console.log(`✅ [FontInput:${controllerId}] Using initial batch for All category:`, {
320
+ // count: preloadedFonts.length,
321
+ // fonts: preloadedFonts.slice(0, 3).map(f => f.family)
322
+ // });
323
+
324
+ // Filter out fonts that should be skipped
325
+ const filteredPreloadedFonts = preloadedFonts.filter(font => !shouldSkipFont(font.family));
326
+
327
+ // Update allFonts with preloaded fonts
328
+ allFonts = filteredPreloadedFonts;
329
+
330
+ // Update filtered fonts immediately
331
+ updateFilteredFonts();
332
+
333
+ // Only set loading to false if we have fonts to display
334
+ if (filteredFonts.length > 0) {
335
+ isInitialLoading = false;
336
+ // console.log(`✅ [FontInput:${controllerId}] Initial loading complete for All category:`, {
337
+ // filteredFonts: filteredFonts.length
338
+ // });
339
+
340
+ // Clear the timeout since we've loaded successfully
341
+ if (initialLoadTimeout) {
342
+ clearTimeout(initialLoadTimeout);
343
+ initialLoadTimeout = null;
344
+ }
345
+ }
346
+ }
347
+ } else if (currentFilter === 'Mine') {
348
+ // console.log(`🔍 [FontInput:${controllerId}] Getting custom fonts for Mine category`);
349
+ // For Mine category, first check if we already have preloaded fonts
350
+ preloadedFonts = fontManagerInstance.getPreloadedFonts('Mine');
351
+
352
+ if (preloadedFonts.length > 0) {
353
+ console.log(`✅ [FontInput:${controllerId}] Using preloaded custom fonts for Mine category:`, {
354
+ count: preloadedFonts.length,
355
+ fonts: preloadedFonts.slice(0, 3).map(f => f.family)
356
+ });
357
+ } else {
358
+ // If no preloaded fonts found, load them now
359
+ console.log(`📚 [FontInput:${controllerId}] No preloaded custom fonts found, loading them now...`);
360
+ preloadedFonts = await fontManagerInstance.loadCustomFonts();
361
+ }
362
+
363
+ if (preloadedFonts.length > 0) {
364
+ // Update allFonts with custom fonts
365
+ allFonts = preloadedFonts;
366
+
367
+ // Update filtered fonts immediately
368
+ updateFilteredFonts();
369
+
370
+ // Only set loading to false if we have fonts to display
371
+ if (filteredFonts.length > 0) {
372
+ isInitialLoading = false;
373
+ // console.log(`✅ [FontInput:${controllerId}] Initial loading complete for Mine category:`, {
374
+ // filteredFonts: filteredFonts.length
375
+ // });
376
+
377
+ // Clear the timeout since we've loaded successfully
378
+ if (initialLoadTimeout) {
379
+ clearTimeout(initialLoadTimeout);
380
+ initialLoadTimeout = null;
381
+ }
382
+ }
383
+ }
384
+ }
385
+
386
+ // Only set loading state if we couldn't get preloaded fonts
387
+ if (preloadedFonts.length === 0) {
388
+ // console.log(`⚠️ [FontInput:${controllerId}] No preloaded fonts available, will load asynchronously`);
389
+ // Keep isInitialLoading as true
390
+
391
+ // Set a timeout to show loading indicator if initialization takes too long
392
+ initialLoadTimeout = setTimeout(() => {
393
+ // If we're still loading after 1 second, show the loading indicator
394
+ if (isInitialLoading) {
395
+ // console.log(`⚠️ [FontInput:${controllerId}] Initial font loading taking longer than expected`);
396
+ }
397
+ }, 1000); // Reduced from 2000ms to 1000ms
398
+ }
399
+
400
+ // Initialize fonts list with saved filter - use a separate async function
401
+ // to avoid blocking the UI
402
+ const initializeFontsAsync = async () => {
403
+ try {
404
+ // console.log(`🔄 [FontInput:${controllerId}] Starting font initialization for controller:`, controllerId);
405
+
406
+ // First, ensure the FontManager is fully initialized
407
+ if (!FontManager.isInitialized) {
408
+ // console.log(`⏳ [FontInput:${controllerId}] Waiting for FontManager to initialize...`);
409
+ // This will wait for the initialization promise to resolve
410
+ await FontManager.getInstance(appConfig.GOOGLE_FONTS_API_KEY);
411
+ }
412
+
413
+ // Skip loading if we already have preloaded fonts
414
+ if (allFonts.length === 0) {
415
+ // console.log(`🔍 [FontInput:${controllerId}] No fonts loaded yet, loading initial batch for ${currentFilter}`);
416
+ // Instead of loading all fonts, just get the first batch
417
+ let initialFonts: FontData[] = [];
418
+
419
+ if (currentFilter === 'Mine') {
420
+ // For Mine category, first check if we have preloaded fonts
421
+ initialFonts = fontManagerInstance.getPreloadedFonts('Mine');
422
+
423
+ if (initialFonts.length > 0) {
424
+ console.log(`📚 [FontInput:${controllerId}] Using preloaded custom fonts:`, {
425
+ count: initialFonts.length,
426
+ fonts: initialFonts.slice(0, 3).map(f => f.family)
427
+ });
428
+ } else {
429
+ // If no preloaded fonts, load them now
430
+ console.log(`📚 [FontInput:${controllerId}] No preloaded custom fonts found, loading them now...`);
431
+ initialFonts = await fontManagerInstance.loadCustomFonts();
432
+ }
433
+ } else if (currentFilter === 'All') {
434
+ // For All category, get the first batch
435
+ initialFonts = fontManagerInstance.getNextBatch(controllerId, 'All');
436
+ // console.log(`📚 [FontInput:${controllerId}] Loaded initial batch for All:`, {
437
+ // count: initialFonts.length,
438
+ // fonts: initialFonts.slice(0, 3).map(f => f.family)
439
+ // });
440
+ } else {
441
+ // For specific categories, first try to get preloaded fonts
442
+ initialFonts = fontManagerInstance.getPreloadedFonts(currentFilter as FontItem['category']);
443
+
444
+ if (initialFonts.length === 0) {
445
+ // If no preloaded fonts, get the first batch filtered by category
446
+ // console.log(`📚 [FontInput:${controllerId}] No preloaded fonts for ${currentFilter}, getting batch`);
447
+ initialFonts = fontManagerInstance.getNextBatch(controllerId, currentFilter);
448
+ } else {
449
+ // console.log(`📚 [FontInput:${controllerId}] Using preloaded fonts for ${currentFilter}:`, {
450
+ // count: initialFonts.length,
451
+ // fonts: initialFonts.slice(0, 3).map(f => f.family)
452
+ // });
453
+ }
454
+ }
455
+
456
+ // Filter out fonts that should be skipped
457
+ const filteredFonts = initialFonts.filter(font => !shouldSkipFont(font.family));
458
+ // console.log(`🔍 [FontInput:${controllerId}] Filtered initial fonts:`, {
459
+ // before: initialFonts.length,
460
+ // after: filteredFonts.length,
461
+ // skipped: initialFonts.length - filteredFonts.length
462
+ // });
463
+
464
+ // Update allFonts with just the initial batch
465
+ allFonts = filteredFonts;
466
+
467
+ // Update filtered fonts immediately
468
+ updateFilteredFonts();
469
+ } else {
470
+ // console.log(`✅ [FontInput:${controllerId}] Already have fonts loaded:`, {
471
+ // count: allFonts.length,
472
+ // category: currentFilter
473
+ // });
474
+ }
475
+
476
+ // Set loading to false after fonts are loaded
477
+ // Only set loading to false if we have fonts to display
478
+ if (filteredFonts.length > 0) {
479
+ isInitialLoading = false;
480
+ // console.log(`✅ [FontInput:${controllerId}] Initialization complete with fonts:`, {
481
+ // count: filteredFonts.length,
482
+ // category: currentFilter
483
+ // });
484
+ } else {
485
+ // console.log(`⚠️ [FontInput:${controllerId}] No fonts found after initialization`);
486
+ // Still set loading to false to show empty state
487
+ isInitialLoading = false;
488
+ }
489
+
490
+ // Clear the timeout since we've loaded successfully
491
+ if (initialLoadTimeout) {
492
+ clearTimeout(initialLoadTimeout);
493
+ initialLoadTimeout = null;
494
+ }
495
+
496
+ // console.log(`✅ [FontInput:${controllerId}] Controller initialized with fonts:`, {
497
+ // controllerId,
498
+ // allFonts: allFonts.length,
499
+ // filteredFonts: filteredFonts.length,
500
+ // recentFonts: recentFonts.length,
501
+ // currentFilter,
502
+ // customFonts: allFonts.filter(f => f.category === 'Mine').length
503
+ // });
504
+ } catch (error) {
505
+ console.error(`❌ [FontInput:${controllerId}] Error during async font initialization:`, error);
506
+ isInitialLoading = false; // Make sure to set loading to false even on error
507
+
508
+ // Clear the timeout on error
509
+ if (initialLoadTimeout) {
510
+ clearTimeout(initialLoadTimeout);
511
+ initialLoadTimeout = null;
512
+ }
513
+ }
514
+ };
515
+
516
+ // Start the async initialization without waiting for it
517
+ initializeFontsAsync();
518
+
519
+ } catch (error) {
520
+ console.error('❌ [FontInput] Error during initialization:', error);
521
+ isInitialLoading = false; // Make sure to set loading to false even on error
522
+
523
+ // Clear the timeout on error
524
+ if (initialLoadTimeout) {
525
+ clearTimeout(initialLoadTimeout);
526
+ initialLoadTimeout = null;
527
+ }
528
+ }
529
+ });
530
+
531
+ /**
532
+ * Initialize fonts based on the current filter
533
+ */
534
+ async function initializeFonts() {
535
+ if (currentFilter === 'Mine') {
536
+ // First check if we have preloaded fonts
537
+ const preloadedCustomFonts = fontManagerInstance.getPreloadedFonts('Mine');
538
+
539
+ if (preloadedCustomFonts.length > 0) {
540
+ console.log(`📚 [FontInput:${controllerId}] Using preloaded custom fonts in initializeFonts:`, {
541
+ count: preloadedCustomFonts.length,
542
+ fonts: preloadedCustomFonts.slice(0, 3).map(f => f.family)
543
+ });
544
+ return preloadedCustomFonts;
545
+ } else {
546
+ // If no preloaded fonts, load them now
547
+ console.log(`📚 [FontInput:${controllerId}] No preloaded custom fonts found in initializeFonts, loading them now...`);
548
+ const customFonts = await fontManagerInstance.loadCustomFonts();
549
+ return customFonts;
550
+ }
551
+ }
552
+
553
+ // For All category, load Google Fonts first, then get custom fonts (already preloaded or load them)
554
+ const googleFonts = await fontManagerInstance.getInitialFontList();
555
+
556
+ // First check if we have preloaded custom fonts
557
+ let customFonts = fontManagerInstance.getPreloadedFonts('Mine');
558
+
559
+ if (customFonts.length === 0) {
560
+ // If no preloaded custom fonts, load them now
561
+ customFonts = await fontManagerInstance.loadCustomFonts();
562
+ }
563
+
564
+ if (currentFilter === 'All') {
565
+ return [
566
+ ...googleFonts.filter(font => !font.isCustom),
567
+ ...customFonts
568
+ ];
569
+ }
570
+
571
+ // For other categories, filter out custom fonts
572
+ return googleFonts.filter(font => !font.isCustom);
573
+ }
574
+
575
+ /**
576
+ * Fetches available fonts from Google Fonts API
577
+ */
578
+ async function fetchGoogleFonts() {
579
+ try {
580
+ // console.log('🔍 [FontInput] Fetching Google Fonts...');
581
+ fontManagerInstance = await FontManager.getInstance(appConfig.GOOGLE_FONTS_API_KEY);
582
+ allFonts = await fontManagerInstance.getCompleteFontList();
583
+ updateFilteredFonts();
584
+
585
+ // console.log('✅ [FontInput] Fonts available:', allFonts.length);
586
+ } catch (error) {
587
+ console.error('❌ [FontInput] Error getting Google Fonts:', error);
588
+ }
589
+ }
590
+
591
+ /**
592
+ * Opens the file browser for font selection
593
+ * Receives font files in the format matching fonts
594
+ */
595
+ async function openFileBrowser() {
596
+
597
+ // console.log('🔍 [FontInput] Opening file browser for fonts:');
598
+
599
+ // try {
600
+ // // Get the current font's ID if it exists in our custom fonts
601
+ // const selectedId = value?.family ?
602
+ // fontManagerInstance?.getCachedFontList()
603
+ // .find(f => f.family === value.family && f.category === 'Mine')?.id
604
+ // : undefined;
605
+
606
+ // console.log('🔍 [FontInput] Opening file browser for fonts:', {
607
+ // fileTypes: ['font'],
608
+ // selectedId
609
+ // });
610
+
611
+ // const result = await window.uiSDK?.mountComponent('file-browser', {
612
+ // container: '#file-browser',
613
+ // config: {
614
+ // fileTypes: ['font'],
615
+ // selectedId,
616
+ // batch: 1000 // allow for a large number of fonts to be selected
617
+ // },
618
+ // callbacks: {
619
+ // onSelect: async (fontData: any) => {
620
+ // if (!fontData) return;
621
+
622
+ // const fontValue = {
623
+ // family: fontData.family,
624
+ // weight: fontData.weight,
625
+ // category: 'Mine' // Set category to Mine for uploaded fonts
626
+ // };
627
+
628
+ // console.log('✅ [FontInput] Font selected:', {
629
+ // ...fontValue,
630
+ // format: fontData.url.split('.').pop()
631
+ // });
632
+
633
+ // if (onChange) {
634
+ // onChange(fontValue);
635
+ // }
636
+ // },
637
+ // onError: (error: Error) => {
638
+ // console.error('❌ [FontInput] File Browser error:', error);
639
+ // }
640
+ // }
641
+ // });
642
+
643
+ // } catch (error) {
644
+ // console.error('❌ [FontInput] Error opening file browser:', error);
645
+ // }
646
+ }
647
+
648
+
649
+ /**
650
+ * Updates the filtered fonts list based on search term and category
651
+ * Also handles setting up the observer after updating
652
+ *
653
+ * Logic flow:
654
+ * 1. Start with all fonts
655
+ * 2. Apply search filter if term exists
656
+ * 3. Apply category filter if not 'All'
657
+ */
658
+ function updateFilteredFonts() {
659
+ let fonts = [...allFonts];
660
+
661
+ // Clear both filters when search is active
662
+ if (searchTerm) {
663
+ const term = searchTerm.toLowerCase();
664
+ fonts = fonts.filter(font =>
665
+ font.family.toLowerCase().includes(term)
666
+ );
667
+
668
+ // Force category to All when searching
669
+ currentFilter = 'All';
670
+ } else {
671
+ if (currentFilter !== 'All') {
672
+ fonts = fonts.filter(font => {
673
+ if (currentFilter === 'Mine') return font.category === 'Mine';
674
+ return normalizeCategory(font.category) === currentFilter;
675
+ });
676
+ }
677
+ }
678
+
679
+ // Optionally filter out recent fonts
680
+ if (hideRecentFromList) {
681
+ const beforeRecent = fonts.length;
682
+ fonts = fonts.filter(font =>
683
+ !recentFonts.some(recent => recent.family === font.family)
684
+ );
685
+ // console.log(`🔍 [FontInput:${controllerId}] Filtered out recent fonts:`, {
686
+ // beforeFilter: beforeRecent,
687
+ // afterFilter: fonts.length,
688
+ // recentFontsRemoved: beforeRecent - fonts.length
689
+ // });
690
+ }
691
+
692
+ // Update filtered fonts
693
+ filteredFonts = fonts;
694
+
695
+ // console.log(`✅ [FontInput:${controllerId}] Filtered fonts result:`, {
696
+ // total: fonts.length,
697
+ // filter: currentFilter,
698
+ // searchTerm,
699
+ // hideRecentFromList,
700
+ // recentFontsHidden: hideRecentFromList ? recentFonts.length : 0,
701
+ // initialFontNumber
702
+ // });
703
+
704
+ // Setup observer after updating filtered fonts
705
+ // Use a small timeout to ensure the DOM has updated
706
+ if (scrollTimeout) {
707
+ clearTimeout(scrollTimeout);
708
+ }
709
+
710
+ scrollTimeout = setTimeout(() => {
711
+ setupFontObserver();
712
+ }, 50);
713
+ }
714
+
715
+ /**
716
+ * Handles font selection and updates recent fonts
717
+ */
718
+ function handleFontSelect(font: FontItem | RecentFont) {
719
+ if (onChange) {
720
+ // Reset weight to Regular or first available weight
721
+ fetchFontWeights(font.family).then(async () => {
722
+ const newWeight = availableWeights.includes('Regular') ?
723
+ 'Regular' : availableWeights[0];
724
+
725
+ selectedWeight = `${controllerId}-${newWeight}`;
726
+ if (browser) {
727
+ localStorage.setItem(FONT_WEIGHT_KEY(controllerId), selectedWeight);
728
+ }
729
+
730
+ // Load the full font when selected
731
+ if (!fontManagerInstance?.isCustomFont(font.family)) {
732
+ await fontManagerInstance?.loadFullFont(font.family);
733
+ }
734
+
735
+ // Create the font value object with reset weight
736
+ const fontValue = {
737
+ family: font.family,
738
+ weight: newWeight,
739
+ category: font.category
740
+ };
741
+
742
+ // Pass the font value object to onChange
743
+ onChange(fontValue);
744
+
745
+ // console.log('✅ [FontInput] Font selected:', {
746
+ // font: fontValue
747
+ // });
748
+ });
749
+ }
750
+ }
751
+
752
+ /**
753
+ * Manages real-time search functionality
754
+ * Prioritizes recent fonts in search results
755
+ * Enhanced to better handle partial matches
756
+ */
757
+ async function handleSearch(event: Event) {
758
+ const input = event.target as HTMLInputElement;
759
+ const newSearchTerm = input.value;
760
+
761
+ // Skip processing if the search term hasn't changed
762
+ if (newSearchTerm === searchTerm) return;
763
+
764
+ // Use debounced version only for typing
765
+ debouncedSearch(event);
766
+ }
767
+
768
+ // Add paste handler
769
+ function handlePaste(event: ClipboardEvent) {
770
+ const pastedText = event.clipboardData?.getData('text/plain') || '';
771
+
772
+ // Don't prevent default - let the paste happen naturally
773
+ // Then trigger an immediate search update with the pasted content
774
+ // searchTerm will be updated through binding
775
+
776
+ // Schedule the handleSearchImmediate for the next tick to ensure
777
+ // the input value is updated with the pasted content first
778
+ setTimeout(() => {
779
+ handleSearchImmediate(pastedText);
780
+ }, 0);
781
+ }
782
+
783
+ // Separate immediate search handler (keep this)
784
+ async function handleSearchImmediate(value: string) {
785
+ // Store current filter before overriding it
786
+ if (!searchTerm && value) {
787
+ lastActiveFilter = currentFilter;
788
+ currentFilter = 'All';
789
+ localStorage.setItem(FONT_FILTER_KEY(controllerId), 'All');
790
+ }
791
+
792
+ // Apply filtering with the new search term
793
+ updateFilteredFonts();
794
+
795
+ // Reset scroll position if needed
796
+ if (fontListContainer) {
797
+ fontListContainer.scrollTop = 0;
798
+ }
799
+ }
800
+
801
+ // Add a derived store for search results
802
+ let currentSearchResults: FontData[] = [];
803
+
804
+ // Update the reactive statement for search
805
+ $: if (searchTerm) {
806
+ const searchValue = searchTerm.toLowerCase().trim();
807
+ // Only process if the search value is not empty
808
+ if (searchValue) {
809
+ // Get both Google Fonts and custom fonts from cache
810
+ const googleFonts = fontManagerInstance?.getCachedFontList().filter(font => !font.isCustom) || [];
811
+ const customFonts = fontManagerInstance?.getCachedFontList().filter(font => font.isCustom) || [];
812
+
813
+ // Merge both lists
814
+ const allCachedFonts = [...googleFonts, ...customFonts];
815
+
816
+ // Modified search algorithm to use exact matching
817
+ const newSearchResults = allCachedFonts
818
+ .filter(font => {
819
+ const familyLower = font.family.toLowerCase().trim();
820
+
821
+ // Skip fonts that should be excluded
822
+ if (shouldSkipFont(font.family)) {
823
+ return false;
824
+ }
825
+
826
+ // Check if the search value is found in the font family
827
+ return familyLower.includes(searchValue);
828
+ })
829
+ .sort((a, b) => {
830
+ const aFamily = a.family.toLowerCase();
831
+ const bFamily = b.family.toLowerCase();
832
+
833
+ // Prioritize fonts that start with the search term
834
+ const aStartsWith = aFamily.startsWith(searchValue);
835
+ const bStartsWith = bFamily.startsWith(searchValue);
836
+
837
+ if (aStartsWith && !bStartsWith) return -1;
838
+ if (!aStartsWith && bStartsWith) return 1;
839
+
840
+ // Then prioritize fonts where search term appears earlier
841
+ const aIndex = aFamily.indexOf(searchValue);
842
+ const bIndex = bFamily.indexOf(searchValue);
843
+
844
+ if (aIndex !== -1 && bIndex !== -1) {
845
+ return aIndex - bIndex;
846
+ }
847
+
848
+ // Fall back to alphabetical sorting
849
+ return aFamily.localeCompare(bFamily);
850
+ });
851
+
852
+ // Only update results if they've changed
853
+ if (JSON.stringify(newSearchResults) !== JSON.stringify(currentSearchResults)) {
854
+ // Store all search results for batching
855
+ currentSearchResults = newSearchResults;
856
+
857
+ // Select first item automatically only if we have results and no item is currently selected
858
+ if (currentSearchResults.length > 0 && activeSearchIndex === -1) {
859
+ activeSearchIndex = 0;
860
+ }
861
+
862
+ // Show more initial results (double the default)
863
+ const initialResultsCount = Math.min(FontManager.initialFontNumber * 2, currentSearchResults.length);
864
+ filteredFonts = currentSearchResults.slice(0, initialResultsCount);
865
+
866
+ // Reset scroll position
867
+ if (fontListContainer) {
868
+ fontListContainer.scrollTop = 0;
869
+ }
870
+
871
+ // Preload only the first batch of fonts
872
+ filteredFonts.forEach(font => {
873
+ fontManagerInstance?.loadFullFont(font.family);
874
+ });
875
+
876
+ // console.log('📝 [FontSearch] Updated results:', {
877
+ // searchValue,
878
+ // totalMatches: currentSearchResults.length,
879
+ // displayedResults: filteredFonts.length,
880
+ // activeIndex: activeSearchIndex
881
+ // });
882
+ }
883
+ }
884
+ }
885
+
886
+ // Handle empty search term separately to avoid reactivity loop
887
+ $: if (searchTerm === '') {
888
+ // Only reset if we actually had search results before or if we have no filtered fonts
889
+ if (currentSearchResults.length > 0 || filteredFonts.length === 0) {
890
+ // console.log('🔄 [FontSearch] Handling empty search term');
891
+
892
+ // Reset search state
893
+ currentSearchResults = [];
894
+ activeSearchIndex = -1; // Reset active index
895
+
896
+ // Reset batch index to start fresh
897
+ if (fontManagerInstance) {
898
+ fontManagerInstance.resetBatchIndex(controllerId);
899
+
900
+ // Get fonts for current category
901
+ let fonts: FontData[] = [];
902
+
903
+ if (currentFilter === 'All') {
904
+ // Get initial batch of fonts instead of all fonts
905
+ fonts = fontManagerInstance.getNextBatch(controllerId, currentFilter);
906
+ // Update all fonts
907
+ allFonts = fonts;
908
+ // Update filtered fonts
909
+ filteredFonts = [];
910
+ updateFilteredFonts();
911
+ } else if (currentFilter === 'Mine') {
912
+ // For Mine category, first check if we have preloaded fonts
913
+ const preloadedCustomFonts = fontManagerInstance.getPreloadedFonts('Mine');
914
+
915
+ if (preloadedCustomFonts.length > 0) {
916
+ // Use preloaded custom fonts
917
+ console.log(`📚 [FontInput:${controllerId}] Using preloaded custom fonts for empty search:`, {
918
+ count: preloadedCustomFonts.length,
919
+ fonts: preloadedCustomFonts.slice(0, 3).map(f => f.family)
920
+ });
921
+ allFonts = preloadedCustomFonts;
922
+ filteredFonts = [];
923
+ updateFilteredFonts();
924
+ } else {
925
+ // If no preloaded fonts, load them asynchronously
926
+ console.log(`📚 [FontInput:${controllerId}] No preloaded custom fonts found for empty search, loading them now...`);
927
+ fontManagerInstance.loadCustomFonts().then(customFonts => {
928
+ allFonts = customFonts || [];
929
+ filteredFonts = [];
930
+ updateFilteredFonts();
931
+ });
932
+ }
933
+ } else {
934
+ // For other categories (Serif, Sans Serif, etc.), get initial batch by category
935
+ fonts = fontManagerInstance.getNextBatch(controllerId, currentFilter);
936
+ // Update all fonts
937
+ allFonts = fonts;
938
+ // Update filtered fonts
939
+ filteredFonts = [];
940
+ updateFilteredFonts();
941
+ }
942
+ }
943
+
944
+ // Reset scroll position when clearing search (for non-Mine categories)
945
+ if (currentFilter !== 'Mine' && fontListContainer) {
946
+ fontListContainer.scrollTop = 0;
947
+ }
948
+
949
+ // Setup observer in next tick for non-Mine categories
950
+ if (currentFilter !== 'Mine') {
951
+ if (scrollTimeout) {
952
+ clearTimeout(scrollTimeout);
953
+ }
954
+
955
+ scrollTimeout = setTimeout(() => {
956
+ if (fontListContainer) {
957
+ setupFontObserver();
958
+ }
959
+ }, 50);
960
+ }
961
+
962
+ // console.log('🔄 [FontSearch] Reset search state:', {
963
+ // currentFilter,
964
+ // allFonts: allFonts.length,
965
+ // filteredFonts: filteredFonts.length
966
+ // });
967
+ }
968
+ }
969
+
970
+ // Update keyboard navigation to use filteredFonts
971
+ function handleSearchKeydown(event: KeyboardEvent | CustomEvent<KeyboardEvent>) {
972
+ // Ensure we have a KeyboardEvent
973
+ const keyEvent = event instanceof KeyboardEvent ? event : event.detail;
974
+
975
+ // Prevent handling backspace when search is already empty
976
+ if (keyEvent.key === 'Backspace' && !searchTerm) {
977
+ keyEvent.preventDefault();
978
+ return;
979
+ }
980
+
981
+ if (!filteredFonts.length) return;
982
+
983
+ if (keyEvent.key === 'ArrowDown') {
984
+ keyEvent.preventDefault();
985
+ handleArrowClick('down');
986
+ } else if (keyEvent.key === 'ArrowUp') {
987
+ keyEvent.preventDefault();
988
+ handleArrowClick('up');
989
+ } else if (keyEvent.key === 'Enter' && activeSearchIndex !== -1) {
990
+ keyEvent.preventDefault();
991
+ handleFontSelect(filteredFonts[activeSearchIndex]);
992
+ }
993
+ }
994
+
995
+ async function handleFilterChange(filter: FilterType) {
996
+ if (currentFilter !== filter) {
997
+ lastActiveFilter = filter; // Store the new filter as last active
998
+ currentFilter = filter;
999
+
1000
+ if (browser) {
1001
+ localStorage.setItem(FONT_FILTER_KEY(controllerId), filter);
1002
+ }
1003
+
1004
+ if (fontManagerInstance) {
1005
+ fontManagerInstance.resetBatchIndex(controllerId);
1006
+
1007
+ try {
1008
+ // console.log(`🔄 [FontInput:${controllerId}] Category changed to:`, filter);
1009
+ let fonts: FontData[] = [];
1010
+
1011
+ if (filter === 'All') {
1012
+ // Get initial batch of fonts instead of all fonts
1013
+ fonts = fontManagerInstance.getNextBatch(controllerId, filter);
1014
+ // console.log(`📚 [FontInput:${controllerId}] Loaded initial batch for All category:`, {
1015
+ // count: fonts.length,
1016
+ // fonts: fonts.slice(0, 3).map(f => f.family)
1017
+ // });
1018
+ } else if (filter === 'Mine') {
1019
+ // Check if custom fonts are already loaded by getting them from the FontManager instance
1020
+ fonts = fontManagerInstance.getPreloadedFonts('Mine');
1021
+
1022
+ if (fonts.length === 0) {
1023
+ // If no preloaded custom fonts found, load them
1024
+ console.log(`📚 [FontInput:${controllerId}] No preloaded custom fonts found, loading them now...`);
1025
+ fonts = await fontManagerInstance.loadCustomFonts();
1026
+ } else {
1027
+ console.log(`📚 [FontInput:${controllerId}] Using preloaded custom fonts:`, {
1028
+ count: fonts.length,
1029
+ fonts: fonts.slice(0, 3).map(f => f.family)
1030
+ });
1031
+ }
1032
+ } else {
1033
+ // For other categories, first try to get preloaded fonts
1034
+ fonts = fontManagerInstance.getPreloadedFonts(filter as FontItem['category']);
1035
+
1036
+ if (fonts.length > 0) {
1037
+ // console.log(`📚 [FontInput:${controllerId}] Using preloaded fonts for ${filter}:`, {
1038
+ // count: fonts.length,
1039
+ // fonts: fonts.slice(0, 3).map(f => f.family)
1040
+ // });
1041
+ } else {
1042
+ // If no preloaded fonts, get the first batch filtered by category
1043
+ // console.log(`📚 [FontInput:${controllerId}] No preloaded fonts for ${filter}, getting batch`);
1044
+ fonts = fontManagerInstance.getNextBatch(controllerId, filter);
1045
+ }
1046
+ }
1047
+
1048
+ // Filter out fonts that should be skipped
1049
+ const filteredFonts = fonts.filter(font => !shouldSkipFont(font.family));
1050
+ // console.log(`🔍 [FontInput:${controllerId}] Filtered fonts after category change:`, {
1051
+ // before: fonts.length,
1052
+ // after: filteredFonts.length,
1053
+ // skipped: fonts.length - filteredFonts.length
1054
+ // });
1055
+
1056
+ allFonts = filteredFonts;
1057
+ updateFilteredFonts();
1058
+
1059
+ // Setup observer in next tick
1060
+ scrollTimeout = setTimeout(() => {
1061
+ if (fontListContainer) {
1062
+ setupFontObserver();
1063
+ }
1064
+ }, 0);
1065
+
1066
+ if (fontListContainer) {
1067
+ fontListContainer.scrollTop = 0;
1068
+ }
1069
+
1070
+ // console.log(`✅ [FontInput:${controllerId}] Category change complete:`, {
1071
+ // filter,
1072
+ // fonts: allFonts.length,
1073
+ // filteredFonts: filteredFonts.length,
1074
+ // customFonts: allFonts.filter(f => f.isCustom).length
1075
+ // });
1076
+ } catch (error) {
1077
+ console.error(`❌ [FontInput:${controllerId}] Error changing category:`, error);
1078
+ }
1079
+ }
1080
+ }
1081
+ }
1082
+ /**
1083
+ * Manages intersection observer for lazy loading fonts
1084
+ * Only loads fonts when they become visible in viewport
1085
+ */
1086
+ function setupFontObserver() {
1087
+ if (!fontListContainer) {
1088
+ // console.log(`⚠️ [FontInput:${controllerId}] Font list container not found, retrying...`);
1089
+ return scrollTimeout = setTimeout(() => {
1090
+ if (fontListContainer) {
1091
+ setupFontObserver();
1092
+ }
1093
+ }, 50);
1094
+ }
1095
+
1096
+ // If we have no fonts to observe, try to update filtered fonts first
1097
+ if (!filteredFonts.length) {
1098
+ // console.log(`⚠️ [FontInput:${controllerId}] No fonts available to observe, updating filtered fonts`);
1099
+ updateFilteredFonts();
1100
+
1101
+ // If we still have no fonts, log a warning and return
1102
+ if (!filteredFonts.length) {
1103
+ // console.warn(`❌ [FontInput:${controllerId}] No fonts available to observe after update`);
1104
+ return;
1105
+ }
1106
+ }
1107
+
1108
+ // console.log(`🔍 [FontInput:${controllerId}] Setting up font observer for`, filteredFonts.length, 'fonts');
1109
+
1110
+ // Get the observer from FontManager
1111
+ const observer = fontManagerInstance?.getObserver(fontListContainer);
1112
+ if (!observer) {
1113
+ // console.warn(`❌ [FontInput:${controllerId}] Failed to create observer`);
1114
+ return;
1115
+ }
1116
+
1117
+ // Disconnect existing observer if it exists
1118
+ if (fontObserver) {
1119
+ fontObserver.disconnect();
1120
+ }
1121
+
1122
+ fontObserver = observer;
1123
+
1124
+ // Use a more efficient approach to observe elements
1125
+ // First, get all font elements that need to be observed
1126
+ const fontElements = fontListContainer.querySelectorAll('[data-font]');
1127
+
1128
+ // If there are no elements to observe, try again after a short delay
1129
+ if (fontElements.length === 0) {
1130
+ // console.log(`⚠️ [FontInput:${controllerId}] No font elements found to observe, will retry`);
1131
+ return scrollTimeout = setTimeout(() => {
1132
+ setupFontObserver();
1133
+ }, 100);
1134
+ }
1135
+
1136
+ // console.log(`📝 [FontInput:${controllerId}] Observing`, fontElements.length, 'font elements');
1137
+
1138
+ // Use a more efficient approach to observe elements
1139
+ // Batch the observation process to reduce performance impact
1140
+ const batchSize = 20; // Process 20 elements at a time
1141
+ let currentIndex = 0;
1142
+
1143
+ const observeBatch = () => {
1144
+ const endIndex = Math.min(currentIndex + batchSize, fontElements.length);
1145
+
1146
+ for (let i = currentIndex; i < endIndex; i++) {
1147
+ observer.observe(fontElements[i]);
1148
+ }
1149
+
1150
+ currentIndex = endIndex;
1151
+
1152
+ if (currentIndex < fontElements.length) {
1153
+ // Process next batch in the next animation frame
1154
+ requestAnimationFrame(observeBatch);
1155
+ } else {
1156
+ // console.log(`✅ [FontInput:${controllerId}] Finished observing all font elements:`, {
1157
+ // total: fontElements.length,
1158
+ // batchSize,
1159
+ // batches: Math.ceil(fontElements.length / batchSize)
1160
+ // });
1161
+ }
1162
+ };
1163
+
1164
+ // Start the batched observation process
1165
+ observeBatch();
1166
+ }
1167
+
1168
+ /**
1169
+ * Handles font list scrolling
1170
+ */
1171
+ function handleFontListScroll(event: Event) {
1172
+ // Ensure target is an HTMLElement
1173
+ if (!(event.target instanceof HTMLElement)) {
1174
+ return;
1175
+ }
1176
+
1177
+ // Prevent event propagation to stop global scroll handler from closing the modal
1178
+ event.stopPropagation();
1179
+
1180
+ // Handle font loading for visible fonts
1181
+ fontManagerInstance?.handleScroll();
1182
+
1183
+ const container = event.target;
1184
+ const { scrollTop, scrollHeight, clientHeight } = container;
1185
+ // Increase the threshold to detect when we're closer to the bottom
1186
+ const scrolledToBottom = (scrollHeight - (scrollTop + clientHeight)) <= 100;
1187
+
1188
+ // Add debug log to help diagnose scrolling issues
1189
+ // console.log(`📜 [FontInput:${controllerId}] Scroll position:`, {
1190
+ // scrollTop,
1191
+ // scrollHeight,
1192
+ // clientHeight,
1193
+ // remainingScroll: scrollHeight - (scrollTop + clientHeight),
1194
+ // scrolledToBottom,
1195
+ // hasSearchTerm: Boolean(searchTerm),
1196
+ // searchResultsCount: currentSearchResults.length,
1197
+ // displayedResults: filteredFonts.length,
1198
+ // currentFilter
1199
+ // });
1200
+
1201
+ if (scrolledToBottom) {
1202
+ if (searchTerm && currentSearchResults.length > 0) {
1203
+ // Calculate next batch using SEARCH_BATCH_SIZE
1204
+ const nextBatchStart = filteredFonts.length;
1205
+ const nextBatch = currentSearchResults.slice(
1206
+ nextBatchStart,
1207
+ nextBatchStart + SEARCH_BATCH_SIZE
1208
+ );
1209
+
1210
+ if (nextBatch.length > 0) {
1211
+ // console.log(`🔄 [FontInput:${controllerId}] Loading next search batch:`, {
1212
+ // batchSize: nextBatch.length,
1213
+ // startIndex: nextBatchStart,
1214
+ // endIndex: nextBatchStart + nextBatch.length,
1215
+ // totalSearchResults: currentSearchResults.length
1216
+ // });
1217
+
1218
+ // Add new batch to filtered fonts
1219
+ filteredFonts = [...filteredFonts, ...nextBatch];
1220
+
1221
+ // Setup observer for new fonts
1222
+ scrollTimeout = setTimeout(() => {
1223
+ if (fontListContainer) {
1224
+ setupFontObserver();
1225
+ }
1226
+ }, 0);
1227
+
1228
+ // console.log(`📝 [FontInput:${controllerId}] Loaded next search batch:`, {
1229
+ // batchSize: nextBatch.length,
1230
+ // totalDisplayed: filteredFonts.length,
1231
+ // remainingFonts: currentSearchResults.length - filteredFonts.length,
1232
+ // nextBatchSize: SEARCH_BATCH_SIZE
1233
+ // });
1234
+ } else {
1235
+ // console.log(`ℹ️ [FontInput:${controllerId}] No more search results to load`);
1236
+ }
1237
+ } else {
1238
+ // Normal font list scrolling
1239
+ if (!isLoadingBatch && fontManagerInstance?.hasMoreFonts(controllerId, currentFilter)) {
1240
+ // console.log(`🔄 [FontInput:${controllerId}] Reached bottom, loading next batch for ${currentFilter}`);
1241
+ loadNextBatch();
1242
+ } else if (isLoadingBatch) {
1243
+ // console.log(`⏳ [FontInput:${controllerId}] Already loading a batch, skipping`);
1244
+ } else if (!fontManagerInstance?.hasMoreFonts(controllerId, currentFilter)) {
1245
+ // console.log(`ℹ️ [FontInput:${controllerId}] No more fonts to load for ${currentFilter}`);
1246
+ }
1247
+ }
1248
+ }
1249
+ }
1250
+
1251
+ /**
1252
+ * Loads the next batch of fonts
1253
+ */
1254
+ async function loadNextBatch() {
1255
+ if (isLoadingBatch) return;
1256
+
1257
+ try {
1258
+ isLoadingBatch = true;
1259
+ // console.log(`🔄 [FontInput:${controllerId}] Loading next batch for ${currentFilter}`);
1260
+
1261
+ // Get next batch for this controller
1262
+ const newFonts = fontManagerInstance.getNextBatch(controllerId, currentFilter);
1263
+ // console.log(`📚 [FontInput:${controllerId}] Received next batch:`, {
1264
+ // count: newFonts.length,
1265
+ // category: currentFilter,
1266
+ // fonts: newFonts.slice(0, 3).map(f => f.family)
1267
+ // });
1268
+
1269
+ // Add to existing fonts, ensuring uniqueness
1270
+ const combinedFonts = [...allFonts, ...newFonts];
1271
+ allFonts = Array.from(
1272
+ new Map(combinedFonts.map(font => [font.family, font])).values()
1273
+ );
1274
+
1275
+ // Update filtered fonts while preserving current filter
1276
+ updateFilteredFonts();
1277
+
1278
+ // console.log(`✅ [FontInput:${controllerId}] Loaded new batch:`, {
1279
+ // controllerId,
1280
+ // filter: currentFilter,
1281
+ // newFonts: newFonts.length,
1282
+ // totalFonts: allFonts.length,
1283
+ // filteredFonts: filteredFonts.length,
1284
+ // hasMoreFonts: fontManagerInstance.hasMoreFonts(controllerId, currentFilter)
1285
+ // });
1286
+
1287
+ } catch (error) {
1288
+ console.error(`❌ [FontInput:${controllerId}] Error loading font batch:`, error);
1289
+ } finally {
1290
+ isLoadingBatch = false;
1291
+ }
1292
+ }
1293
+
1294
+ function handleToggle() {
1295
+ // console.log('Font dropdown toggled:', controllerId);
1296
+ toggleDropdown(controllerId);
1297
+ }
1298
+
1299
+ function closeDropdown() {
1300
+ if (open) {
1301
+ handleToggle();
1302
+ }
1303
+ }
1304
+
1305
+ // Create a custom action for font observation
1306
+ function observeFont(node: HTMLElement) {
1307
+ if (fontObserver) {
1308
+ fontObserver.observe(node);
1309
+ }
1310
+
1311
+ return {
1312
+ destroy() {
1313
+ if (fontObserver) {
1314
+ fontObserver.unobserve(node);
1315
+ }
1316
+ }
1317
+ };
1318
+ }
1319
+
1320
+ $: open = $openDropdowns.has(controllerId);
1321
+
1322
+ // Add a reactive statement to set up observer when fonts are loaded
1323
+ $: if (filteredFonts.length > 0 && fontListContainer) {
1324
+ // console.log('📝 [FontInput] Fonts loaded, setting up observer...');
1325
+ setupFontObserver();
1326
+ }
1327
+
1328
+ $: popoverStyle = `width: ${buttonWidth}px`;
1329
+
1330
+ /**
1331
+ * Fetches and processes font weights from Google Fonts API
1332
+ * or from fonts.json for custom fonts
1333
+ */
1334
+ async function fetchFontWeights(fontFamily: string) {
1335
+ try {
1336
+ // First check if it's a custom font using FontManager
1337
+ if (fontManagerInstance?.isCustomFont(fontFamily)) {
1338
+ // Get custom font data
1339
+ const customFontResponse = await fetch(fontsJsonUrl);
1340
+ if (customFontResponse.ok) {
1341
+ const customFontsData = await customFontResponse.json();
1342
+ const customFont = customFontsData.fontFamilies.find(
1343
+ (f: any) => f.family === fontFamily
1344
+ );
1345
+
1346
+ if (customFont) {
1347
+ // For custom fonts, get all variants from fonts.json
1348
+ availableWeights = customFont.variants
1349
+ .map((v: any) => v.weight)
1350
+ // Remove duplicates if any
1351
+ .filter((weight: string, index: number, self: string[]) =>
1352
+ self.indexOf(weight) === index
1353
+ )
1354
+ // Sort with Regular first, then alphabetically
1355
+ .sort((a: string, b: string) => {
1356
+ if (a === 'Regular') return -1;
1357
+ if (b === 'Regular') return 1;
1358
+ return a.localeCompare(b);
1359
+ });
1360
+
1361
+ // Set default weight only if not already set for this controller
1362
+ if (!selectedWeight.startsWith(controllerId)) {
1363
+ selectedWeight = `${controllerId}-${availableWeights.includes('Regular') ?
1364
+ 'Regular' : availableWeights[0]}`;
1365
+ }
1366
+
1367
+ return;
1368
+ }
1369
+ }
1370
+ }
1371
+
1372
+ // Google font: use catalog cached by FontManager (no extra API call per font)
1373
+ const variantIds = fontManagerInstance?.getGoogleFontVariantIds(fontFamily);
1374
+ if (variantIds?.length) {
1375
+ availableWeights = mapGoogleVariantIdsToWeightNames(variantIds);
1376
+ if (!availableWeights.includes(selectedWeight.replace(`${controllerId}-`, ''))) {
1377
+ selectedWeight = `${controllerId}-${availableWeights.includes('Regular') ? 'Regular' : availableWeights[0]}`;
1378
+ }
1379
+ return;
1380
+ }
1381
+
1382
+ // API unavailable (403 / no catalog): sensible defaults, still loads via fonts.googleapis.com CSS
1383
+ console.warn('[FontInput] Using default weights for font (Google catalog not loaded):', fontFamily);
1384
+ availableWeights = ['Regular', 'Bold'];
1385
+ selectedWeight = `${controllerId}-Regular`;
1386
+ } catch (error) {
1387
+ console.error('Error fetching font weights:', error);
1388
+ availableWeights = ['Regular'];
1389
+ selectedWeight = `${controllerId}-Regular`;
1390
+ }
1391
+ }
1392
+
1393
+ $: if (resolvedFont?.family) {
1394
+ fetchFontWeights(resolvedFont.family);
1395
+ }
1396
+
1397
+ /**
1398
+ * Highlights search term matches in font names
1399
+ * @param name Font name
1400
+ * @param term Search term
1401
+ * @returns HTML string with highlighted match
1402
+ *
1403
+ * Note: Returns safe HTML as we're only wrapping with <strong>
1404
+ */
1405
+ function highlightMatch(name: string, term: string) {
1406
+ // Check if this font is the selected one
1407
+ const isSelected = value?.family === name;
1408
+
1409
+ if (!term) {
1410
+ // If no search term, just return the name with appropriate color
1411
+ return isSelected
1412
+ ? `<span style="color: white">${name}</span>`
1413
+ : `<span style="color: hsl(var(--grey7))">${name}</span>`;
1414
+ }
1415
+
1416
+ const nameLower = name.toLowerCase();
1417
+ const termLower = term.toLowerCase().trim();
1418
+
1419
+ // If the name contains the search term, highlight that part
1420
+ if (nameLower.includes(termLower)) {
1421
+ const index = nameLower.indexOf(termLower);
1422
+ const beforeMatch = name.slice(0, index);
1423
+ const match = name.slice(index, index + termLower.length);
1424
+ const afterMatch = name.slice(index + termLower.length);
1425
+
1426
+ if (isSelected) {
1427
+ // For selected items: text is white, highlight is grey7
1428
+ return `<span style="color: white">${beforeMatch}<span style="color: hsl(var(--grey7))">${match}</span>${afterMatch}</span>`;
1429
+ } else {
1430
+ // For non-selected items: text is grey7, highlight is sage-green
1431
+ return `<span style="color: hsl(var(--grey7))">${beforeMatch}<span style="color: hsl(var(--sage-green))">${match}</span>${afterMatch}</span>`;
1432
+ }
1433
+ }
1434
+
1435
+ // Default case - just return the name with appropriate color
1436
+ return isSelected
1437
+ ? `<span style="color: white">${name}</span>`
1438
+ : `<span style="color: hsl(var(--grey7))">${name}</span>`;
1439
+ }
1440
+
1441
+ /**
1442
+ * Handles global scroll events
1443
+ */
1444
+ function handleGlobalScroll(event: Event) {
1445
+ // Ensure target is an HTMLElement before using closest
1446
+ if (!(event.target instanceof HTMLElement)) {
1447
+ return;
1448
+ }
1449
+
1450
+ const target = event.target;
1451
+
1452
+ // Check if scroll happened inside our containers
1453
+ const isInsideFontList = target === fontListContainer;
1454
+ const isInsidePopover = target.closest('.font-popover-content');
1455
+ const isInsideCommand = target.closest('[cmdk-root]');
1456
+
1457
+ // If scrolling is inside any of our containers, don't close
1458
+ if (isInsideFontList || isInsidePopover || isInsideCommand) {
1459
+ return;
1460
+ }
1461
+
1462
+ // If scrolling outside our containers, close dropdowns
1463
+ if ($openDropdowns?.has(controllerId)) {
1464
+ toggleDropdown(controllerId);
1465
+ }
1466
+ if ($openDropdowns?.has('weight-' + controllerId)) {
1467
+ toggleDropdown('weight-' + controllerId);
1468
+ }
1469
+ }
1470
+
1471
+ // Update onMount to add scroll and touch listeners with passive option
1472
+ onMount(() => {
1473
+ // Add scroll listener with passive option
1474
+ window.addEventListener('scroll', handleGlobalScroll, {
1475
+ capture: true,
1476
+ passive: true
1477
+ });
1478
+
1479
+ // Add touch listeners with passive option
1480
+ window.addEventListener('touchstart', () => {}, { passive: true });
1481
+ window.addEventListener('touchmove', () => {}, { passive: true });
1482
+ });
1483
+
1484
+ // Update onDestroy to remove all listeners
1485
+ onDestroy(() => {
1486
+ window.removeEventListener('scroll', handleGlobalScroll, true);
1487
+ window.removeEventListener('touchstart', () => {});
1488
+ window.removeEventListener('touchmove', () => {});
1489
+
1490
+ if (fontObserver) {
1491
+ fontObserver.disconnect();
1492
+ }
1493
+ // Clear any timeouts
1494
+ if (scrollTimeout) {
1495
+ clearTimeout(scrollTimeout);
1496
+ }
1497
+ if (initialLoadTimeout) {
1498
+ clearTimeout(initialLoadTimeout);
1499
+ }
1500
+
1501
+ console.log('🧹 [FontInput] Cleanup complete:', {
1502
+ controllerId,
1503
+ lastFilter: currentFilter
1504
+ });
1505
+ });
1506
+
1507
+ // Add this function to the script section
1508
+ function handleDeleteCustomFont(font: FontItem | RecentFont, event: Event) {
1509
+ event.stopPropagation();
1510
+ // Convert returned RecentFonts to FontItems
1511
+ recentFonts = fontUtils.removeCustomFont(font.family).map(font => ({
1512
+ family: font.family,
1513
+ category: font.category as FontItem['category'],
1514
+ isCustom: font.isCustom
1515
+ }));
1516
+ updateFilteredFonts();
1517
+ }
1518
+
1519
+ // Add this function after the script tag
1520
+ function normalizeCategory(category: string): FontItem['category'] {
1521
+ return fontCategories.normalize(category);
1522
+ }
1523
+
1524
+ async function removeFont(fontFamily: string) {
1525
+ try {
1526
+ // Remove from manager
1527
+ await fontManagerInstance?.removeCustomFont(fontFamily);
1528
+
1529
+ // If the removed font was selected, clear the selection
1530
+ if (value?.family === fontFamily && onChange) {
1531
+ onChange({
1532
+ family: '',
1533
+ weight: 'Regular',
1534
+ category: ''
1535
+ });
1536
+ }
1537
+
1538
+ // Remove from recent fonts if present
1539
+ const updatedRecentFonts = recentFonts.filter(f => f.family !== fontFamily);
1540
+ recentFonts = updatedRecentFonts;
1541
+
1542
+ // Add lastUsed property before updating store
1543
+ const recentFontsWithLastUsed = updatedRecentFonts.map(f => ({
1544
+ ...f,
1545
+ lastUsed: Date.now()
1546
+ }));
1547
+ recentFontsStore.set(recentFontsWithLastUsed);
1548
+
1549
+ // Remove from all fonts
1550
+ allFonts = allFonts.filter(f => f.family !== fontFamily);
1551
+
1552
+ // Update filtered fonts
1553
+ updateFilteredFonts();
1554
+
1555
+ console.log('✅ [FontInput] Font removed:', {
1556
+ fontFamily,
1557
+ remainingFonts: allFonts.length,
1558
+ filteredFonts: filteredFonts.length,
1559
+ recentFonts: recentFonts.length
1560
+ });
1561
+ } catch (error) {
1562
+ console.error('❌ [FontInput] Error removing font:', error);
1563
+ }
1564
+ }
1565
+
1566
+ // Update the weight selection handler
1567
+ function handleWeightSelect(weight: string) {
1568
+ selectedWeight = `${controllerId}-${weight}`;
1569
+ if (browser) {
1570
+ localStorage.setItem(FONT_WEIGHT_KEY(controllerId), selectedWeight);
1571
+ }
1572
+ if (onChange) {
1573
+ onChange({
1574
+ family: value?.family || '',
1575
+ weight: weight,
1576
+ category: value?.category || ''
1577
+ });
1578
+ }
1579
+ }
1580
+
1581
+ // Update the template to use the modified weight
1582
+ $: displayWeight = selectedWeight.replace(`${controllerId}-`, '');
1583
+
1584
+ // Update arrow click handler to use filteredFonts
1585
+ function handleArrowClick(direction: 'up' | 'down') {
1586
+ if (!filteredFonts.length) return;
1587
+
1588
+ if (direction === 'down') {
1589
+ activeSearchIndex = (activeSearchIndex + 1) % filteredFonts.length;
1590
+ } else {
1591
+ activeSearchIndex = activeSearchIndex <= 0 ? filteredFonts.length - 1 : activeSearchIndex - 1;
1592
+ }
1593
+
1594
+ // Ensure the selected item is visible
1595
+ const selectedElement = fontListContainer?.querySelector(`[data-font="${filteredFonts[activeSearchIndex].family}"]`);
1596
+ selectedElement?.scrollIntoView({ block: 'nearest' });
1597
+ }
1598
+
1599
+ // Add a function to handle mouse enter events on font items
1600
+ function handleMouseEnter(index: number) {
1601
+ // Update the active index to the hovered item
1602
+ activeSearchIndex = index;
1603
+ }
1604
+
1605
+ // Functions for RecentItemsSection
1606
+ function renderFontItem(font: FontItem) {
1607
+ return {
1608
+ html: `
1609
+ <div class="flex items-center flex-1 min-w-0">
1610
+ <span
1611
+ class="text-[24px] truncate"
1612
+ style="font-family: '${font.family}', sans-serif; font-display: swap;"
1613
+ >
1614
+ ${font.family}
1615
+ </span>
1616
+ </div>
1617
+ `
1618
+ };
1619
+ }
1620
+
1621
+ function isFontSelected(font: FontItem) {
1622
+ return resolvedFont?.family === font.family;
1623
+ }
1624
+
1625
+ function isCustomFont(font: FontItem) {
1626
+ return font.category === 'Mine' && Boolean(font.isCustom);
1627
+ }
1628
+
1629
+ function getFontKey(font: FontItem) {
1630
+ // Create a unique key by combining family with category
1631
+ // This prevents duplicate keys when two fonts have the same family name
1632
+ return `${font.family}-${font.category}`;
1633
+ }
1634
+
1635
+ function handleFontDelete(font: FontItem, event: Event) {
1636
+ event.stopPropagation();
1637
+ event.preventDefault();
1638
+ removeFont(font.family);
1639
+ }
1640
+ </script>
1641
+
1642
+ <!-- Dual IC: labels left, 185px inputs on the right (even when label is empty) -->
1643
+ <div class="font-ic flex flex-col gap-2 w-full min-w-0 widgetic-colors">
1644
+ <div class="font-family-row flex items-center justify-between w-full min-w-0 gap-2" style="min-height: 48px;">
1645
+ <div class="font-family-label-ct flex-1 min-w-0">
1646
+ {#if label}
1647
+ <InputControllerHeader {label} />
1648
+ {/if}
1649
+ </div>
1650
+ <div class="font-family-input-ct h-12 shrink-0 rounded-large border border-grey2 flex items-center bg-grey1 transition-colors duration-200 hover:border-forest-green" style="width: 185px; max-width: 185px; min-width: 185px; margin-left: auto;">
1651
+ <!-- Font Family Button -->
1652
+ <Popover.Root
1653
+ open={$openDropdowns?.has(controllerId)}
1654
+ onOpenChange={(isOpen) => {
1655
+ if (isOpen !== $openDropdowns?.has(controllerId)) {
1656
+ if (!isOpen && resolvedFont?.family && !shouldSkipFont(resolvedFont.family)) {
1657
+ fontUtils.addRecentFont({
1658
+ family: resolvedFont.family,
1659
+ category: normalizeCategory(resolvedFont.category) as FontItem['category'],
1660
+ isCustom: fontManagerInstance?.isCustomFont(resolvedFont.family) || false
1661
+ });
1662
+ } else if (isOpen) {
1663
+ // Reset active search index when opening the dropdown
1664
+ activeSearchIndex = -1;
1665
+
1666
+ // When opening, ensure we have fonts ready
1667
+ if (filteredFonts.length === 0 && allFonts.length > 0) {
1668
+ // If we have allFonts but no filtered fonts, update them
1669
+ updateFilteredFonts();
1670
+ }
1671
+
1672
+ // Use a small timeout to ensure the DOM has updated before setting up the observer
1673
+ // This prevents the initial delay when opening the popover
1674
+ setTimeout(() => {
1675
+ if (fontListContainer) {
1676
+ setupFontObserver();
1677
+ }
1678
+ }, 50);
1679
+
1680
+ toggleDropdown(controllerId);
1681
+ }
1682
+ }
1683
+ }}
1684
+ >
1685
+ <Popover.Trigger class="w-full h-full flex items-center border-0 bg-transparent hover:bg-transparent focus:bg-transparent pl-3 pr-[16px] group"
1686
+ onmouseenter={() => {
1687
+ // Preload fonts when hovering over the button
1688
+ // This reduces the delay when actually clicking to open
1689
+ if (!$openDropdowns?.has(controllerId) && filteredFonts.length === 0 && allFonts.length > 0) {
1690
+ updateFilteredFonts();
1691
+ }
1692
+ }}
1693
+ >
1694
+ <span
1695
+ class="truncate flex-1 text-left subtext text-grey7"
1696
+ style="font-family: '{resolvedFont?.family || 'system-ui'}', sans-serif; font-display: swap;"
1697
+ >
1698
+ {resolvedFont?.family || "Select font..."}
1699
+ </span>
1700
+ <IconCaretDownFilled class="w-5 h-5 shrink-0 text-grey5 group-hover:text-grey7" />
1701
+ </Popover.Trigger>
1702
+
1703
+ <Popover.Content class="font-input-popover w-[374px] h-[565px] !p-0 widgetic-colors widgetic-radius rounded-medium modal-shadow border border-grey2">
1704
+ <div class="flex flex-col h-full font-popover-content">
1705
+ <!-- Title and upload icon -->
1706
+ <div class="relative pt-6 pb-[11px] px-5">
1707
+ <div class="h5 text-grey7 widgetic-colors">Fonts</div>
1708
+ <button
1709
+ type="button"
1710
+ class="absolute right-6 top-[26px] bg-transparent border-0 cursor-pointer p-0 rounded-full hover:bg-grey3 transition-all duration-300"
1711
+ onclick={() => {
1712
+ try {
1713
+ openFileBrowser();
1714
+ } catch (error) {
1715
+ console.error('Error opening file browser:', error);
1716
+ }
1717
+ }}
1718
+ >
1719
+ <IconCloudUpload class="w-6 h-6 text-grey7 hover:text-grey7 transition-colors duration-300 widgetic-colors" />
1720
+ </button>
1721
+ </div>
1722
+
1723
+ {#if description}
1724
+ <p class="px-4 -mt-2 mb-2 subtext text-grey5">{description}</p>
1725
+ {/if}
1726
+
1727
+ <!-- Search input -->
1728
+ <div class="relative px-4">
1729
+ <SearchInput
1730
+ placeholder="Search fonts..."
1731
+ bind:value={searchTerm}
1732
+ onInput={(e: Event) => {
1733
+ // Use debounced search only for typing
1734
+ debouncedSearch(e);
1735
+ }}
1736
+ onPaste={handlePaste}
1737
+ onKeydown={handleSearchKeydown}
1738
+ onClear={() => {
1739
+ searchTerm = ''; // Reset search term
1740
+
1741
+ // Restore the previous filter when clearing search
1742
+ currentFilter = lastActiveFilter;
1743
+ if (browser) {
1744
+ localStorage.setItem(FONT_FILTER_KEY(controllerId), lastActiveFilter);
1745
+ }
1746
+
1747
+ // ... rest of your onClear code ...
1748
+ }}
1749
+ />
1750
+ </div>
1751
+ <!-- Filter toggle group - sticky header -->
1752
+ {#if !searchTerm}
1753
+ <div class="sticky top-0 bg-white z-10">
1754
+ <CategoryTabs
1755
+ categories={filterOptions}
1756
+ activeCategory={currentFilter}
1757
+ onCategorySelect={handleFilterChange}
1758
+ filterVisible={!searchTerm}
1759
+ />
1760
+ </div>
1761
+ {/if}
1762
+
1763
+ <!-- Scrollable content -->
1764
+ <div
1765
+ class="flex-1 {filteredFonts.length > 0 ? 'overflow-y-auto' : 'overflow-hidden'} min-h-0 font-list-container"
1766
+ bind:this={fontListContainer}
1767
+ onscroll={(e) => {
1768
+ e.stopPropagation();
1769
+ handleFontListScroll(e);
1770
+ updateScrollState();
1771
+ }}
1772
+ id={`font-list-${controllerId}`}
1773
+ role="listbox"
1774
+ aria-label="Font list"
1775
+ >
1776
+ <div class="space-y-0">
1777
+ <!-- Recently used section - only show when not searching -->
1778
+ {#if recentFonts.length > 0 && !searchTerm}
1779
+ <RecentItemsSection
1780
+ recentItems={recentFonts}
1781
+ title="Recently used"
1782
+ onItemSelect={handleFontSelect}
1783
+ onItemDelete={handleFontDelete}
1784
+ isItemSelected={isFontSelected}
1785
+ renderItem={renderFontItem}
1786
+ isCustomItem={isCustomFont}
1787
+ getItemKey={getFontKey}
1788
+ observeItem={observeFont}
1789
+ displayMode="list"
1790
+ hideTrashIcon={hideTrashIcon}
1791
+ itemType="font"
1792
+ />
1793
+ {/if}
1794
+
1795
+ <!-- All fonts section -->
1796
+ <div class="mt-6 px-4">
1797
+ {#if filteredFonts.length > 0}
1798
+ <div id="font-header-{controllerId}" class="sticky top-0 -mt-2 bg-white z-10 pt-2 {isScrolled ? 'border-b border-grey2' : 'border-b border-transparent'} transition-border">
1799
+ <h3 class="subtext text-grey7 pb-2 flex items-center">
1800
+ {#if isInitialLoading}
1801
+ <div class="w-4 h-4 border-2 border-t-sage-green border-r-sage-green border-b-grey2 border-l-grey2 rounded-full animate-spin mr-2"></div>
1802
+ {/if}
1803
+ {searchTerm ? 'Search results' : 'More'}
1804
+ </h3>
1805
+ </div>
1806
+ {/if}
1807
+
1808
+ {#if filteredFonts.length === 0}
1809
+ <div class="empty-state-wrapper">
1810
+ {#if isInitialLoading}
1811
+ <!-- Show compact loading indicator while fonts are loading -->
1812
+ <div class="flex items-center">
1813
+ <div class="text-grey5 subtext mr-2">Loading fonts</div>
1814
+ <div class="w-4 h-4 border-2 border-t-sage-green border-r-sage-green border-b-grey2 border-l-grey2 rounded-full animate-spin"></div>
1815
+ </div>
1816
+ {:else}
1817
+ <!-- Only show empty state when loading is complete and no fonts were found -->
1818
+ <EmptyState
1819
+ message={searchTerm ? 'No fonts found' : 'No fonts available'}
1820
+ height="auto"
1821
+ />
1822
+ {/if}
1823
+ </div>
1824
+ {:else}
1825
+ <div class="pb-4"> <!-- Add a container with bottom padding -->
1826
+ {#each filteredFonts as font, index (`${font.family}-${searchTerm ? 'search' : currentFilter}-${index}`)}
1827
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
1828
+ <div
1829
+ class="font-list-row w-full py-1 px-4 cursor-pointer overflow-hidden relative rounded-small h-[42px] flex items-center
1830
+ {isFontSelected(font) ? 'bg-sage-green selected-item' :
1831
+ index === activeSearchIndex && searchTerm ? 'bg-light-green' :
1832
+ 'hover:bg-light-green'}"
1833
+ data-font={font.family}
1834
+ use:observeFont
1835
+ onclick={() => handleFontSelect(font)}
1836
+ onmouseenter={() => handleMouseEnter(index)}
1837
+ role="button"
1838
+ tabindex="0"
1839
+ style="color: {isFontSelected(font) ? '#ffffff' : 'hsl(var(--grey7))'};"
1840
+ >
1841
+ <div class="flex items-center flex-1 min-w-0 group">
1842
+ <Check class="mr-2 h-6 w-6 flex-shrink-0 {isFontSelected(font) ? 'text-white' : 'text-sage-green opacity-0'}"/>
1843
+ <span
1844
+ class="text-[24px] truncate {index === activeSearchIndex && searchTerm ? 'font-medium' : ''}"
1845
+ style="font-family: '{font.family}', sans-serif; font-display: swap; color: inherit; -webkit-text-fill-color: currentColor;"
1846
+ >
1847
+ {@html highlightMatch(font.family, searchTerm)}
1848
+ </span>
1849
+
1850
+ {#if font.category === 'Mine' && font.isCustom && !hideTrashIcon}
1851
+ <div
1852
+ role="button"
1853
+ tabindex="-1"
1854
+ class="ml-auto transition-opacity {value?.family === font.family ? 'opacity-0 pointer-events-none' : 'opacity-0 group-hover:opacity-100'}"
1855
+ onclick={(e) => {
1856
+ e.stopPropagation();
1857
+ e.preventDefault();
1858
+ removeFont(font.family);
1859
+ }}
1860
+ >
1861
+ <img
1862
+ src={icTrash}
1863
+ alt="Remove font"
1864
+ class="w-4 h-4 text-grey5 hover:text-error"
1865
+ draggable="false"
1866
+ />
1867
+ <span class="sr-only">Remove font</span>
1868
+ </div>
1869
+ {/if}
1870
+ </div>
1871
+ </div>
1872
+ {/each}
1873
+ </div>
1874
+ {/if}
1875
+ </div>
1876
+ </div>
1877
+ </div>
1878
+ </div>
1879
+ </Popover.Content>
1880
+ </Popover.Root>
1881
+ </div>
1882
+ </div>
1883
+
1884
+ <div class="font-weight-row flex items-center justify-between w-full min-w-0 gap-2" style="min-height: 48px;">
1885
+ <div class="font-weight-label-ct flex-1 min-w-0">
1886
+ <InputControllerHeader label="Font Weight" />
1887
+ </div>
1888
+ <div class="font-weight-input-ct h-12 shrink-0 rounded-large border border-grey2 flex items-center bg-grey1 transition-colors duration-200 hover:border-forest-green" style="width: 185px; max-width: 185px; min-width: 185px; margin-left: auto;">
1889
+ <Popover.Root
1890
+ open={$openDropdowns?.has('weight-' + controllerId)}
1891
+ onOpenChange={(isOpen) => {
1892
+ if (isOpen !== $openDropdowns?.has('weight-' + controllerId)) {
1893
+ toggleDropdown('weight-' + controllerId);
1894
+ }
1895
+ }}
1896
+ >
1897
+ <Popover.Trigger class="w-full h-full flex items-center border-0 bg-transparent hover:bg-transparent focus:bg-transparent pl-3 pr-[16px] group">
1898
+ <span class="truncate flex-1 text-left subtext text-grey7">
1899
+ {displayWeight || 'Regular'}
1900
+ </span>
1901
+ <IconCaretDownFilled class="w-5 h-5 shrink-0 text-grey5 group-hover:text-grey7" />
1902
+ </Popover.Trigger>
1903
+ <Popover.Content class="p-0 w-[150px]">
1904
+ <Command.Root>
1905
+ <Command.List>
1906
+ <Command.Group>
1907
+ {#each availableWeights as weight}
1908
+ <Command.Item
1909
+ value={weight}
1910
+ onSelect={() => handleWeightSelect(weight)}
1911
+ class="px-2 py-2"
1912
+ >
1913
+ <Check
1914
+ class="mr-1 h-4 w-4 {displayWeight === weight ? 'opacity-100' : 'opacity-0'}"
1915
+ />
1916
+ <span class="truncate">{weight}</span>
1917
+ </Command.Item>
1918
+ {/each}
1919
+ </Command.Group>
1920
+ </Command.List>
1921
+ </Command.Root>
1922
+ </Popover.Content>
1923
+ </Popover.Root>
1924
+ </div>
1925
+ </div>
1926
+
1927
+ {#if description}
1928
+ <p class="subtext text-grey5">{description}</p>
1929
+ {/if}
1930
+ </div>
1931
+
1932
+ <style>
1933
+ .font-family-row,
1934
+ .font-weight-row {
1935
+ display: flex;
1936
+ align-items: center;
1937
+ justify-content: space-between;
1938
+ width: 100%;
1939
+ }
1940
+
1941
+ .font-family-input-ct,
1942
+ .font-weight-input-ct {
1943
+ width: 185px !important;
1944
+ max-width: 185px !important;
1945
+ min-width: 185px !important;
1946
+ margin-left: auto;
1947
+ flex-shrink: 0;
1948
+ position: relative;
1949
+ }
1950
+
1951
+ /* Update button styles */
1952
+ :global(button.bg-transparent) {
1953
+ background: transparent !important;
1954
+ border: none !important;
1955
+ }
1956
+
1957
+ :global(button.bg-transparent:hover),
1958
+ :global(button.bg-transparent:focus) {
1959
+ background-color: transparent !important;
1960
+ }
1961
+
1962
+ /* Keep scrollbar styles */
1963
+ :global(.overflow-y-auto) {
1964
+ -webkit-overflow-scrolling: touch;
1965
+ scrollbar-width: thin;
1966
+ scrollbar-color: hsl(var(--grey3)) transparent;
1967
+ }
1968
+
1969
+ /* Trigger fills the 185px column only — do not leak width:100% to other ICs */
1970
+ .font-family-input-ct :global(.popover-trigger),
1971
+ .font-family-input-ct :global(.popover-trigger button),
1972
+ .font-weight-input-ct :global(.popover-trigger),
1973
+ .font-weight-input-ct :global(.popover-trigger button) {
1974
+ width: 100% !important;
1975
+ height: 100% !important;
1976
+ }
1977
+
1978
+ /* Custom styling for font items in RecentItemsSection */
1979
+ :global([data-item]) {
1980
+ border-radius: var(--radius-small) !important;
1981
+ }
1982
+
1983
+ :global([data-item].bg-\[hsl\(var\(--forest-green-10\)\)\]) {
1984
+ background-color: hsl(var(--light-green)) !important;
1985
+ color: hsl(var(--sage-green)) !important;
1986
+ }
1987
+
1988
+ :global([data-item]:hover) {
1989
+ background-color: hsl(var(--light-green)) !important;
1990
+ }
1991
+
1992
+ /* Optimize font popover rendering */
1993
+ :global(.font-input-popover) {
1994
+ z-index: 50;
1995
+ will-change: transform;
1996
+ transform: translateZ(0);
1997
+ backface-visibility: hidden;
1998
+ }
1999
+
2000
+ :global(.font-popover-content) {
2001
+ will-change: transform;
2002
+ transform: translateZ(0);
2003
+ backface-visibility: hidden;
2004
+ }
2005
+
2006
+ /* Optimize font list container scrolling */
2007
+ :global(.font-list-container) {
2008
+ -webkit-overflow-scrolling: touch;
2009
+ scrollbar-width: thin;
2010
+ scrollbar-color: hsl(var(--grey3)) transparent;
2011
+ will-change: transform;
2012
+ transform: translateZ(0);
2013
+ backface-visibility: hidden;
2014
+ }
2015
+
2016
+ /* Add transition only for border appearance */
2017
+ :global(.transition-border) {
2018
+ transition: border-color 0.2s ease-in;
2019
+ }
2020
+
2021
+ /* Prevent scrolling when showing empty state */
2022
+ .empty-state-wrapper {
2023
+ position: absolute;
2024
+ top: 0;
2025
+ left: 0;
2026
+ right: 0;
2027
+ bottom: 0;
2028
+ display: flex;
2029
+ align-items: center;
2030
+ justify-content: center;
2031
+ overflow: hidden;
2032
+ padding-top: 60px; /* Add padding to account for the header */
2033
+ padding-bottom: 60px; /* Balance the padding */
2034
+ z-index: 10; /* Ensure it appears above other content */
2035
+ }
2036
+
2037
+ /* Prevent hover effect on selected items */
2038
+ :global(.selected-item:hover) {
2039
+ background-color: hsl(var(--sage-green)) !important;
2040
+ color: white !important;
2041
+ }
2042
+
2043
+ /* Ensure text is white in selected items */
2044
+ :global(.selected-item) {
2045
+ color: white !important;
2046
+ }
2047
+
2048
+ /* Style for search result highlighting */
2049
+ :global(.search-result-highlight) {
2050
+ color: hsl(var(--sage-green)) !important;
2051
+ }
2052
+
2053
+ /* Style for active search item */
2054
+ :global(.active-search-item) {
2055
+ background-color: hsl(var(--light-green)) !important;
2056
+ color: hsl(var(--sage-green)) !important;
2057
+ }
2058
+
2059
+ /* Style for selected item text */
2060
+ :global(.selected-item-text) {
2061
+ color: white !important;
2062
+ }
2063
+
2064
+ /* Style for selected item highlight */
2065
+ :global(.selected-item-highlight) {
2066
+ color: hsl(var(--grey7)) !important;
2067
+ }
2068
+
2069
+ /* Style for checkmark icon */
2070
+ :global(.checkmark-icon) {
2071
+ margin-left: 12px !important;
2072
+ }
2073
+
2074
+ /* Style for font item padding */
2075
+ :global(.font-item) {
2076
+ padding-left: 48px !important;
2077
+ }
2078
+
2079
+ /* Style for font item height */
2080
+ :global(.font-item) {
2081
+ height: 42px !important;
2082
+ display: flex !important;
2083
+ align-items: center !important;
2084
+ }
2085
+
2086
+ /* Style for font item text */
2087
+ :global(.font-item-text) {
2088
+ font-size: 24px !important;
2089
+ }
2090
+
2091
+ /* Style for font item padding */
2092
+ :global(.font-item) {
2093
+ padding: 8px !important;
2094
+ }
2095
+ </style>
2096
+
2097
+
2098
+