@widgetic/editor 3.11.3 → 4.0.1

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