@sdcorejs/angular 19.0.0-rc.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 (740) hide show
  1. package/README.md +723 -0
  2. package/assets/fonts/fonts.scss +116 -0
  3. package/assets/fonts/material-icons/material-icons-outlined-v110-latin-regular.woff2 +0 -0
  4. package/assets/fonts/material-icons/material-icons-v145-latin-regular.woff2 +0 -0
  5. package/assets/fonts/material-symbols/material-symbols-v29-latin-regular.woff2 +0 -0
  6. package/assets/fonts/roboto/roboto-v50-latin_vietnamese-500.woff2 +0 -0
  7. package/assets/fonts/roboto/roboto-v50-latin_vietnamese-600.woff2 +0 -0
  8. package/assets/fonts/roboto/roboto-v50-latin_vietnamese-italic.woff2 +0 -0
  9. package/assets/fonts/roboto/roboto-v50-latin_vietnamese-regular.woff2 +0 -0
  10. package/assets/images/coming-soon.svg +23 -0
  11. package/assets/images/data-empty.svg +21 -0
  12. package/assets/images/expired.svg +22 -0
  13. package/assets/images/file-error.svg +28 -0
  14. package/assets/images/filter-empty.svg +20 -0
  15. package/assets/images/filter-required.svg +21 -0
  16. package/assets/images/forbidden.svg +15 -0
  17. package/assets/images/image-error.svg +27 -0
  18. package/assets/images/maintenance.svg +23 -0
  19. package/assets/images/not-found.svg +20 -0
  20. package/assets/images/offline.svg +20 -0
  21. package/assets/images/submitted.svg +18 -0
  22. package/assets/images/success.svg +18 -0
  23. package/assets/images/unauthorized.svg +22 -0
  24. package/assets/images/unknown-error.svg +18 -0
  25. package/assets/scss/README.md +604 -0
  26. package/assets/scss/ckeditor5.scss +88 -0
  27. package/assets/scss/core/color.scss +74 -0
  28. package/assets/scss/core/form.scss +302 -0
  29. package/assets/scss/core/image.scss +35 -0
  30. package/assets/scss/core/scrollbar.scss +30 -0
  31. package/assets/scss/core/utilities/_base.scss +333 -0
  32. package/assets/scss/core/utilities/_border.scss +22 -0
  33. package/assets/scss/core/utilities/_display.scss +10 -0
  34. package/assets/scss/core/utilities/_elevation.scss +80 -0
  35. package/assets/scss/core/utilities/_flexbox.scss +54 -0
  36. package/assets/scss/core/utilities/_gap.scss +9 -0
  37. package/assets/scss/core/utilities/_grid.scss +143 -0
  38. package/assets/scss/core/utilities/_index.scss +17 -0
  39. package/assets/scss/core/utilities/_misc.scss +20 -0
  40. package/assets/scss/core/utilities/_overflow.scss +9 -0
  41. package/assets/scss/core/utilities/_position.scss +8 -0
  42. package/assets/scss/core/utilities/_sizing.scss +28 -0
  43. package/assets/scss/core/utilities/_spacing.scss +31 -0
  44. package/assets/scss/core/utilities/_typography.scss +99 -0
  45. package/assets/scss/sd-core.scss +17 -0
  46. package/assets/scss/themes/default.scss +45 -0
  47. package/assets/scss/themes/material-theme.scss +101 -0
  48. package/components/anchor/index.d.ts +1 -0
  49. package/components/anchor/src/components/anchor/anchor.component.d.ts +22 -0
  50. package/components/anchor/src/components/anchor-item/anchor-item.component.d.ts +13 -0
  51. package/components/anchor/src/components/anchor-nav/anchor-nav.component.d.ts +21 -0
  52. package/components/anchor/src/components/index.d.ts +2 -0
  53. package/components/anchor/src/index.d.ts +2 -0
  54. package/components/anchor/src/models/index.d.ts +1 -0
  55. package/components/anchor/src/models/sd-anchor.model.d.ts +7 -0
  56. package/components/autoid-inspector/index.d.ts +8 -0
  57. package/components/autoid-inspector/src/autoid-inspector.component.d.ts +55 -0
  58. package/components/autoid-inspector/src/models/autoid-element.model.d.ts +55 -0
  59. package/components/autoid-inspector/src/models/autoid-export-format.model.d.ts +6 -0
  60. package/components/autoid-inspector/src/models/autoid-inspector-config.model.d.ts +8 -0
  61. package/components/autoid-inspector/src/services/autoid-audit.service.d.ts +19 -0
  62. package/components/autoid-inspector/src/services/autoid-export.service.d.ts +21 -0
  63. package/components/autoid-inspector/src/services/autoid-highlight.service.d.ts +12 -0
  64. package/components/autoid-inspector/src/services/autoid-scanner.service.d.ts +23 -0
  65. package/components/avatar/index.d.ts +1 -0
  66. package/components/avatar/src/avatar.component.d.ts +19 -0
  67. package/components/badge/index.d.ts +1 -0
  68. package/components/badge/src/badge.component.d.ts +98 -0
  69. package/components/base/index.d.ts +1 -0
  70. package/components/base/src/base-secure.component.d.ts +8 -0
  71. package/components/button/index.d.ts +1 -0
  72. package/components/button/src/button.component.d.ts +42 -0
  73. package/components/chart/index.d.ts +4 -0
  74. package/components/chart/src/bar-chart.component.d.ts +18 -0
  75. package/components/chart/src/doughnut-chart.component.d.ts +16 -0
  76. package/components/chart/src/line-chart.component.d.ts +18 -0
  77. package/components/chart/src/pie-chart.component.d.ts +16 -0
  78. package/components/ckeditor-styles/index.d.ts +1 -0
  79. package/components/ckeditor-styles/src/ckeditor-styles.component.d.ts +25 -0
  80. package/components/code-editor/index.d.ts +1 -0
  81. package/components/code-editor/src/code-editor.component.d.ts +25 -0
  82. package/components/document-builder/index.d.ts +4 -0
  83. package/components/document-builder/src/components/header-footer-builder/header-footer-builder.component.d.ts +16 -0
  84. package/components/document-builder/src/components/header-footer-builder/plugins/page-number.plugin.d.ts +4 -0
  85. package/components/document-builder/src/components/index.d.ts +1 -0
  86. package/components/document-builder/src/document-builder.component.d.ts +53 -0
  87. package/components/document-builder/src/document-builder.config.d.ts +21 -0
  88. package/components/document-builder/src/document-builder.model.d.ts +41 -0
  89. package/components/document-builder/src/document-builder.utils.d.ts +10 -0
  90. package/components/document-builder/src/plugins/block-space/block-space.plugin.d.ts +9 -0
  91. package/components/document-builder/src/plugins/ck-comment/ck-comment.plugin.d.ts +44 -0
  92. package/components/document-builder/src/plugins/ck-comment/ck-comment.plugin.model.d.ts +57 -0
  93. package/components/document-builder/src/plugins/heading/heading.plugin.d.ts +5 -0
  94. package/components/document-builder/src/plugins/highlight-range/highlight-range.plugin.d.ts +4 -0
  95. package/components/document-builder/src/plugins/image-custom/image-custom.plugin.d.ts +31 -0
  96. package/components/document-builder/src/plugins/image-upload/image-upload.plugin.d.ts +5 -0
  97. package/components/document-builder/src/plugins/index.d.ts +11 -0
  98. package/components/document-builder/src/plugins/page-orientation/page-orientation.plugin.d.ts +24 -0
  99. package/components/document-builder/src/plugins/paste-handler/filters/bookmark.d.ts +14 -0
  100. package/components/document-builder/src/plugins/paste-handler/filters/br.d.ts +15 -0
  101. package/components/document-builder/src/plugins/paste-handler/filters/image.d.ts +25 -0
  102. package/components/document-builder/src/plugins/paste-handler/filters/list.d.ts +29 -0
  103. package/components/document-builder/src/plugins/paste-handler/filters/parse.d.ts +35 -0
  104. package/components/document-builder/src/plugins/paste-handler/filters/removeboldwrapper.d.ts +15 -0
  105. package/components/document-builder/src/plugins/paste-handler/filters/removegooglesheetstag.d.ts +15 -0
  106. package/components/document-builder/src/plugins/paste-handler/filters/removeinvalidtablewidth.d.ts +15 -0
  107. package/components/document-builder/src/plugins/paste-handler/filters/removemsattributes.d.ts +15 -0
  108. package/components/document-builder/src/plugins/paste-handler/filters/removestyleblock.d.ts +15 -0
  109. package/components/document-builder/src/plugins/paste-handler/filters/removexmlns.d.ts +15 -0
  110. package/components/document-builder/src/plugins/paste-handler/filters/replacemsfootnotes.d.ts +54 -0
  111. package/components/document-builder/src/plugins/paste-handler/filters/replacetabswithinprewithspaces.d.ts +24 -0
  112. package/components/document-builder/src/plugins/paste-handler/filters/space.d.ts +27 -0
  113. package/components/document-builder/src/plugins/paste-handler/filters/table.d.ts +16 -0
  114. package/components/document-builder/src/plugins/paste-handler/filters/utils.d.ts +25 -0
  115. package/components/document-builder/src/plugins/paste-handler/index.d.ts +35 -0
  116. package/components/document-builder/src/plugins/paste-handler/normalizers/googledocsnormalizer.d.ts +31 -0
  117. package/components/document-builder/src/plugins/paste-handler/normalizers/googlesheetsnormalizer.d.ts +31 -0
  118. package/components/document-builder/src/plugins/paste-handler/normalizers/mswordnormalizer.d.ts +29 -0
  119. package/components/document-builder/src/plugins/paste-handler/types.d.ts +30 -0
  120. package/components/document-builder/src/plugins/table-custom/index.d.ts +34 -0
  121. package/components/document-builder/src/plugins/variable/variable.plugin.d.ts +51 -0
  122. package/components/editor/index.d.ts +3 -0
  123. package/components/editor/src/configurations/editor.configuration.d.ts +12 -0
  124. package/components/editor/src/configurations/index.d.ts +1 -0
  125. package/components/editor/src/editor.component.d.ts +52 -0
  126. package/components/editor/src/models/editor.model.d.ts +10 -0
  127. package/components/editor/src/models/image-upload.plugin.model.d.ts +51 -0
  128. package/components/editor/src/models/index.d.ts +2 -0
  129. package/components/editor/src/plugins/image-upload/image-upload.plugin.d.ts +22 -0
  130. package/components/editor/src/plugins/image-upload/utils/batch.utils.d.ts +14 -0
  131. package/components/editor/src/plugins/image-upload/utils/index.d.ts +3 -0
  132. package/components/editor/src/plugins/image-upload/utils/style.utils.d.ts +3 -0
  133. package/components/editor/src/plugins/image-upload/utils/validate.utils.d.ts +4 -0
  134. package/components/form-generic/index.d.ts +4 -0
  135. package/components/form-generic/src/components/form-builder/components/attribute-expression/attribute-expression.component.d.ts +23 -0
  136. package/components/form-generic/src/components/form-builder/components/attribute-input/attribute-input.component.d.ts +17 -0
  137. package/components/form-generic/src/components/form-builder/components/attribute-input-number/attribute-input-number.component.d.ts +14 -0
  138. package/components/form-generic/src/components/form-builder/components/attribute-parameter/attribute-parameter.component.d.ts +33 -0
  139. package/components/form-generic/src/components/form-builder/components/attribute-select/attribute-select.component.d.ts +26 -0
  140. package/components/form-generic/src/components/form-builder/components/attribute-selection/attribute-selection.component.d.ts +27 -0
  141. package/components/form-generic/src/components/form-builder/components/attribute-selection/components/build-queries/build-queries.component.d.ts +35 -0
  142. package/components/form-generic/src/components/form-builder/components/attribute-selection/components/build-variables/build-variables.component.d.ts +41 -0
  143. package/components/form-generic/src/components/form-builder/components/attribute-switch/attribute-switch.component.d.ts +14 -0
  144. package/components/form-generic/src/components/form-builder/components/attribute-table/attribute-table.component.d.ts +36 -0
  145. package/components/form-generic/src/components/form-builder/components/attribute-template/attribute-template.component.d.ts +15 -0
  146. package/components/form-generic/src/components/form-builder/components/attribute-textarea/attribute-textarea.component.d.ts +17 -0
  147. package/components/form-generic/src/components/form-builder/components/checkbox/attribute/checkbox-attribute.component.d.ts +21 -0
  148. package/components/form-generic/src/components/form-builder/components/checkbox/control/checkbox-control.component.d.ts +8 -0
  149. package/components/form-generic/src/components/form-builder/components/checkbox/index.d.ts +2 -0
  150. package/components/form-generic/src/components/form-builder/components/chip-calendar/attribute/chip-calendar-attribute.component.d.ts +22 -0
  151. package/components/form-generic/src/components/form-builder/components/chip-calendar/control/chip-calendar-control.component.d.ts +16 -0
  152. package/components/form-generic/src/components/form-builder/components/chip-calendar/index.d.ts +2 -0
  153. package/components/form-generic/src/components/form-builder/components/chip-string/attribute/chip-string-attribute.component.d.ts +22 -0
  154. package/components/form-generic/src/components/form-builder/components/chip-string/control/chip-string-control.component.d.ts +16 -0
  155. package/components/form-generic/src/components/form-builder/components/chip-string/index.d.ts +2 -0
  156. package/components/form-generic/src/components/form-builder/components/configure-validation/configure-validation.component.d.ts +31 -0
  157. package/components/form-generic/src/components/form-builder/components/datetime/attribute/datetime-attribute.component.d.ts +21 -0
  158. package/components/form-generic/src/components/form-builder/components/datetime/control/datetime-control.component.d.ts +18 -0
  159. package/components/form-generic/src/components/form-builder/components/datetime/index.d.ts +2 -0
  160. package/components/form-generic/src/components/form-builder/components/expression-builder/expression-builder.component.d.ts +47 -0
  161. package/components/form-generic/src/components/form-builder/components/group/attribute/group-attribute.component.d.ts +23 -0
  162. package/components/form-generic/src/components/form-builder/components/group/index.d.ts +1 -0
  163. package/components/form-generic/src/components/form-builder/components/html/attribute/components/build-queries/build-queries.component.d.ts +35 -0
  164. package/components/form-generic/src/components/form-builder/components/html/attribute/html-attribute.component.d.ts +30 -0
  165. package/components/form-generic/src/components/form-builder/components/html/control/html-control.component.d.ts +20 -0
  166. package/components/form-generic/src/components/form-builder/components/html/index.d.ts +2 -0
  167. package/components/form-generic/src/components/form-builder/components/index.d.ts +13 -0
  168. package/components/form-generic/src/components/form-builder/components/number/attribute/number-attribute.component.d.ts +22 -0
  169. package/components/form-generic/src/components/form-builder/components/number/control/number-control.component.d.ts +17 -0
  170. package/components/form-generic/src/components/form-builder/components/number/index.d.ts +2 -0
  171. package/components/form-generic/src/components/form-builder/components/radio/attribute/radio-attribute.component.d.ts +22 -0
  172. package/components/form-generic/src/components/form-builder/components/radio/control/radio-control.component.d.ts +16 -0
  173. package/components/form-generic/src/components/form-builder/components/radio/index.d.ts +2 -0
  174. package/components/form-generic/src/components/form-builder/components/select/attribute/select-attribute.component.d.ts +22 -0
  175. package/components/form-generic/src/components/form-builder/components/select/control/select-control.component.d.ts +16 -0
  176. package/components/form-generic/src/components/form-builder/components/select/index.d.ts +2 -0
  177. package/components/form-generic/src/components/form-builder/components/table/attribute/table-attribute.component.d.ts +22 -0
  178. package/components/form-generic/src/components/form-builder/components/table/control/table-control.component.d.ts +19 -0
  179. package/components/form-generic/src/components/form-builder/components/table/index.d.ts +2 -0
  180. package/components/form-generic/src/components/form-builder/components/textarea/attribute/textarea-attribute.component.d.ts +21 -0
  181. package/components/form-generic/src/components/form-builder/components/textarea/control/textarea-control.component.d.ts +16 -0
  182. package/components/form-generic/src/components/form-builder/components/textarea/index.d.ts +2 -0
  183. package/components/form-generic/src/components/form-builder/components/textfield/attribute/textfield-attribute.component.d.ts +21 -0
  184. package/components/form-generic/src/components/form-builder/components/textfield/control/textfield-control.component.d.ts +16 -0
  185. package/components/form-generic/src/components/form-builder/components/textfield/index.d.ts +2 -0
  186. package/components/form-generic/src/components/form-builder/components/upload/attribute/upload-attribute.component.d.ts +27 -0
  187. package/components/form-generic/src/components/form-builder/components/upload/control/upload-control.component.d.ts +16 -0
  188. package/components/form-generic/src/components/form-builder/components/upload/index.d.ts +2 -0
  189. package/components/form-generic/src/components/form-builder/form-builder.component.d.ts +120 -0
  190. package/components/form-generic/src/components/form-builder/services/builder.service.d.ts +11 -0
  191. package/components/form-generic/src/components/form-builder/services/index.d.ts +1 -0
  192. package/components/form-generic/src/components/form-render/components/index.d.ts +2 -0
  193. package/components/form-generic/src/components/form-render/components/item/components/chip-calendar/chip-calendar.component.d.ts +30 -0
  194. package/components/form-generic/src/components/form-render/components/item/components/chip-string/chip-string.component.d.ts +30 -0
  195. package/components/form-generic/src/components/form-render/components/item/components/datetime/datetime.component.d.ts +34 -0
  196. package/components/form-generic/src/components/form-render/components/item/components/html/html.component.d.ts +32 -0
  197. package/components/form-generic/src/components/form-render/components/item/components/index.d.ts +11 -0
  198. package/components/form-generic/src/components/form-render/components/item/components/number/number.component.d.ts +30 -0
  199. package/components/form-generic/src/components/form-render/components/item/components/radio/radio.component.d.ts +38 -0
  200. package/components/form-generic/src/components/form-render/components/item/components/select/select.component.d.ts +43 -0
  201. package/components/form-generic/src/components/form-render/components/item/components/table/table.component.d.ts +51 -0
  202. package/components/form-generic/src/components/form-render/components/item/components/textarea/textarea.component.d.ts +30 -0
  203. package/components/form-generic/src/components/form-render/components/item/components/textfield/textfield.component.d.ts +33 -0
  204. package/components/form-generic/src/components/form-render/components/item/components/upload/upload.component.d.ts +36 -0
  205. package/components/form-generic/src/components/form-render/components/item/item.component.d.ts +32 -0
  206. package/components/form-generic/src/components/form-render/components/variable/variable.component.d.ts +22 -0
  207. package/components/form-generic/src/components/form-render/form-render.component.d.ts +39 -0
  208. package/components/form-generic/src/components/index.d.ts +3 -0
  209. package/components/form-generic/src/components/sd-feel-expression/sd-feel-expression.component.d.ts +43 -0
  210. package/components/form-generic/src/configurations/form-generic.configuration.d.ts +6 -0
  211. package/components/form-generic/src/configurations/form.configuration.d.ts +12 -0
  212. package/components/form-generic/src/configurations/index.d.ts +2 -0
  213. package/components/form-generic/src/models/form-generic-component.model.d.ts +318 -0
  214. package/components/form-generic/src/models/form-generic-definition-html.model.d.ts +28 -0
  215. package/components/form-generic/src/models/form-generic-definition-selection.model.d.ts +49 -0
  216. package/components/form-generic/src/models/form-generic-definition-table.model.d.ts +6 -0
  217. package/components/form-generic/src/models/form-generic-expression.model.d.ts +74 -0
  218. package/components/form-generic/src/models/form-generic-template.model.d.ts +6 -0
  219. package/components/form-generic/src/models/form-generic-validation.model.d.ts +30 -0
  220. package/components/form-generic/src/models/form-generic.model.d.ts +7 -0
  221. package/components/form-generic/src/models/form-render/form-render-args.model.d.ts +7 -0
  222. package/components/form-generic/src/models/form-render/form-render-entity.model.d.ts +3 -0
  223. package/components/form-generic/src/models/form-render/index.d.ts +2 -0
  224. package/components/form-generic/src/models/index.d.ts +8 -0
  225. package/components/form-generic/src/pipes/component-viewed.pipe.d.ts +14 -0
  226. package/components/form-generic/src/pipes/expression-feel.pipe.d.ts +10 -0
  227. package/components/form-generic/src/pipes/expression-query.pipe.d.ts +10 -0
  228. package/components/form-generic/src/pipes/expression-view.pipe.d.ts +8 -0
  229. package/components/form-generic/src/pipes/html.pipe.d.ts +11 -0
  230. package/components/form-generic/src/pipes/hyperlink.pipe.d.ts +8 -0
  231. package/components/form-generic/src/pipes/index.d.ts +7 -0
  232. package/components/form-generic/src/pipes/when-expression.pipe.d.ts +13 -0
  233. package/components/form-generic/src/services/form-generic.service.d.ts +26 -0
  234. package/components/form-generic/src/services/form-render.service.d.ts +10 -0
  235. package/components/form-generic/src/services/index.d.ts +2 -0
  236. package/components/history/index.d.ts +1 -0
  237. package/components/history/models/history.model.d.ts +13 -0
  238. package/components/history/pipes/view-date.pipe.d.ts +8 -0
  239. package/components/history/src/history.component.d.ts +7 -0
  240. package/components/import-excel/index.d.ts +2 -0
  241. package/components/import-excel/src/import-excel.component.d.ts +47 -0
  242. package/components/import-excel/src/import-excel.model.d.ts +102 -0
  243. package/components/import-excel/src/pipes/columm-hidden.pipe.d.ts +10 -0
  244. package/components/import-excel/src/pipes/column-transform.pipe.d.ts +11 -0
  245. package/components/index.d.ts +23 -0
  246. package/components/inform/index.d.ts +2 -0
  247. package/components/inform/src/inform-action.directive.d.ts +9 -0
  248. package/components/inform/src/inform.component.d.ts +59 -0
  249. package/components/mini-editor/index.d.ts +2 -0
  250. package/components/mini-editor/src/mini-editor.component.d.ts +91 -0
  251. package/components/mini-editor/src/mini-editor.model.d.ts +44 -0
  252. package/components/modal/index.d.ts +1 -0
  253. package/components/modal/src/modal.component.d.ts +30 -0
  254. package/components/operator/index.d.ts +1 -0
  255. package/components/operator/src/operator.component.d.ts +34 -0
  256. package/components/preview/index.d.ts +4 -0
  257. package/components/preview/src/preview-image/preview-image.component.d.ts +88 -0
  258. package/components/preview/src/preview-image/preview-image.types.d.ts +53 -0
  259. package/components/preview/src/preview-pdf/preview-pdf.component.d.ts +186 -0
  260. package/components/preview/src/preview-pdf/preview-pdf.types.d.ts +84 -0
  261. package/components/query-bar/index.d.ts +2 -0
  262. package/components/query-bar/src/components/actions-bar/actions-bar.component.d.ts +28 -0
  263. package/components/query-bar/src/components/build-chip/build-chip.component.d.ts +67 -0
  264. package/components/query-bar/src/components/chip-popover/chip-popover.component.d.ts +73 -0
  265. package/components/query-bar/src/components/field-picker/field-picker.component.d.ts +37 -0
  266. package/components/query-bar/src/components/inline-chip/inline-chip.component.d.ts +87 -0
  267. package/components/query-bar/src/components/inline-value-chip/inline-value-chip.component.d.ts +71 -0
  268. package/components/query-bar/src/components/popover-chip/popover-chip.component.d.ts +35 -0
  269. package/components/query-bar/src/components/saved-filters-menu/saved-filters-menu.component.d.ts +39 -0
  270. package/components/query-bar/src/query-bar.component.d.ts +137 -0
  271. package/components/query-bar/src/query-bar.model.d.ts +136 -0
  272. package/components/query-builder/index.d.ts +1 -0
  273. package/components/query-builder/src/query-builder.component.d.ts +26 -0
  274. package/components/quick-action/index.d.ts +1 -0
  275. package/components/quick-action/src/quick-action.component.d.ts +9 -0
  276. package/components/section/index.d.ts +2 -0
  277. package/components/section/src/section-item/section-item.component.d.ts +7 -0
  278. package/components/section/src/section.component.d.ts +18 -0
  279. package/components/side-drawer/index.d.ts +1 -0
  280. package/components/side-drawer/src/side-drawer.component.d.ts +30 -0
  281. package/components/splitter/index.d.ts +3 -0
  282. package/components/splitter/src/splitter-handle/splitter-handle.component.d.ts +24 -0
  283. package/components/splitter/src/splitter-panel/splitter-panel.component.d.ts +16 -0
  284. package/components/splitter/src/splitter-state.service.d.ts +26 -0
  285. package/components/splitter/src/splitter.component.d.ts +28 -0
  286. package/components/splitter/src/splitter.models.d.ts +23 -0
  287. package/components/stepper/index.d.ts +2 -0
  288. package/components/stepper/src/step.component.d.ts +17 -0
  289. package/components/stepper/src/stepper.component.d.ts +33 -0
  290. package/components/tab/index.d.ts +2 -0
  291. package/components/tab/src/tab-group.component.d.ts +37 -0
  292. package/components/tab/src/tab.component.d.ts +13 -0
  293. package/components/tab-router/index.d.ts +4 -0
  294. package/components/tab-router/src/actions/tab-router.action.d.ts +5 -0
  295. package/components/tab-router/src/components/tab-router-item/tab-router-item.component.d.ts +22 -0
  296. package/components/tab-router/src/components/tab-router-nav/tab-router-nav.component.d.ts +16 -0
  297. package/components/tab-router/src/components/tab-router-outlet/tab-router-outlet.component.d.ts +15 -0
  298. package/components/tab-router/src/decorators/tab.decorator.d.ts +16 -0
  299. package/components/tab-router/src/events/tab-router.event.d.ts +11 -0
  300. package/components/tab-router/src/models/index.d.ts +1 -0
  301. package/components/tab-router/src/models/tab-router.model.d.ts +21 -0
  302. package/components/tab-router/src/pipes/tab-info.pipe.d.ts +11 -0
  303. package/components/tab-router/src/services/tab-decorator.service.d.ts +9 -0
  304. package/components/tab-router/src/services/tab-router.service.d.ts +27 -0
  305. package/components/table/index.d.ts +11 -0
  306. package/components/table/src/components/column-title/column-title.component.d.ts +10 -0
  307. package/components/table/src/components/command/desktop-command.component.d.ts +14 -0
  308. package/components/table/src/components/command/pipes/command.pipe.d.ts +30 -0
  309. package/components/table/src/components/command/pipes/filter.pipe.d.ts +35 -0
  310. package/components/table/src/components/config/config.component.d.ts +25 -0
  311. package/components/table/src/components/desktop-cell/desktop-cell.component.d.ts +24 -0
  312. package/components/table/src/components/desktop-cell/view/view.component.d.ts +24 -0
  313. package/components/table/src/components/filter/column-filter/column-filter.component.d.ts +32 -0
  314. package/components/table/src/components/filter/external-filter/external-filter.component.d.ts +27 -0
  315. package/components/table/src/components/filter/index.d.ts +3 -0
  316. package/components/table/src/components/filter/mobile-filter/mobile-filter.component.d.ts +42 -0
  317. package/components/table/src/components/index.d.ts +3 -0
  318. package/components/table/src/components/popup-export/popup-export.component.d.ts +51 -0
  319. package/components/table/src/components/selector-action/action-filter.pipe.d.ts +34 -0
  320. package/components/table/src/components/selector-action/selector-action.component.d.ts +16 -0
  321. package/components/table/src/configurations/index.d.ts +1 -0
  322. package/components/table/src/configurations/table.configuration.d.ts +35 -0
  323. package/components/table/src/directives/index.d.ts +6 -0
  324. package/components/table/src/directives/sd-column-resize.directive.d.ts +13 -0
  325. package/components/table/src/directives/sd-table-cell-def.directive.d.ts +16 -0
  326. package/components/table/src/directives/sd-table-column-filter-def.directive.d.ts +16 -0
  327. package/components/table/src/directives/sd-table-expand-def.directive.d.ts +13 -0
  328. package/components/table/src/directives/sd-table-filter-def.directive.d.ts +14 -0
  329. package/components/table/src/directives/sd-table-footer-def.directive.d.ts +16 -0
  330. package/components/table/src/directives/sd-table-group-def.directive.d.ts +44 -0
  331. package/components/table/src/directives/sd-table-title-def.directive.d.ts +14 -0
  332. package/components/table/src/directives/sticky-shadow.directive.d.ts +17 -0
  333. package/components/table/src/models/index.d.ts +5 -0
  334. package/components/table/src/models/table-column.model.d.ts +119 -0
  335. package/components/table/src/models/table-command.model.d.ts +25 -0
  336. package/components/table/src/models/table-item.model.d.ts +65 -0
  337. package/components/table/src/models/table-option-config.model.d.ts +42 -0
  338. package/components/table/src/models/table-option-expand.model.d.ts +6 -0
  339. package/components/table/src/models/table-option-export.model.d.ts +42 -0
  340. package/components/table/src/models/table-option-group.model.d.ts +21 -0
  341. package/components/table/src/models/table-option-paginate.model.d.ts +7 -0
  342. package/components/table/src/models/table-option-reload.model.d.ts +7 -0
  343. package/components/table/src/models/table-option-selector.model.d.ts +105 -0
  344. package/components/table/src/models/table-option-sort.model.d.ts +3 -0
  345. package/components/table/src/models/table-option-style.model.d.ts +11 -0
  346. package/components/table/src/models/table-option-tree.model.d.ts +12 -0
  347. package/components/table/src/models/table-option.model.d.ts +181 -0
  348. package/components/table/src/pipes/filter-values.pipe.d.ts +8 -0
  349. package/components/table/src/pipes/index.d.ts +2 -0
  350. package/components/table/src/pipes/sd-group.pipe.d.ts +23 -0
  351. package/components/table/src/pipes/sd-tree.pipe.d.ts +9 -0
  352. package/components/table/src/pipes/selection-disabled.pipe.d.ts +9 -0
  353. package/components/table/src/pipes/selection-visible-select-all.pipe.d.ts +9 -0
  354. package/components/table/src/pipes/selection-visible.pipe.d.ts +9 -0
  355. package/components/table/src/services/column-width.util.d.ts +7 -0
  356. package/components/table/src/services/config.service.d.ts +21 -0
  357. package/components/table/src/services/index.d.ts +3 -0
  358. package/components/table/src/services/table-export/table-export.service.d.ts +26 -0
  359. package/components/table/src/services/table-filter/table-filter.model.d.ts +150 -0
  360. package/components/table/src/services/table-filter/table-filter.service.d.ts +20 -0
  361. package/components/table/src/services/table-format/table-format.service.d.ts +16 -0
  362. package/components/table/src/services/tree/tree.util.d.ts +14 -0
  363. package/components/table/src/table.component.d.ts +137 -0
  364. package/components/upload-file/index.d.ts +2 -0
  365. package/components/upload-file/src/components/preview/preview.component.d.ts +23 -0
  366. package/components/upload-file/src/configurations/index.d.ts +1 -0
  367. package/components/upload-file/src/configurations/upload-file.configuration.d.ts +50 -0
  368. package/components/upload-file/src/pipes/document.pipe.d.ts +8 -0
  369. package/components/upload-file/src/pipes/image.pipe.d.ts +8 -0
  370. package/components/upload-file/src/pipes/index.d.ts +2 -0
  371. package/components/upload-file/src/services/index.d.ts +2 -0
  372. package/components/upload-file/src/services/upload-file.model.d.ts +12 -0
  373. package/components/upload-file/src/services/upload-file.service.d.ts +10 -0
  374. package/components/upload-file/src/upload-file.component.d.ts +85 -0
  375. package/components/view/index.d.ts +1 -0
  376. package/components/view/src/view.component.d.ts +17 -0
  377. package/configurations/index.d.ts +1 -0
  378. package/configurations/src/sd-core.configuration.d.ts +10 -0
  379. package/directives/index.d.ts +6 -0
  380. package/directives/src/sd-desktop.directive.d.ts +7 -0
  381. package/directives/src/sd-hover-copy.directive.d.ts +16 -0
  382. package/directives/src/sd-href.directive.d.ts +9 -0
  383. package/directives/src/sd-mobile.directive.d.ts +7 -0
  384. package/directives/src/sd-scroll.directive.d.ts +14 -0
  385. package/directives/src/sd-tooltip.directive.d.ts +26 -0
  386. package/fesm2022/sdcorejs-angular-components-anchor.mjs +197 -0
  387. package/fesm2022/sdcorejs-angular-components-anchor.mjs.map +1 -0
  388. package/fesm2022/sdcorejs-angular-components-autoid-inspector.mjs +723 -0
  389. package/fesm2022/sdcorejs-angular-components-autoid-inspector.mjs.map +1 -0
  390. package/fesm2022/sdcorejs-angular-components-avatar.mjs +103 -0
  391. package/fesm2022/sdcorejs-angular-components-avatar.mjs.map +1 -0
  392. package/fesm2022/sdcorejs-angular-components-badge.mjs +138 -0
  393. package/fesm2022/sdcorejs-angular-components-badge.mjs.map +1 -0
  394. package/fesm2022/sdcorejs-angular-components-base.mjs +23 -0
  395. package/fesm2022/sdcorejs-angular-components-base.mjs.map +1 -0
  396. package/fesm2022/sdcorejs-angular-components-button.mjs +118 -0
  397. package/fesm2022/sdcorejs-angular-components-button.mjs.map +1 -0
  398. package/fesm2022/sdcorejs-angular-components-chart.mjs +290 -0
  399. package/fesm2022/sdcorejs-angular-components-chart.mjs.map +1 -0
  400. package/fesm2022/sdcorejs-angular-components-ckeditor-styles.mjs +38 -0
  401. package/fesm2022/sdcorejs-angular-components-ckeditor-styles.mjs.map +1 -0
  402. package/fesm2022/sdcorejs-angular-components-code-editor.mjs +128 -0
  403. package/fesm2022/sdcorejs-angular-components-code-editor.mjs.map +1 -0
  404. package/fesm2022/sdcorejs-angular-components-document-builder.mjs +5053 -0
  405. package/fesm2022/sdcorejs-angular-components-document-builder.mjs.map +1 -0
  406. package/fesm2022/sdcorejs-angular-components-editor.mjs +1079 -0
  407. package/fesm2022/sdcorejs-angular-components-editor.mjs.map +1 -0
  408. package/fesm2022/sdcorejs-angular-components-form-generic.mjs +6715 -0
  409. package/fesm2022/sdcorejs-angular-components-form-generic.mjs.map +1 -0
  410. package/fesm2022/sdcorejs-angular-components-history.mjs +51 -0
  411. package/fesm2022/sdcorejs-angular-components-history.mjs.map +1 -0
  412. package/fesm2022/sdcorejs-angular-components-import-excel.mjs +583 -0
  413. package/fesm2022/sdcorejs-angular-components-import-excel.mjs.map +1 -0
  414. package/fesm2022/sdcorejs-angular-components-inform.mjs +190 -0
  415. package/fesm2022/sdcorejs-angular-components-inform.mjs.map +1 -0
  416. package/fesm2022/sdcorejs-angular-components-mini-editor.mjs +333 -0
  417. package/fesm2022/sdcorejs-angular-components-mini-editor.mjs.map +1 -0
  418. package/fesm2022/sdcorejs-angular-components-modal.mjs +108 -0
  419. package/fesm2022/sdcorejs-angular-components-modal.mjs.map +1 -0
  420. package/fesm2022/sdcorejs-angular-components-operator.mjs +75 -0
  421. package/fesm2022/sdcorejs-angular-components-operator.mjs.map +1 -0
  422. package/fesm2022/sdcorejs-angular-components-preview.mjs +1987 -0
  423. package/fesm2022/sdcorejs-angular-components-preview.mjs.map +1 -0
  424. package/fesm2022/sdcorejs-angular-components-query-bar.mjs +1430 -0
  425. package/fesm2022/sdcorejs-angular-components-query-bar.mjs.map +1 -0
  426. package/fesm2022/sdcorejs-angular-components-query-builder.mjs +83 -0
  427. package/fesm2022/sdcorejs-angular-components-query-builder.mjs.map +1 -0
  428. package/fesm2022/sdcorejs-angular-components-quick-action.mjs +22 -0
  429. package/fesm2022/sdcorejs-angular-components-quick-action.mjs.map +1 -0
  430. package/fesm2022/sdcorejs-angular-components-section.mjs +64 -0
  431. package/fesm2022/sdcorejs-angular-components-section.mjs.map +1 -0
  432. package/fesm2022/sdcorejs-angular-components-side-drawer.mjs +129 -0
  433. package/fesm2022/sdcorejs-angular-components-side-drawer.mjs.map +1 -0
  434. package/fesm2022/sdcorejs-angular-components-splitter.mjs +649 -0
  435. package/fesm2022/sdcorejs-angular-components-splitter.mjs.map +1 -0
  436. package/fesm2022/sdcorejs-angular-components-stepper.mjs +140 -0
  437. package/fesm2022/sdcorejs-angular-components-stepper.mjs.map +1 -0
  438. package/fesm2022/sdcorejs-angular-components-tab-router.mjs +605 -0
  439. package/fesm2022/sdcorejs-angular-components-tab-router.mjs.map +1 -0
  440. package/fesm2022/sdcorejs-angular-components-tab.mjs +145 -0
  441. package/fesm2022/sdcorejs-angular-components-tab.mjs.map +1 -0
  442. package/fesm2022/sdcorejs-angular-components-table.mjs +4524 -0
  443. package/fesm2022/sdcorejs-angular-components-table.mjs.map +1 -0
  444. package/fesm2022/sdcorejs-angular-components-upload-file.mjs +868 -0
  445. package/fesm2022/sdcorejs-angular-components-upload-file.mjs.map +1 -0
  446. package/fesm2022/sdcorejs-angular-components-view.mjs +48 -0
  447. package/fesm2022/sdcorejs-angular-components-view.mjs.map +1 -0
  448. package/fesm2022/sdcorejs-angular-components.mjs +28 -0
  449. package/fesm2022/sdcorejs-angular-components.mjs.map +1 -0
  450. package/fesm2022/sdcorejs-angular-configurations.mjs +10 -0
  451. package/fesm2022/sdcorejs-angular-configurations.mjs.map +1 -0
  452. package/fesm2022/sdcorejs-angular-directives.mjs +497 -0
  453. package/fesm2022/sdcorejs-angular-directives.mjs.map +1 -0
  454. package/fesm2022/sdcorejs-angular-forms-autocomplete.mjs +454 -0
  455. package/fesm2022/sdcorejs-angular-forms-autocomplete.mjs.map +1 -0
  456. package/fesm2022/sdcorejs-angular-forms-checkbox.mjs +139 -0
  457. package/fesm2022/sdcorejs-angular-forms-checkbox.mjs.map +1 -0
  458. package/fesm2022/sdcorejs-angular-forms-chip-calendar.mjs +364 -0
  459. package/fesm2022/sdcorejs-angular-forms-chip-calendar.mjs.map +1 -0
  460. package/fesm2022/sdcorejs-angular-forms-chip.mjs +363 -0
  461. package/fesm2022/sdcorejs-angular-forms-chip.mjs.map +1 -0
  462. package/fesm2022/sdcorejs-angular-forms-date-range.mjs +354 -0
  463. package/fesm2022/sdcorejs-angular-forms-date-range.mjs.map +1 -0
  464. package/fesm2022/sdcorejs-angular-forms-date.mjs +356 -0
  465. package/fesm2022/sdcorejs-angular-forms-date.mjs.map +1 -0
  466. package/fesm2022/sdcorejs-angular-forms-datetime.mjs +748 -0
  467. package/fesm2022/sdcorejs-angular-forms-datetime.mjs.map +1 -0
  468. package/fesm2022/sdcorejs-angular-forms-directives.mjs +67 -0
  469. package/fesm2022/sdcorejs-angular-forms-directives.mjs.map +1 -0
  470. package/fesm2022/sdcorejs-angular-forms-input-color.mjs +117 -0
  471. package/fesm2022/sdcorejs-angular-forms-input-color.mjs.map +1 -0
  472. package/fesm2022/sdcorejs-angular-forms-input-number.mjs +438 -0
  473. package/fesm2022/sdcorejs-angular-forms-input-number.mjs.map +1 -0
  474. package/fesm2022/sdcorejs-angular-forms-input.mjs +354 -0
  475. package/fesm2022/sdcorejs-angular-forms-input.mjs.map +1 -0
  476. package/fesm2022/sdcorejs-angular-forms-label.mjs +51 -0
  477. package/fesm2022/sdcorejs-angular-forms-label.mjs.map +1 -0
  478. package/fesm2022/sdcorejs-angular-forms-models.mjs +137 -0
  479. package/fesm2022/sdcorejs-angular-forms-models.mjs.map +1 -0
  480. package/fesm2022/sdcorejs-angular-forms-radio.mjs +190 -0
  481. package/fesm2022/sdcorejs-angular-forms-radio.mjs.map +1 -0
  482. package/fesm2022/sdcorejs-angular-forms-select.mjs +593 -0
  483. package/fesm2022/sdcorejs-angular-forms-select.mjs.map +1 -0
  484. package/fesm2022/sdcorejs-angular-forms-switch.mjs +144 -0
  485. package/fesm2022/sdcorejs-angular-forms-switch.mjs.map +1 -0
  486. package/fesm2022/sdcorejs-angular-forms-textarea.mjs +300 -0
  487. package/fesm2022/sdcorejs-angular-forms-textarea.mjs.map +1 -0
  488. package/fesm2022/sdcorejs-angular-forms.mjs +48 -0
  489. package/fesm2022/sdcorejs-angular-forms.mjs.map +1 -0
  490. package/fesm2022/sdcorejs-angular-handlers.mjs +67 -0
  491. package/fesm2022/sdcorejs-angular-handlers.mjs.map +1 -0
  492. package/fesm2022/sdcorejs-angular-i18n.mjs +2592 -0
  493. package/fesm2022/sdcorejs-angular-i18n.mjs.map +1 -0
  494. package/fesm2022/sdcorejs-angular-interceptors.mjs +166 -0
  495. package/fesm2022/sdcorejs-angular-interceptors.mjs.map +1 -0
  496. package/fesm2022/sdcorejs-angular-models.mjs +6 -0
  497. package/fesm2022/sdcorejs-angular-models.mjs.map +1 -0
  498. package/fesm2022/sdcorejs-angular-modules-auth.mjs +137 -0
  499. package/fesm2022/sdcorejs-angular-modules-auth.mjs.map +1 -0
  500. package/fesm2022/sdcorejs-angular-modules-authom.mjs +360 -0
  501. package/fesm2022/sdcorejs-angular-modules-authom.mjs.map +1 -0
  502. package/fesm2022/sdcorejs-angular-modules-keycloak.mjs +127 -0
  503. package/fesm2022/sdcorejs-angular-modules-keycloak.mjs.map +1 -0
  504. package/fesm2022/sdcorejs-angular-modules-layout.mjs +1633 -0
  505. package/fesm2022/sdcorejs-angular-modules-layout.mjs.map +1 -0
  506. package/fesm2022/sdcorejs-angular-modules-permission.mjs +249 -0
  507. package/fesm2022/sdcorejs-angular-modules-permission.mjs.map +1 -0
  508. package/fesm2022/sdcorejs-angular-modules.mjs +10 -0
  509. package/fesm2022/sdcorejs-angular-modules.mjs.map +1 -0
  510. package/fesm2022/sdcorejs-angular-pipes.mjs +149 -0
  511. package/fesm2022/sdcorejs-angular-pipes.mjs.map +1 -0
  512. package/fesm2022/sdcorejs-angular-services-api.mjs +235 -0
  513. package/fesm2022/sdcorejs-angular-services-api.mjs.map +1 -0
  514. package/fesm2022/sdcorejs-angular-services-cache.mjs +161 -0
  515. package/fesm2022/sdcorejs-angular-services-cache.mjs.map +1 -0
  516. package/fesm2022/sdcorejs-angular-services-confirm.mjs +208 -0
  517. package/fesm2022/sdcorejs-angular-services-confirm.mjs.map +1 -0
  518. package/fesm2022/sdcorejs-angular-services-docx.mjs +300 -0
  519. package/fesm2022/sdcorejs-angular-services-docx.mjs.map +1 -0
  520. package/fesm2022/sdcorejs-angular-services-excel.mjs +422 -0
  521. package/fesm2022/sdcorejs-angular-services-excel.mjs.map +1 -0
  522. package/fesm2022/sdcorejs-angular-services-license.mjs +104 -0
  523. package/fesm2022/sdcorejs-angular-services-license.mjs.map +1 -0
  524. package/fesm2022/sdcorejs-angular-services-loading.mjs +92 -0
  525. package/fesm2022/sdcorejs-angular-services-loading.mjs.map +1 -0
  526. package/fesm2022/sdcorejs-angular-services-notify.mjs +268 -0
  527. package/fesm2022/sdcorejs-angular-services-notify.mjs.map +1 -0
  528. package/fesm2022/sdcorejs-angular-services-storage.mjs +174 -0
  529. package/fesm2022/sdcorejs-angular-services-storage.mjs.map +1 -0
  530. package/fesm2022/sdcorejs-angular-services.mjs +13 -0
  531. package/fesm2022/sdcorejs-angular-services.mjs.map +1 -0
  532. package/fesm2022/sdcorejs-angular-utilities-data-state.mjs +38 -0
  533. package/fesm2022/sdcorejs-angular-utilities-data-state.mjs.map +1 -0
  534. package/fesm2022/sdcorejs-angular-utilities-extensions.mjs +391 -0
  535. package/fesm2022/sdcorejs-angular-utilities-extensions.mjs.map +1 -0
  536. package/fesm2022/sdcorejs-angular-utilities-models.mjs +55 -0
  537. package/fesm2022/sdcorejs-angular-utilities-models.mjs.map +1 -0
  538. package/fesm2022/sdcorejs-angular-utilities.mjs +8 -0
  539. package/fesm2022/sdcorejs-angular-utilities.mjs.map +1 -0
  540. package/fesm2022/sdcorejs-angular.mjs +19 -0
  541. package/fesm2022/sdcorejs-angular.mjs.map +1 -0
  542. package/forms/autocomplete/index.d.ts +1 -0
  543. package/forms/autocomplete/src/autocomplete.component.d.ts +93 -0
  544. package/forms/checkbox/index.d.ts +1 -0
  545. package/forms/checkbox/src/checkbox.component.d.ts +28 -0
  546. package/forms/chip/index.d.ts +1 -0
  547. package/forms/chip/src/chip.component.d.ts +71 -0
  548. package/forms/chip/src/index.d.ts +2 -0
  549. package/forms/chip/src/pipes/index.d.ts +1 -0
  550. package/forms/chip/src/pipes/removable-chip.pipe.d.ts +8 -0
  551. package/forms/chip-calendar/index.d.ts +1 -0
  552. package/forms/chip-calendar/src/chip-calendar.component.d.ts +71 -0
  553. package/forms/chip-calendar/src/pipes/index.d.ts +1 -0
  554. package/forms/chip-calendar/src/pipes/removable-chip.pipe.d.ts +8 -0
  555. package/forms/date/index.d.ts +1 -0
  556. package/forms/date/src/date.component.d.ts +79 -0
  557. package/forms/date-range/index.d.ts +1 -0
  558. package/forms/date-range/src/date-range.component.d.ts +81 -0
  559. package/forms/datetime/index.d.ts +1 -0
  560. package/forms/datetime/src/datetime.component.d.ts +89 -0
  561. package/forms/datetime/src/popup/sd-datetime-picker.component.d.ts +37 -0
  562. package/forms/datetime/src/popup/sd-time-spinner.component.d.ts +41 -0
  563. package/forms/directives/index.d.ts +4 -0
  564. package/forms/directives/src/item-def.directive.d.ts +13 -0
  565. package/forms/directives/src/label-def.directive.d.ts +9 -0
  566. package/forms/directives/src/suffix-def.directive.d.ts +9 -0
  567. package/forms/directives/src/view-def.directive.d.ts +14 -0
  568. package/forms/index.d.ts +17 -0
  569. package/forms/input/index.d.ts +1 -0
  570. package/forms/input/src/input.component.d.ts +84 -0
  571. package/forms/input-color/index.d.ts +1 -0
  572. package/forms/input-color/src/input-color.component.d.ts +32 -0
  573. package/forms/input-number/index.d.ts +1 -0
  574. package/forms/input-number/src/input-number.component.d.ts +95 -0
  575. package/forms/label/index.d.ts +1 -0
  576. package/forms/label/src/label.component.d.ts +13 -0
  577. package/forms/models/index.d.ts +6 -0
  578. package/forms/models/src/form-control-state.d.ts +23 -0
  579. package/forms/models/src/sd-custom-validator.model.d.ts +15 -0
  580. package/forms/models/src/sd-form-control.model.d.ts +21 -0
  581. package/forms/models/src/sd-form.configuration.d.ts +6 -0
  582. package/forms/models/src/sd-search.model.d.ts +7 -0
  583. package/forms/models/src/sd-selection-data.model.d.ts +7 -0
  584. package/forms/radio/index.d.ts +1 -0
  585. package/forms/radio/src/radio.component.d.ts +49 -0
  586. package/forms/select/index.d.ts +1 -0
  587. package/forms/select/src/select.component.d.ts +93 -0
  588. package/forms/src/forms.module.d.ts +14 -0
  589. package/forms/switch/index.d.ts +1 -0
  590. package/forms/switch/src/switch.component.d.ts +32 -0
  591. package/forms/textarea/index.d.ts +1 -0
  592. package/forms/textarea/src/textarea.component.d.ts +65 -0
  593. package/handlers/global-error.handler.d.ts +9 -0
  594. package/handlers/index.d.ts +1 -0
  595. package/i18n/index.d.ts +5 -0
  596. package/i18n/src/en.d.ts +449 -0
  597. package/i18n/src/i18n.messages.d.ts +2 -0
  598. package/i18n/src/i18n.pipe.d.ts +9 -0
  599. package/i18n/src/i18n.service.d.ts +14 -0
  600. package/i18n/src/i18n.token.d.ts +1 -0
  601. package/i18n/src/i18n.types.d.ts +31 -0
  602. package/i18n/src/ja.d.ts +2 -0
  603. package/i18n/src/ko.d.ts +2 -0
  604. package/i18n/src/vi.d.ts +2 -0
  605. package/i18n/src/zh.d.ts +2 -0
  606. package/index.d.ts +5 -0
  607. package/interceptors/index.d.ts +2 -0
  608. package/interceptors/no-internet/no-internet.interceptor.d.ts +14 -0
  609. package/interceptors/unauthorized/unauthorized.interceptor.d.ts +12 -0
  610. package/models/index.d.ts +1 -0
  611. package/models/src/language.model.d.ts +2 -0
  612. package/modules/auth/configurations/auth.configuration.d.ts +19 -0
  613. package/modules/auth/configurations/index.d.ts +1 -0
  614. package/modules/auth/guards/auth.guard.d.ts +11 -0
  615. package/modules/auth/guards/index.d.ts +2 -0
  616. package/modules/auth/guards/portal.guard.d.ts +11 -0
  617. package/modules/auth/index.d.ts +3 -0
  618. package/modules/auth/services/auth.model.d.ts +8 -0
  619. package/modules/auth/services/auth.service.d.ts +17 -0
  620. package/modules/auth/services/index.d.ts +2 -0
  621. package/modules/authom/authom.configuration.d.ts +17 -0
  622. package/modules/authom/authom.interceptor.d.ts +3 -0
  623. package/modules/authom/authom.module.d.ts +16 -0
  624. package/modules/authom/authom.service.d.ts +32 -0
  625. package/modules/authom/index.d.ts +35 -0
  626. package/modules/index.d.ts +5 -0
  627. package/modules/keycloak/index.d.ts +4 -0
  628. package/modules/keycloak/keycloak.configuration.d.ts +11 -0
  629. package/modules/keycloak/keycloak.interceptor.d.ts +2 -0
  630. package/modules/keycloak/keycloak.module.d.ts +18 -0
  631. package/modules/keycloak/keycloak.service.d.ts +14 -0
  632. package/modules/layout/components/index.d.ts +4 -0
  633. package/modules/layout/components/layout-main/layout-main.component.d.ts +12 -0
  634. package/modules/layout/components/page/page.component.d.ts +10 -0
  635. package/modules/layout/components/sidebar-mobile-v1/components/sidebar/sidebar.component.d.ts +35 -0
  636. package/modules/layout/components/sidebar-mobile-v1/components/user/user.component.d.ts +24 -0
  637. package/modules/layout/components/sidebar-mobile-v1/main.component.d.ts +21 -0
  638. package/modules/layout/components/sidebar-v1/components/sidebar/sidebar.component.d.ts +55 -0
  639. package/modules/layout/components/sidebar-v1/components/user/user.component.d.ts +22 -0
  640. package/modules/layout/components/sidebar-v1/main.component.d.ts +24 -0
  641. package/modules/layout/configurations/index.d.ts +1 -0
  642. package/modules/layout/configurations/layout.configuration.d.ts +67 -0
  643. package/modules/layout/index.d.ts +5 -0
  644. package/modules/layout/layout.module.d.ts +7 -0
  645. package/modules/layout/layout.routing.d.ts +2 -0
  646. package/modules/layout/modules/forbidden/forbidden.module.d.ts +8 -0
  647. package/modules/layout/modules/forbidden/forbidden.routing.d.ts +2 -0
  648. package/modules/layout/modules/forbidden/index.d.ts +1 -0
  649. package/modules/layout/modules/forbidden/pages/root/root.component.d.ts +10 -0
  650. package/modules/layout/modules/home/components/home-page/home-page.component.d.ts +33 -0
  651. package/modules/layout/modules/home/guards/home-redirect.guard.d.ts +11 -0
  652. package/modules/layout/modules/home/home.module.d.ts +8 -0
  653. package/modules/layout/modules/home/home.routing.d.ts +7 -0
  654. package/modules/layout/modules/home/index.d.ts +1 -0
  655. package/modules/layout/modules/index.d.ts +3 -0
  656. package/modules/layout/modules/not-found/index.d.ts +1 -0
  657. package/modules/layout/modules/not-found/not-found.module.d.ts +8 -0
  658. package/modules/layout/modules/not-found/not-found.routing.d.ts +2 -0
  659. package/modules/layout/modules/not-found/pages/root/root.component.d.ts +10 -0
  660. package/modules/layout/pipes/high-light-search.pipe.d.ts +10 -0
  661. package/modules/layout/pipes/index.d.ts +3 -0
  662. package/modules/layout/pipes/menu-focus.pipe.d.ts +10 -0
  663. package/modules/layout/pipes/menu.pipe.d.ts +13 -0
  664. package/modules/layout/services/index.d.ts +3 -0
  665. package/modules/layout/services/layout.service.d.ts +10 -0
  666. package/modules/layout/services/menu/menu.model.d.ts +24 -0
  667. package/modules/layout/services/storage/storage.service.d.ts +13 -0
  668. package/modules/permission/index.d.ts +4 -0
  669. package/modules/permission/src/configurations/index.d.ts +1 -0
  670. package/modules/permission/src/configurations/permission.configuration.d.ts +63 -0
  671. package/modules/permission/src/directives/index.d.ts +1 -0
  672. package/modules/permission/src/directives/permission.directive.d.ts +9 -0
  673. package/modules/permission/src/guards/index.d.ts +1 -0
  674. package/modules/permission/src/guards/permission.guard.d.ts +14 -0
  675. package/modules/permission/src/services/index.d.ts +1 -0
  676. package/modules/permission/src/services/permission.service.d.ts +12 -0
  677. package/package.json +344 -0
  678. package/pipes/index.d.ts +4 -0
  679. package/pipes/src/empty.pipe.d.ts +7 -0
  680. package/pipes/src/format-number.pipe.d.ts +11 -0
  681. package/pipes/src/safe-html.pipe.d.ts +11 -0
  682. package/pipes/src/time-different.pipe.d.ts +13 -0
  683. package/public-api.d.ts +10 -0
  684. package/services/api/index.d.ts +4 -0
  685. package/services/api/src/api.model.d.ts +36 -0
  686. package/services/api/src/api.module.d.ts +6 -0
  687. package/services/api/src/api.service.d.ts +19 -0
  688. package/services/api/src/interceptors/api.interceptor.d.ts +9 -0
  689. package/services/cache/index.d.ts +2 -0
  690. package/services/cache/src/cache.model.d.ts +30 -0
  691. package/services/cache/src/cache.service.d.ts +9 -0
  692. package/services/confirm/index.d.ts +1 -0
  693. package/services/confirm/src/lib/components/dialog-confirm/dialog-confirm.component.d.ts +46 -0
  694. package/services/confirm/src/lib/confirm.service.d.ts +55 -0
  695. package/services/confirm/src/public-api.d.ts +2 -0
  696. package/services/docx/index.d.ts +1 -0
  697. package/services/docx/src/lib/docx.model.d.ts +9 -0
  698. package/services/docx/src/lib/docx.service.d.ts +15 -0
  699. package/services/docx/src/lib/pandoc-core.d.ts +11 -0
  700. package/services/docx/src/public-api.d.ts +2 -0
  701. package/services/excel/index.d.ts +1 -0
  702. package/services/excel/src/lib/excel.model.d.ts +31 -0
  703. package/services/excel/src/lib/excel.service.d.ts +19 -0
  704. package/services/excel/src/public-api.d.ts +2 -0
  705. package/services/index.d.ts +8 -0
  706. package/services/license/index.d.ts +1 -0
  707. package/services/license/src/license.service.d.ts +10 -0
  708. package/services/loading/index.d.ts +1 -0
  709. package/services/loading/src/loading.service.d.ts +14 -0
  710. package/services/notify/index.d.ts +2 -0
  711. package/services/notify/src/components/toast/toast.component.d.ts +28 -0
  712. package/services/notify/src/components/toast-container.component.d.ts +8 -0
  713. package/services/notify/src/notify.model.d.ts +16 -0
  714. package/services/notify/src/notify.service.d.ts +20 -0
  715. package/services/storage/index.d.ts +2 -0
  716. package/services/storage/src/storage.model.d.ts +20 -0
  717. package/services/storage/src/storage.service.d.ts +10 -0
  718. package/utilities/data-state/index.d.ts +1 -0
  719. package/utilities/data-state/src/data-state.d.ts +11 -0
  720. package/utilities/extensions/index.d.ts +6 -0
  721. package/utilities/extensions/src/array.extension.d.ts +1 -0
  722. package/utilities/extensions/src/color.extension.d.ts +5 -0
  723. package/utilities/extensions/src/date.extension.d.ts +1 -0
  724. package/utilities/extensions/src/number.extension.d.ts +1 -0
  725. package/utilities/extensions/src/string.extension.d.ts +50 -0
  726. package/utilities/extensions/src/utility.extension.d.ts +31 -0
  727. package/utilities/index.d.ts +3 -0
  728. package/utilities/models/index.d.ts +12 -0
  729. package/utilities/models/src/color.model.d.ts +4 -0
  730. package/utilities/models/src/empty.model.d.ts +4 -0
  731. package/utilities/models/src/filter.model.d.ts +12 -0
  732. package/utilities/models/src/icon.model.d.ts +2 -0
  733. package/utilities/models/src/maybe-async.model.d.ts +10 -0
  734. package/utilities/models/src/nested-key-of.model.d.ts +4 -0
  735. package/utilities/models/src/operator.model.d.ts +13 -0
  736. package/utilities/models/src/order.model.d.ts +4 -0
  737. package/utilities/models/src/paging.model.d.ts +8 -0
  738. package/utilities/models/src/pattern.model.d.ts +21 -0
  739. package/utilities/models/src/size.model.d.ts +4 -0
  740. package/utilities/models/src/unwrap-signal.model.d.ts +6 -0
@@ -0,0 +1,1987 @@
1
+ import * as i1 from '@angular/common';
2
+ import { CommonModule } from '@angular/common';
3
+ import * as i0 from '@angular/core';
4
+ import { inject, ElementRef, DestroyRef, input, computed, output, signal, effect, afterNextRender, HostListener, ChangeDetectionStrategy, Component, InjectionToken, viewChild, viewChildren } from '@angular/core';
5
+ import * as i2 from '@angular/material/icon';
6
+ import { MatIconModule } from '@angular/material/icon';
7
+ import { TranslatePipe } from '@sdcorejs/angular/i18n';
8
+ import * as uuid from 'uuid';
9
+ import { FormsModule } from '@angular/forms';
10
+ import * as pdfjsLib from 'pdfjs-dist';
11
+
12
+ var _a$1;
13
+ class SdPreviewImage {
14
+ // ==========================================
15
+ // CONSTANTS
16
+ // ==========================================
17
+ // Zoom giới hạn theo handoff: stage hiển thị từ 25% → 400% là dải hữu dụng.
18
+ // Ngoài dải này ảnh hoặc quá nhỏ để thao tác hoặc đã pixelated nặng.
19
+ static MIN_ZOOM = 0.25;
20
+ static MAX_ZOOM = 4;
21
+ static ZOOM_STEP = 0.1;
22
+ static SWIPE_THRESHOLD = 40;
23
+ // ==========================================
24
+ // DI
25
+ // ==========================================
26
+ #hostEl = inject(ElementRef);
27
+ #destroyRef = inject(DestroyRef);
28
+ // ==========================================
29
+ // INPUTS (signal-based, Angular 19 style)
30
+ // ==========================================
31
+ // Declarative items input — consumer drives state. Mỗi lần items() đổi,
32
+ // effect() bên dưới sẽ normalize + load lại danh sách.
33
+ // autoId prefix `preview-image-`. Per-element child autoIds derive `${autoId}-{suffix}`.
34
+ autoIdInput = input(undefined, { alias: 'autoId' });
35
+ autoId = computed(() => (this.autoIdInput() ? `components-preview-image-${this.autoIdInput()}` : undefined));
36
+ #childAutoId = (suffix) => {
37
+ return this.autoId() ? `${this.autoId()}-${suffix}` : undefined;
38
+ };
39
+ autoIdPrev = computed(() => this.#childAutoId('prev'));
40
+ autoIdNext = computed(() => this.#childAutoId('next'));
41
+ autoIdZoomIn = computed(() => this.#childAutoId('zoom-in'));
42
+ autoIdZoomOut = computed(() => this.#childAutoId('zoom-out'));
43
+ autoIdFit = computed(() => this.#childAutoId('fit'));
44
+ autoIdRotate = computed(() => this.#childAutoId('rotate'));
45
+ autoIdDownload = computed(() => this.#childAutoId('download'));
46
+ autoIdFullscreen = computed(() => this.#childAutoId('fullscreen'));
47
+ autoIdRetry = computed(() => this.#childAutoId('retry'));
48
+ autoIdThumb = (i) => this.#childAutoId(`thumb-${i}`);
49
+ autoIdDot = (i) => this.#childAutoId(`dot-${i}`);
50
+ items = input([]);
51
+ title = input(undefined);
52
+ thumbnailPosition = input('bottom');
53
+ showToolbar = input(true);
54
+ downloadable = input(true);
55
+ zoomable = input(true);
56
+ loop = input(true);
57
+ startIndex = input(0);
58
+ // Theme variant — drives [data-theme] host attribute. Default 'dark' preserves
59
+ // pre-existing visual contract; consumers opt-in to 'light' explicitly.
60
+ theme = input('dark');
61
+ // ==========================================
62
+ // OUTPUTS
63
+ // ==========================================
64
+ // `close`: user requested dismissal (X button hoặc Esc). Consumer tự quyết
65
+ // định đóng modal / điều hướng / hide section.
66
+ close = output();
67
+ activeIndexChange = output();
68
+ download = output();
69
+ imageError = output();
70
+ // ==========================================
71
+ // STATE (signals)
72
+ // ==========================================
73
+ #activeIndex = signal(0);
74
+ #images = signal([]);
75
+ #zoom = signal(1);
76
+ #rotation = signal(0);
77
+ #pan = signal({ x: 0, y: 0 });
78
+ #stage = signal('empty');
79
+ #isFullscreen = signal(false);
80
+ #isDragging = signal(false);
81
+ // Track every blob URL we created so we can revoke on destroy / re-normalize.
82
+ // CDN strings hoá ra File→blob URL nội bộ — bắt buộc phải revoke để không leak.
83
+ #ownedBlobUrls = new Set();
84
+ // Token chống race: nếu items() đổi liên tiếp (user click rất nhanh) thì
85
+ // promise normalize của lần trước phải bị bỏ qua khi resolve.
86
+ #loadToken = 0;
87
+ // Pointer state for drag/pinch — không cần signal vì không drive template.
88
+ #dragStart = null;
89
+ #swipeStart = null;
90
+ #activePointers = new Map();
91
+ #pinchStartDistance = 0;
92
+ #pinchStartZoom = 1;
93
+ // ==========================================
94
+ // COMPUTED (template-readable)
95
+ // ==========================================
96
+ activeIndex = this.#activeIndex.asReadonly();
97
+ images = this.#images.asReadonly();
98
+ zoom = this.#zoom.asReadonly();
99
+ rotation = this.#rotation.asReadonly();
100
+ pan = this.#pan.asReadonly();
101
+ stage = this.#stage.asReadonly();
102
+ isFullscreen = this.#isFullscreen.asReadonly();
103
+ activeImage = computed(() => this.#images()[this.#activeIndex()]);
104
+ hasPrev = computed(() => {
105
+ const len = this.#images().length;
106
+ return len > 1 && (this.loop() || this.#activeIndex() > 0);
107
+ });
108
+ hasNext = computed(() => {
109
+ const len = this.#images().length;
110
+ return len > 1 && (this.loop() || this.#activeIndex() < len - 1);
111
+ });
112
+ zoomPercent = computed(() => Math.round(this.#zoom() * 100));
113
+ // Transform style cho ảnh stage. WHY: gom translate/scale/rotate vào 1 string
114
+ // duy nhất để OnPush phát hiện thay đổi qua signal kéo theo computed.
115
+ imageTransform = computed(() => {
116
+ const p = this.#pan();
117
+ const z = this.#zoom();
118
+ const r = this.#rotation();
119
+ return `translate(${p.x}px, ${p.y}px) scale(${z}) rotate(${r}deg)`;
120
+ });
121
+ shellClass = computed(() => {
122
+ const pos = this.thumbnailPosition();
123
+ return {
124
+ 'sd-preview-shell': true,
125
+ [`sd-preview-shell--pos-${pos}`]: true,
126
+ 'sd-preview-shell--fullscreen': this.#isFullscreen(),
127
+ };
128
+ });
129
+ // ==========================================
130
+ // CONSTRUCTOR — declarative wiring
131
+ // ==========================================
132
+ constructor() {
133
+ // Đồng bộ #isFullscreen với trạng thái thực của browser (sự kiện 'fullscreenchange' fire
134
+ // bất kể user thoát fullscreen bằng cách nào: nút Thoát, Esc, F11, programmatic).
135
+ const onFullscreenChange = () => {
136
+ this.#isFullscreen.set(document.fullscreenElement === this.#hostEl.nativeElement);
137
+ };
138
+ document.addEventListener('fullscreenchange', onFullscreenChange);
139
+ this.#destroyRef.onDestroy(() => {
140
+ document.removeEventListener('fullscreenchange', onFullscreenChange);
141
+ });
142
+ // Reactive normalize: mỗi lần items() đổi → revoke blob cũ → load mới.
143
+ // WHY effect không async: effect() callback không hỗ trợ async cleanup,
144
+ // dùng token-based race guard trong #normalizeAndLoad để bỏ qua kết quả cũ.
145
+ effect(() => {
146
+ const list = this.items();
147
+ const start = this.startIndex();
148
+ this.#normalizeAndLoad(list, start);
149
+ });
150
+ // Auto-focus host sau lần render đầu để keyboard hoạt động ngay.
151
+ // WHY: nếu consumer KHÔNG bọc trong modal, không có ai focus giúp.
152
+ // Consumer bọc modal sẽ tự focus trap riêng — focus() này vô hại (no-op
153
+ // nếu element không visible).
154
+ afterNextRender(() => {
155
+ try {
156
+ this.#hostEl.nativeElement.focus({ preventScroll: true });
157
+ }
158
+ catch {
159
+ // ignore — focus() can throw on detached/hidden elements.
160
+ }
161
+ });
162
+ // Cleanup khi component bị destroy.
163
+ this.#destroyRef.onDestroy(() => {
164
+ this.#revokeAllBlobs();
165
+ if (document.fullscreenElement === this.#hostEl.nativeElement) {
166
+ // Tránh để fullscreen "treo" khi component bị huỷ giữa chừng.
167
+ document.exitFullscreen?.().catch(() => undefined);
168
+ }
169
+ });
170
+ }
171
+ // ==========================================
172
+ // LIFECYCLE
173
+ // ==========================================
174
+ ngOnDestroy() {
175
+ // Giữ ngOnDestroy để vẫn tương thích với khai báo `implements OnDestroy`.
176
+ // Logic dọn dẹp đã được dời sang DestroyRef.onDestroy ở constructor để có
177
+ // thể chia sẻ với effect() teardown nếu cần sau này.
178
+ }
179
+ // ==========================================
180
+ // PUBLIC API
181
+ // ==========================================
182
+ onClickThumbnailImage(index) {
183
+ if (index < 0 || index >= this.#images().length)
184
+ return;
185
+ if (index === this.#activeIndex())
186
+ return;
187
+ this.#setActive(index);
188
+ }
189
+ updateCurrentImage(direction) {
190
+ const len = this.#images().length;
191
+ if (len <= 1)
192
+ return;
193
+ const next = this.#activeIndex() + direction;
194
+ let target;
195
+ if (this.loop()) {
196
+ target = (next + len) % len;
197
+ }
198
+ else {
199
+ if (next < 0 || next >= len)
200
+ return;
201
+ target = next;
202
+ }
203
+ this.#setActive(target);
204
+ }
205
+ zoomIn() {
206
+ if (!this.zoomable())
207
+ return;
208
+ this.#setZoom(this.#zoom() + _a$1.ZOOM_STEP);
209
+ }
210
+ zoomOut() {
211
+ if (!this.zoomable())
212
+ return;
213
+ this.#setZoom(this.#zoom() - _a$1.ZOOM_STEP);
214
+ }
215
+ fitToScreen() {
216
+ this.#resetTransform();
217
+ }
218
+ rotate(direction) {
219
+ const delta = direction === 'right' ? 90 : -90;
220
+ // Wrap về [0, 360) cho gọn — không strictly cần thiết về mặt visual.
221
+ this.#rotation.update(r => ((r + delta) % 360 + 360) % 360);
222
+ }
223
+ downloadCurrent() {
224
+ if (!this.downloadable())
225
+ return;
226
+ const img = this.activeImage();
227
+ if (!img || img.error)
228
+ return;
229
+ // Ưu tiên CDN URL nếu có để tận dụng Content-Disposition của server,
230
+ // fallback về blob URL khi user upload File trực tiếp.
231
+ const href = img.url || img.blobUrl;
232
+ const a = document.createElement('a');
233
+ a.href = href;
234
+ a.download = img.name || 'image';
235
+ document.body.appendChild(a);
236
+ a.click();
237
+ a.remove();
238
+ this.download.emit({ index: this.#activeIndex(), item: img });
239
+ }
240
+ toggleFullscreen() {
241
+ // WHY: trạng thái #isFullscreen được sync qua sự kiện 'fullscreenchange' (xem constructor)
242
+ // — single source of truth. Không cần .then(set(true/false)) ở đây nữa.
243
+ if (!document.fullscreenElement) {
244
+ this.#hostEl.nativeElement.requestFullscreen?.().catch(() => undefined);
245
+ }
246
+ else {
247
+ document.exitFullscreen?.().catch(() => undefined);
248
+ }
249
+ }
250
+ /** Retry just the active image (Artboard G action). */
251
+ async retryActive() {
252
+ const idx = this.#activeIndex();
253
+ const img = this.#images()[idx];
254
+ if (!img || !img.url)
255
+ return;
256
+ // Per-image loading: chỉ đánh dấu record này loading, KHÔNG đụng stage().
257
+ // Nhờ vậy nav arrows + thumbnails vẫn render trong lúc retry, không flicker.
258
+ this.#images.update(arr => {
259
+ const next = [...arr];
260
+ next[idx] = { ...arr[idx], loading: true, error: false };
261
+ return next;
262
+ });
263
+ const refreshed = await this.#normalize(img.url, { name: img.name, caption: img.caption, alt: img.alt });
264
+ if (refreshed) {
265
+ // Trao đổi nguyên record để giữ id thumbnail (avoid track-by reset).
266
+ this.#images.update(arr => {
267
+ const next = [...arr];
268
+ // Revoke blob cũ TRƯỚC khi thay để không leak record vừa fail tải.
269
+ if (this.#ownedBlobUrls.has(arr[idx].blobUrl)) {
270
+ URL.revokeObjectURL(arr[idx].blobUrl);
271
+ this.#ownedBlobUrls.delete(arr[idx].blobUrl);
272
+ }
273
+ next[idx] = { ...refreshed, id: img.id, loading: false };
274
+ return next;
275
+ });
276
+ }
277
+ else {
278
+ // Normalize trả null → giữ record cũ nhưng tắt loading + mark error
279
+ // để inline placeholder hiển thị (không lật cả stage).
280
+ this.#images.update(arr => {
281
+ const next = [...arr];
282
+ next[idx] = { ...arr[idx], loading: false, error: true };
283
+ return next;
284
+ });
285
+ }
286
+ }
287
+ /** User clicked the X button — emit close intent for the consumer to react. */
288
+ requestClose() {
289
+ this.close.emit();
290
+ }
291
+ // ==========================================
292
+ // KEYBOARD — bound to HOST (not document)
293
+ // ==========================================
294
+ // WHY @HostListener('keydown') on host: component có tabindex=0 nên nhận được
295
+ // keyboard event khi nó hoặc descendant đang focused. Không bind document để
296
+ // tránh "cướp" phím tắt của các phần khác trong app khi component không hoạt động.
297
+ onKeyDown(event) {
298
+ // Bỏ qua khi user đang gõ trong input — toolbar / overlay có thể chứa
299
+ // input ẩn (search), không muốn ← → cướp event của họ.
300
+ const target = event.target;
301
+ if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) {
302
+ return;
303
+ }
304
+ switch (event.key) {
305
+ case 'ArrowLeft':
306
+ event.preventDefault();
307
+ this.updateCurrentImage(-1);
308
+ break;
309
+ case 'ArrowRight':
310
+ event.preventDefault();
311
+ this.updateCurrentImage(1);
312
+ break;
313
+ case '+':
314
+ case '=':
315
+ if (this.zoomable()) {
316
+ event.preventDefault();
317
+ this.zoomIn();
318
+ }
319
+ break;
320
+ case '-':
321
+ if (this.zoomable()) {
322
+ event.preventDefault();
323
+ this.zoomOut();
324
+ }
325
+ break;
326
+ case '0':
327
+ event.preventDefault();
328
+ this.fitToScreen();
329
+ break;
330
+ case 'r':
331
+ event.preventDefault();
332
+ this.rotate('right');
333
+ break;
334
+ case 'R':
335
+ event.preventDefault();
336
+ this.rotate('left');
337
+ break;
338
+ case 'f':
339
+ event.preventDefault();
340
+ this.toggleFullscreen();
341
+ break;
342
+ case 'd':
343
+ if (this.downloadable()) {
344
+ event.preventDefault();
345
+ this.downloadCurrent();
346
+ }
347
+ break;
348
+ }
349
+ }
350
+ // ==========================================
351
+ // MOUSE / POINTER HANDLERS (template-bound)
352
+ // ==========================================
353
+ onWheel(event) {
354
+ if (!this.zoomable())
355
+ return;
356
+ event.preventDefault();
357
+ // deltaY < 0 nghĩa là cuộn lên = zoom in (giống Google Photos / macOS Preview).
358
+ const factor = event.deltaY < 0 ? 1 + _a$1.ZOOM_STEP : 1 - _a$1.ZOOM_STEP;
359
+ this.#setZoom(this.#zoom() * factor);
360
+ }
361
+ onPointerDown(event) {
362
+ if (event.button !== 0 && event.pointerType === 'mouse')
363
+ return;
364
+ // Bỏ qua nếu pointer xuất phát trên control tương tác (toolbar / nav / dots / retry button).
365
+ // WHY: stage chứa các button con; setPointerCapture đi cùng drag/swipe logic sẽ
366
+ // "ăn" click event của những button đó (như nút Thoát fullscreen / zoom / rotate).
367
+ if (event.target.closest('button, [role="button"]'))
368
+ return;
369
+ event.target.setPointerCapture?.(event.pointerId);
370
+ this.#activePointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
371
+ if (this.#activePointers.size === 2) {
372
+ // Bắt đầu pinch — khoá zoom hiện tại làm anchor.
373
+ const pts = [...this.#activePointers.values()];
374
+ this.#pinchStartDistance = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
375
+ this.#pinchStartZoom = this.#zoom();
376
+ this.#dragStart = null;
377
+ this.#swipeStart = null;
378
+ return;
379
+ }
380
+ if (this.#zoom() > 1) {
381
+ // Đã zoom → pointer drag = pan, không phải swipe.
382
+ this.#isDragging.set(true);
383
+ this.#dragStart = {
384
+ x: event.clientX,
385
+ y: event.clientY,
386
+ panX: this.#pan().x,
387
+ panY: this.#pan().y,
388
+ };
389
+ }
390
+ else {
391
+ // Ở mức 100% → pointer drag được coi là swipe sang ảnh kế.
392
+ this.#swipeStart = { x: event.clientX, y: event.clientY, t: performance.now() };
393
+ }
394
+ }
395
+ onPointerMove(event) {
396
+ if (!this.#activePointers.has(event.pointerId))
397
+ return;
398
+ this.#activePointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
399
+ if (this.#activePointers.size === 2 && this.zoomable() && this.#pinchStartDistance > 0) {
400
+ const pts = [...this.#activePointers.values()];
401
+ const dist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
402
+ const ratio = dist / this.#pinchStartDistance;
403
+ this.#setZoom(this.#pinchStartZoom * ratio);
404
+ return;
405
+ }
406
+ if (this.#dragStart && this.#zoom() > 1) {
407
+ const dx = event.clientX - this.#dragStart.x;
408
+ const dy = event.clientY - this.#dragStart.y;
409
+ this.#pan.set({ x: this.#dragStart.panX + dx, y: this.#dragStart.panY + dy });
410
+ }
411
+ }
412
+ onPointerUp(event) {
413
+ const start = this.#swipeStart;
414
+ this.#activePointers.delete(event.pointerId);
415
+ if (this.#activePointers.size < 2) {
416
+ this.#pinchStartDistance = 0;
417
+ }
418
+ if (this.#dragStart) {
419
+ this.#dragStart = null;
420
+ this.#isDragging.set(false);
421
+ }
422
+ // Detect swipe — chỉ khi đang ở fit/100% và di chuyển ngang đủ xa.
423
+ if (start && this.#zoom() <= 1) {
424
+ const dx = event.clientX - start.x;
425
+ const dy = event.clientY - start.y;
426
+ if (Math.abs(dx) >= _a$1.SWIPE_THRESHOLD && Math.abs(dx) > Math.abs(dy) * 1.5) {
427
+ this.updateCurrentImage(dx < 0 ? 1 : -1);
428
+ }
429
+ }
430
+ this.#swipeStart = null;
431
+ }
432
+ onPointerCancel(event) {
433
+ this.#activePointers.delete(event.pointerId);
434
+ this.#dragStart = null;
435
+ this.#swipeStart = null;
436
+ this.#isDragging.set(false);
437
+ if (this.#activePointers.size < 2) {
438
+ this.#pinchStartDistance = 0;
439
+ }
440
+ }
441
+ onImageDblClick() {
442
+ if (!this.zoomable())
443
+ return;
444
+ if (this.#zoom() === 1) {
445
+ this.#setZoom(2);
446
+ }
447
+ else {
448
+ this.#resetTransform();
449
+ }
450
+ }
451
+ onImageError() {
452
+ const idx = this.#activeIndex();
453
+ const img = this.#images()[idx];
454
+ if (!img)
455
+ return;
456
+ this.#images.update(arr => {
457
+ const next = [...arr];
458
+ next[idx] = { ...img, error: true, loading: false };
459
+ return next;
460
+ });
461
+ // WHY không set stage='error': lỗi từng ảnh không nên ẩn nav/dots/thumb của
462
+ // cả viewer. Template render placeholder inline ngay trong khung ảnh khi
463
+ // activeImage().error === true — user vẫn có thể chuyển sang ảnh khác.
464
+ this.imageError.emit({ index: idx, reason: 'render-failed' });
465
+ }
466
+ trackByImage(_i, item) {
467
+ return item.id;
468
+ }
469
+ formatBytes(bytes) {
470
+ if (!bytes || bytes < 0)
471
+ return '';
472
+ if (bytes < 1024)
473
+ return `${bytes} B`;
474
+ if (bytes < 1024 * 1024)
475
+ return `${(bytes / 1024).toFixed(1)} KB`;
476
+ if (bytes < 1024 * 1024 * 1024)
477
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
478
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
479
+ }
480
+ // ==========================================
481
+ // INTERNALS
482
+ // ==========================================
483
+ async #normalizeAndLoad(items, startIndex) {
484
+ const token = ++this.#loadToken;
485
+ // Revoke trước khi map mới — items() đổi liên tiếp sẽ leak nếu không.
486
+ this.#revokeAllBlobs();
487
+ this.#resetTransform();
488
+ if (!Array.isArray(items) || items.length === 0) {
489
+ this.#images.set([]);
490
+ this.#activeIndex.set(0);
491
+ this.#stage.set('empty');
492
+ return;
493
+ }
494
+ this.#activeIndex.set(Math.max(0, Math.min(startIndex ?? 0, items.length - 1)));
495
+ this.#stage.set('loading');
496
+ const normalized = await Promise.all(items.map(item => this.#normalize(item)));
497
+ // Race guard: nếu items() đã đổi trong lúc await thì kết quả này là stale.
498
+ // Lưu ý: blob URL của batch này đã add vào #ownedBlobUrls — batch mới sẽ
499
+ // revoke chúng khi nó cũng vào #normalizeAndLoad.
500
+ if (token !== this.#loadToken) {
501
+ return;
502
+ }
503
+ const valid = normalized.filter((img) => img !== null);
504
+ this.#images.set(valid);
505
+ if (valid.length === 0) {
506
+ this.#stage.set('empty');
507
+ }
508
+ else {
509
+ const idx = Math.min(this.#activeIndex(), valid.length - 1);
510
+ this.#activeIndex.set(idx);
511
+ // WHY luôn 'ready' kể cả khi ảnh đầu bị error: per-image error render
512
+ // inline để giữ nav + thumb interactive. User vẫn điều hướng được sang
513
+ // ảnh khác.
514
+ this.#stage.set('ready');
515
+ }
516
+ }
517
+ #setActive(index) {
518
+ this.#activeIndex.set(index);
519
+ this.#resetTransform();
520
+ // WHY luôn set 'ready' (không phụ thuộc img.error): per-image error giờ
521
+ // render inline trong stage thay vì lật cả stage sang 'error'. Giữ stage
522
+ // 'ready' để nav/dots/thumbnail/toolbar không bị ẩn.
523
+ this.#stage.set('ready');
524
+ this.activeIndexChange.emit(index);
525
+ // Auto-scroll thumbnail strip — đợi 1 microtask để DOM rendered xong.
526
+ // WHY scoped query: thumbnail id là global ('sd-preview-thumb-N'); nếu có
527
+ // > 1 instance trên page sẽ collide. Query trong nativeElement để isolate.
528
+ queueMicrotask(() => {
529
+ const el = this.#hostEl.nativeElement.querySelector(`#sd-preview-thumb-${index}`);
530
+ el?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
531
+ });
532
+ }
533
+ #setZoom(value) {
534
+ const clamped = Math.min(_a$1.MAX_ZOOM, Math.max(_a$1.MIN_ZOOM, value));
535
+ this.#zoom.set(clamped);
536
+ if (clamped <= 1) {
537
+ // Khi về fit/100% — pan không còn ý nghĩa, reset để không bị "lệch" khi
538
+ // zoom lại lần sau.
539
+ this.#pan.set({ x: 0, y: 0 });
540
+ }
541
+ }
542
+ #resetTransform() {
543
+ this.#zoom.set(1);
544
+ this.#rotation.set(0);
545
+ this.#pan.set({ x: 0, y: 0 });
546
+ }
547
+ #revokeAllBlobs() {
548
+ for (const url of this.#ownedBlobUrls) {
549
+ URL.revokeObjectURL(url);
550
+ }
551
+ this.#ownedBlobUrls.clear();
552
+ }
553
+ async #normalize(item, override) {
554
+ try {
555
+ if (typeof item === 'string') {
556
+ return await this.#fromUrl(item, override);
557
+ }
558
+ if (item instanceof File) {
559
+ return this.#fromFile(item, override);
560
+ }
561
+ if (item && typeof item === 'object') {
562
+ // Object form: chấp nhận file hoặc url, ưu tiên file vì không cần network.
563
+ if (item.file) {
564
+ return this.#fromFile(item.file, { name: item.name, caption: item.caption, alt: item.alt });
565
+ }
566
+ if (item.url) {
567
+ return await this.#fromUrl(item.url, {
568
+ name: item.name,
569
+ caption: item.caption,
570
+ alt: item.alt,
571
+ });
572
+ }
573
+ }
574
+ return null;
575
+ }
576
+ catch (err) {
577
+ console.error('[sd-preview-image] normalize failed', err);
578
+ return null;
579
+ }
580
+ }
581
+ async #fromUrl(url, override) {
582
+ const id = uuid.v4();
583
+ // Lấy filename từ phần path cuối — bỏ query string.
584
+ const baseSrc = url.split('?')[0];
585
+ const inferredName = override?.name || baseSrc.substring(baseSrc.lastIndexOf('/') + 1) || 'image';
586
+ try {
587
+ const res = await fetch(url);
588
+ if (!res.ok)
589
+ throw new Error(`HTTP ${res.status}`);
590
+ const blob = await res.blob();
591
+ const file = new File([blob], inferredName, { type: blob.type });
592
+ const blobUrl = URL.createObjectURL(file);
593
+ this.#ownedBlobUrls.add(blobUrl);
594
+ return {
595
+ id,
596
+ url,
597
+ blobUrl,
598
+ name: inferredName,
599
+ size: file.size,
600
+ caption: override?.caption,
601
+ alt: override?.alt,
602
+ loading: false,
603
+ error: false,
604
+ };
605
+ }
606
+ catch {
607
+ // Vẫn trả về record để Artboard G có thể hiển thị Retry — error=true sẽ
608
+ // được stage signal chuyển sang trạng thái lỗi khi đây là active image.
609
+ return {
610
+ id,
611
+ url,
612
+ blobUrl: '',
613
+ name: inferredName,
614
+ size: 0,
615
+ caption: override?.caption,
616
+ alt: override?.alt,
617
+ loading: false,
618
+ error: true,
619
+ };
620
+ }
621
+ }
622
+ #fromFile(file, override) {
623
+ if (!file.type.startsWith('image/')) {
624
+ // Silently filter — handoff yêu cầu non-image File bị drop yên lặng để
625
+ // tương thích với upload form nơi user trộn lẫn ảnh và tài liệu.
626
+ return null;
627
+ }
628
+ const blobUrl = URL.createObjectURL(file);
629
+ this.#ownedBlobUrls.add(blobUrl);
630
+ return {
631
+ id: uuid.v4(),
632
+ blobUrl,
633
+ name: override?.name || file.name,
634
+ size: file.size,
635
+ caption: override?.caption,
636
+ alt: override?.alt,
637
+ loading: false,
638
+ error: false,
639
+ };
640
+ }
641
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdPreviewImage, deps: [], target: i0.ɵɵFactoryTarget.Component });
642
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.21", type: SdPreviewImage, isStandalone: true, selector: "sd-preview-image", inputs: { autoIdInput: { classPropertyName: "autoIdInput", publicName: "autoId", isSignal: true, isRequired: false, transformFunction: null }, items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, thumbnailPosition: { classPropertyName: "thumbnailPosition", publicName: "thumbnailPosition", isSignal: true, isRequired: false, transformFunction: null }, showToolbar: { classPropertyName: "showToolbar", publicName: "showToolbar", isSignal: true, isRequired: false, transformFunction: null }, downloadable: { classPropertyName: "downloadable", publicName: "downloadable", isSignal: true, isRequired: false, transformFunction: null }, zoomable: { classPropertyName: "zoomable", publicName: "zoomable", isSignal: true, isRequired: false, transformFunction: null }, loop: { classPropertyName: "loop", publicName: "loop", isSignal: true, isRequired: false, transformFunction: null }, startIndex: { classPropertyName: "startIndex", publicName: "startIndex", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { close: "close", activeIndexChange: "activeIndexChange", download: "download", imageError: "imageError" }, host: { attributes: { "tabindex": "0" }, listeners: { "keydown": "onKeyDown($event)" }, properties: { "attr.data-theme": "theme()", "class.sd-preview-image-host--fullscreen": "isFullscreen()" }, classAttribute: "sd-preview-image-host" }, ngImport: i0, template: "<!--\n Self-contained viewer shell. Component sizes to its parent (width:100%;\n height:100%) \u2014 consumer decides whether to wrap in <sd-modal>,\n <sd-side-drawer>, a page section, or use inline.\n-->\n<div [ngClass]=\"shellClass()\" role=\"region\">\n\n <!-- ============================================\n HEADER BAR\n ============================================ -->\n @if (stage() !== 'empty' && activeImage(); as img) {\n <div class=\"sd-preview-header\">\n <div class=\"sd-preview-header__left\">\n <mat-icon class=\"sd-preview-header__icon\" fontIcon=\"image\"></mat-icon>\n <div class=\"sd-preview-header__meta\">\n <div class=\"sd-preview-header__name T14M\" [title]=\"img.name\">{{ img.name }}</div>\n @if (img.size) {\n <div class=\"sd-preview-header__sub\">\n {{ formatBytes(img.size) }}\n </div>\n }\n </div>\n </div>\n\n @if (images().length > 1) {\n <div class=\"sd-preview-header__counter\" aria-live=\"polite\">\n {{ activeIndex() + 1 }} / {{ images().length }}\n </div>\n }\n </div>\n } @else if (title()) {\n <!-- Optional override title even on empty state -->\n <div class=\"sd-preview-header sd-preview-header--simple\">\n <div class=\"sd-preview-header__name T14M\">{{ title() }}</div>\n </div>\n }\n\n <!-- ============================================\n BODY (stage + thumbnails per layout)\n ============================================ -->\n <div class=\"sd-preview-body\">\n\n <!-- STAGE ............................................................... -->\n <div\n class=\"sd-preview-stage\"\n [class.sd-preview-stage--dragging]=\"zoom() > 1\"\n (wheel)=\"onWheel($event)\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerCancel($event)\"\n (dblclick)=\"onImageDblClick()\">\n\n @switch (stage()) {\n @case ('empty') {\n <div class=\"sd-preview-status\">\n <div class=\"sd-preview-status__tile sd-preview-status__tile--muted\">\n <mat-icon fontIcon=\"image_not_supported\"></mat-icon>\n </div>\n <div class=\"sd-preview-status__title\">\n {{ 'core.component.preview-image.no-image' | translate }}\n </div>\n <div class=\"sd-preview-status__hint\">\n {{ 'core.component.preview-image.no-image-hint' | translate }}\n </div>\n </div>\n }\n @case ('loading') {\n <div class=\"sd-preview-status\">\n <div class=\"sd-preview-spinner\" role=\"progressbar\"></div>\n <div class=\"sd-preview-status__hint\">\n {{ 'core.component.preview-image.loading' | translate }}\n </div>\n </div>\n }\n @default {\n @if (activeImage(); as img) {\n @if (img.loading) {\n <!-- Per-image loading (retry inline) \u2014 gi\u1EEF nav/dots/thumbs visible -->\n <div class=\"sd-preview-image-loading\" role=\"status\"\n [attr.aria-label]=\"'core.component.preview-image.loading' | translate\">\n <div class=\"sd-preview-spinner\"></div>\n <div class=\"sd-preview-status__hint\">\n {{ 'core.component.preview-image.loading' | translate }}\n </div>\n </div>\n } @else if (img.error) {\n <!-- Inline broken-image placeholder \u2014 gi\u1EEF nav/dots/thumbs visible -->\n <div class=\"sd-preview-image-broken\" role=\"img\"\n [attr.aria-label]=\"'core.component.preview-image.load-error' | translate\">\n <div class=\"sd-preview-status__tile sd-preview-status__tile--danger\">\n <mat-icon fontIcon=\"broken_image\"></mat-icon>\n </div>\n <div class=\"sd-preview-status__title\">\n {{ 'core.component.preview-image.load-error' | translate }}\n </div>\n <div class=\"sd-preview-status__hint\">\n {{ 'core.component.preview-image.load-error-hint' | translate }}\n </div>\n <button type=\"button\" class=\"sd-preview-btn-primary\" (click)=\"retryActive()\" [attr.data-autoId]=\"autoIdRetry()\">\n <mat-icon fontIcon=\"refresh\"></mat-icon>\n <span>{{ 'core.component.preview-image.retry' | translate }}</span>\n </button>\n </div>\n } @else {\n <img\n class=\"sd-preview-image\"\n [src]=\"img.blobUrl || img.url\"\n [alt]=\"img.alt || img.name\"\n [style.transform]=\"imageTransform()\"\n (error)=\"onImageError()\"\n draggable=\"false\" />\n\n @if (img.caption) {\n <div class=\"sd-preview-caption\">{{ img.caption }}</div>\n }\n }\n }\n }\n }\n\n <!-- KEYBOARD HINT ............................................. -->\n @if (stage() === 'ready' && images().length > 1) {\n <div class=\"sd-preview-kbd-hint\" aria-hidden=\"true\">\n <span class=\"sd-preview-kbd\">\u2190</span>\n <span class=\"sd-preview-kbd\">\u2192</span>\n <span class=\"sd-preview-kbd-text\">{{ 'core.component.preview-image.kbd-hint-nav' | translate }}</span>\n </div>\n }\n\n <!-- NAV ARROWS ............................................. -->\n @if (stage() === 'ready' && images().length > 1) {\n <button\n type=\"button\"\n class=\"sd-preview-nav sd-preview-nav--prev\"\n [disabled]=\"!hasPrev()\"\n [attr.aria-label]=\"'core.component.preview-image.prev' | translate\"\n [attr.data-autoId]=\"autoIdPrev()\"\n (click)=\"updateCurrentImage(-1)\">\n <mat-icon fontIcon=\"chevron_left\"></mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"sd-preview-nav sd-preview-nav--next\"\n [disabled]=\"!hasNext()\"\n [attr.aria-label]=\"'core.component.preview-image.next' | translate\"\n [attr.data-autoId]=\"autoIdNext()\"\n (click)=\"updateCurrentImage(1)\">\n <mat-icon fontIcon=\"chevron_right\"></mat-icon>\n </button>\n }\n\n <!-- DOTS INDICATOR ......................................... -->\n @if (thumbnailPosition() === 'dots' && stage() === 'ready' && images().length > 1) {\n <div class=\"sd-preview-dots\" role=\"tablist\">\n @for (img of images(); track img.id; let i = $index) {\n <button\n type=\"button\"\n class=\"sd-preview-dot\"\n [class.sd-preview-dot--active]=\"i === activeIndex()\"\n [attr.aria-label]=\"'core.component.preview-image.next' | translate\"\n [attr.data-autoId]=\"autoIdDot(i)\"\n (click)=\"onClickThumbnailImage(i)\"></button>\n }\n </div>\n }\n\n <!-- FLOATING TOOLBAR ....................................... -->\n @if (showToolbar() && stage() === 'ready') {\n <div class=\"sd-preview-toolbar\" role=\"toolbar\">\n @if (zoomable()) {\n <button\n type=\"button\"\n class=\"sd-preview-toolbar__btn\"\n [attr.aria-label]=\"'core.component.preview-image.zoom-out' | translate\"\n [attr.title]=\"'core.component.preview-image.zoom-out' | translate\"\n [attr.data-autoId]=\"autoIdZoomOut()\"\n (click)=\"zoomOut()\">\n <mat-icon fontIcon=\"zoom_out\"></mat-icon>\n </button>\n <span class=\"sd-preview-toolbar__readout\">{{ zoomPercent() }}%</span>\n <button\n type=\"button\"\n class=\"sd-preview-toolbar__btn\"\n [attr.aria-label]=\"'core.component.preview-image.zoom-in' | translate\"\n [attr.title]=\"'core.component.preview-image.zoom-in' | translate\"\n [attr.data-autoId]=\"autoIdZoomIn()\"\n (click)=\"zoomIn()\">\n <mat-icon fontIcon=\"zoom_in\"></mat-icon>\n </button>\n <span class=\"sd-preview-toolbar__divider\"></span>\n }\n <button\n type=\"button\"\n class=\"sd-preview-toolbar__btn sd-preview-toolbar__btn--with-label\"\n [attr.aria-label]=\"'core.component.preview-image.fit' | translate\"\n [attr.data-autoId]=\"autoIdFit()\"\n (click)=\"fitToScreen()\">\n <mat-icon fontIcon=\"fit_screen\"></mat-icon>\n <span class=\"sd-preview-toolbar__btn-label\">{{ 'core.component.preview-image.fit' | translate }}</span>\n </button>\n <button\n type=\"button\"\n class=\"sd-preview-toolbar__btn\"\n [attr.aria-label]=\"'core.component.preview-image.rotate' | translate\"\n [attr.title]=\"'core.component.preview-image.rotate' | translate\"\n [attr.data-autoId]=\"autoIdRotate()\"\n (click)=\"rotate('right')\">\n <mat-icon fontIcon=\"rotate_right\"></mat-icon>\n </button>\n <span class=\"sd-preview-toolbar__divider\"></span>\n @if (downloadable()) {\n <button\n type=\"button\"\n class=\"sd-preview-toolbar__btn\"\n [attr.aria-label]=\"'core.component.preview-image.download' | translate\"\n [attr.title]=\"'core.component.preview-image.download' | translate\"\n [attr.data-autoId]=\"autoIdDownload()\"\n (click)=\"downloadCurrent()\">\n <mat-icon fontIcon=\"download\"></mat-icon>\n </button>\n }\n <button\n type=\"button\"\n class=\"sd-preview-toolbar__btn\"\n [attr.aria-label]=\"'core.component.preview-image.fullscreen' | translate\"\n [attr.title]=\"'core.component.preview-image.fullscreen' | translate\"\n [attr.data-autoId]=\"autoIdFullscreen()\"\n (click)=\"toggleFullscreen()\">\n <mat-icon [fontIcon]=\"isFullscreen() ? 'fullscreen_exit' : 'fullscreen'\"></mat-icon>\n </button>\n </div>\n }\n </div>\n\n <!-- THUMBNAIL STRIP .................................................... -->\n @if (\n stage() === 'ready' &&\n images().length > 1 &&\n thumbnailPosition() !== 'none' &&\n thumbnailPosition() !== 'dots'\n ) {\n <div\n class=\"sd-preview-thumbs\"\n [class.sd-preview-thumbs--vertical]=\"thumbnailPosition() === 'left' || thumbnailPosition() === 'right'\">\n @for (img of images(); track trackByImage($index, img); let i = $index) {\n <button\n type=\"button\"\n class=\"sd-preview-thumb\"\n [class.sd-preview-thumb--active]=\"i === activeIndex()\"\n [id]=\"'sd-preview-thumb-' + i\"\n [attr.aria-label]=\"img.name\"\n [attr.aria-current]=\"i === activeIndex() ? 'true' : null\"\n [attr.data-autoId]=\"autoIdThumb(i)\"\n (click)=\"onClickThumbnailImage(i)\">\n @if (img.error) {\n <div class=\"sd-preview-thumb__broken\">\n <mat-icon fontIcon=\"broken_image\"></mat-icon>\n </div>\n } @else {\n <img class=\"sd-preview-thumb__img\" [src]=\"img.blobUrl || img.url\" [alt]=\"img.alt || img.name\" />\n }\n <span class=\"sd-preview-thumb__badge\">{{ i + 1 }}</span>\n @if (thumbnailPosition() === 'right' || thumbnailPosition() === 'left') {\n <span class=\"sd-preview-thumb__name\" [title]=\"img.name\">{{ img.name }}</span>\n }\n </button>\n }\n </div>\n }\n </div>\n</div>\n", styles: [":host{--sd-preview-shell-bg: #0d0e10;--sd-preview-shell-bg-2: #17191c;--sd-preview-panel-bg: rgba(15, 17, 20, .92);--sd-preview-divider: rgba(255, 255, 255, .08);--sd-preview-fg: #f5f6f7;--sd-preview-fg-muted: rgba(255, 255, 255, .6);--sd-preview-fg-subtle: rgba(255, 255, 255, .4);--sd-preview-accent: var(--sd-primary, #3b82f6);--sd-preview-accent-soft: rgba(59, 130, 246, .2);--sd-preview-danger: var(--sd-error, #ef4444);--sd-preview-success: var(--sd-success, #22c55e);--sd-preview-danger-soft: rgba(239, 68, 68, .1);--sd-preview-danger-border: rgba(239, 68, 68, .3);--sd-preview-hover-bg: rgba(255, 255, 255, .1);display:block;width:100%;height:100%;min-height:320px;outline:none}:host([data-theme=light]){--sd-preview-shell-bg: #f7f8fa;--sd-preview-shell-bg-2: #ffffff;--sd-preview-panel-bg: rgba(255, 255, 255, .94);--sd-preview-divider: rgba(0, 0, 0, .08);--sd-preview-fg: #1a1c1f;--sd-preview-fg-muted: rgba(0, 0, 0, .62);--sd-preview-fg-subtle: rgba(0, 0, 0, .42);--sd-preview-accent: var(--sd-primary, #1f49bf);--sd-preview-accent-soft: rgba(31, 73, 191, .18);--sd-preview-danger: var(--sd-error, #ef4444);--sd-preview-success: var(--sd-success, #22c55e);--sd-preview-danger-soft: rgba(239, 68, 68, .08);--sd-preview-danger-border: rgba(239, 68, 68, .25);--sd-preview-hover-bg: rgba(0, 0, 0, .06)}.sd-preview-shell{position:relative;display:flex;flex-direction:column;width:100%;height:100%;background:var(--sd-preview-shell-bg);color:var(--sd-preview-fg);border-radius:12px;overflow:hidden;box-shadow:0 30px 80px #0000008c,0 0 0 1px #ffffff0a;font-family:inherit}.sd-preview-shell--fullscreen{height:100vh;width:100vw;border-radius:0}:host(.sd-preview-image-host--fullscreen){width:100vw;height:100vh;min-height:0}.sd-preview-header{position:relative;display:flex;align-items:center;height:68px;padding:0 16px;background:var(--sd-preview-panel-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--sd-preview-divider);flex:0 0 auto;z-index:2}.sd-preview-header--simple{justify-content:space-between}.sd-preview-header__left{display:flex;align-items:center;gap:12px;min-width:0;flex:1 1 auto}.sd-preview-header__icon{color:var(--sd-preview-fg-muted);font-size:20px;width:20px;height:20px}.sd-preview-header__meta{display:flex;flex-direction:column;min-width:0}.sd-preview-header__name{color:var(--sd-preview-fg);max-width:360px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-preview-header__sub{font-size:12px;color:var(--sd-preview-fg-muted);margin-top:2px}.sd-preview-header__counter{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-size:13px;font-weight:500;color:var(--sd-preview-fg-muted);background:#ffffff14;padding:4px 12px;border-radius:999px;font-variant-numeric:tabular-nums}.sd-preview-body{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow:hidden}.sd-preview-shell--pos-bottom .sd-preview-body{flex-direction:column}.sd-preview-shell--pos-top .sd-preview-body{flex-direction:column-reverse}.sd-preview-shell--pos-right .sd-preview-body{flex-direction:row}.sd-preview-shell--pos-left .sd-preview-body{flex-direction:row-reverse}.sd-preview-shell--pos-dots .sd-preview-body,.sd-preview-shell--pos-none .sd-preview-body{flex-direction:column}.sd-preview-stage{position:relative;flex:1 1 auto;min-height:0;min-width:0;background:var(--sd-preview-shell-bg);background-image:linear-gradient(45deg,rgba(255,255,255,.02) 25%,transparent 25%),linear-gradient(-45deg,rgba(255,255,255,.02) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(255,255,255,.02) 75%),linear-gradient(-45deg,transparent 75%,rgba(255,255,255,.02) 75%);background-size:24px 24px;background-position:0 0,0 12px,12px -12px,-12px 0;display:flex;align-items:center;justify-content:center;overflow:hidden;touch-action:none;-webkit-user-select:none;user-select:none}.sd-preview-stage--dragging{cursor:grab}:host([data-theme=light]) .sd-preview-stage{background-image:linear-gradient(45deg,rgba(0,0,0,.015) 25%,transparent 25%),linear-gradient(-45deg,rgba(0,0,0,.015) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(0,0,0,.015) 75%),linear-gradient(-45deg,transparent 75%,rgba(0,0,0,.015) 75%)}.sd-preview-image{max-width:100%;max-height:100%;object-fit:contain;transform-origin:center center;transition:transform .2s ease;will-change:transform;pointer-events:none}.sd-preview-caption{position:absolute;bottom:96px;left:50%;transform:translate(-50%);max-width:70%;padding:6px 12px;background:#0f1114b3;color:var(--sd-preview-fg);border-radius:8px;font-size:13px;text-align:center}.sd-preview-image-broken,.sd-preview-image-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;width:100%;max-width:480px;padding:24px;text-align:center}.sd-preview-nav{position:absolute;top:50%;transform:translateY(-50%);width:48px;height:48px;border-radius:999px;border:none;background:#14141699;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);color:var(--sd-preview-fg);display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 4px 16px #0000004d;transition:background .12s;z-index:3}.sd-preview-nav mat-icon{font-size:28px;width:28px;height:28px}.sd-preview-nav:hover:not(:disabled){background:#ffffff2e}.sd-preview-nav:focus-visible{outline:2px solid var(--sd-preview-accent);outline-offset:2px}.sd-preview-nav:disabled{background:#ffffff0a;opacity:.35;cursor:not-allowed}.sd-preview-nav--prev{left:16px}.sd-preview-nav--next{right:16px}.sd-preview-toolbar{position:absolute;bottom:24px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:4px;padding:6px 8px;background:var(--sd-preview-panel-bg);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border:1px solid var(--sd-preview-divider);border-radius:12px;box-shadow:0 8px 30px #0006;z-index:3}.sd-preview-toolbar__btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-width:36px;height:36px;padding:0 8px;background:transparent;border:none;color:var(--sd-preview-fg);border-radius:8px;cursor:pointer;transition:background .12s}.sd-preview-toolbar__btn mat-icon{font-size:20px;width:20px;height:20px}.sd-preview-toolbar__btn--with-label{padding:0 12px}.sd-preview-toolbar__btn:hover:not(:disabled){background:var(--sd-preview-hover-bg)}.sd-preview-toolbar__btn:focus-visible{outline:2px solid var(--sd-preview-accent);outline-offset:2px}.sd-preview-toolbar__btn-label{font-size:13px;font-weight:500}.sd-preview-toolbar__readout{min-width:56px;text-align:center;color:var(--sd-preview-fg);font-size:13px;font-weight:500;font-variant-numeric:tabular-nums}.sd-preview-toolbar__divider{width:1px;height:20px;background:var(--sd-preview-divider);margin:0 4px}.sd-preview-thumbs{display:flex;gap:8px;padding:12px;background:var(--sd-preview-shell-bg-2);overflow:auto;flex:0 0 auto;flex-direction:row;height:116px}.sd-preview-thumbs--vertical{flex-direction:column;width:152px;height:100%}.sd-preview-thumb{position:relative;flex:0 0 auto;width:120px;height:76px;padding:0;border:none;background:transparent;border-radius:8px;outline:1px solid var(--sd-preview-divider);outline-offset:-1px;opacity:.7;overflow:hidden;cursor:pointer;transition:opacity .12s,outline-color .12s}.sd-preview-thumb:hover{opacity:1}.sd-preview-thumb--active{opacity:1;outline:2px solid var(--sd-preview-accent);outline-offset:-2px;box-shadow:inset 0 0 0 2px var(--sd-preview-accent)}.sd-preview-thumb:focus-visible{outline:2px solid var(--sd-preview-accent);outline-offset:2px}.sd-preview-thumb__img{width:100%;height:100%;object-fit:cover;display:block;pointer-events:none}.sd-preview-thumb__broken{display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:var(--sd-preview-danger-soft);color:var(--sd-preview-danger);pointer-events:none}.sd-preview-thumb__broken mat-icon{font-size:24px;width:24px;height:24px}.sd-preview-thumb__badge{position:absolute;top:4px;right:4px;background:#000000b3;color:#fff;font-size:10px;font-weight:600;padding:1px 5px;border-radius:4px;pointer-events:none}.sd-preview-thumb__name{position:absolute;left:0;right:0;bottom:0;padding:14px 6px 4px;color:var(--sd-preview-fg);font-size:11px;line-height:1.3;background:linear-gradient(to top,rgba(0,0,0,.85),transparent);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;pointer-events:none}.sd-preview-thumbs--vertical .sd-preview-thumb{width:128px}.sd-preview-dots{position:absolute;bottom:24px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:6px;z-index:3}.sd-preview-dot{width:8px;height:8px;border:none;background:#ffffff4d;border-radius:999px;padding:0;cursor:pointer;transition:width .18s,background .18s}.sd-preview-dot--active{width:24px;background:#f5f6f7}.sd-preview-dot:focus-visible{outline:2px solid var(--sd-preview-accent);outline-offset:2px}.sd-preview-status{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;text-align:center;padding:24px}.sd-preview-status__tile{width:96px;height:96px;border-radius:24px;display:flex;align-items:center;justify-content:center;border:1px solid var(--sd-preview-divider);background:#ffffff0d}.sd-preview-status__tile mat-icon{font-size:48px;width:48px;height:48px;color:var(--sd-preview-fg-subtle)}.sd-preview-status__tile--muted{background:#ffffff0d;border-color:var(--sd-preview-divider)}.sd-preview-status__tile--danger{background:var(--sd-preview-danger-soft);border-color:var(--sd-preview-danger-border)}.sd-preview-status__tile--danger mat-icon{color:var(--sd-preview-danger)}.sd-preview-status__title{font-size:16px;font-weight:500;color:var(--sd-preview-fg)}.sd-preview-status__hint{font-size:13px;color:var(--sd-preview-fg-muted);max-width:320px}.sd-preview-spinner{width:56px;height:56px;border-radius:999px;border:3px solid rgba(255,255,255,.08);border-top-color:var(--sd-preview-accent);animation:sd-preview-spin .9s linear infinite}@keyframes sd-preview-spin{to{transform:rotate(360deg)}}.sd-preview-btn-primary{display:inline-flex;align-items:center;gap:6px;height:36px;padding:0 16px;background:var(--sd-preview-accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:13px;font-weight:500;transition:filter .12s}.sd-preview-btn-primary mat-icon{font-size:18px;width:18px;height:18px}.sd-preview-btn-primary:hover{filter:brightness(1.1)}.sd-preview-btn-primary:focus-visible{outline:2px solid #fff;outline-offset:2px}.sd-preview-kbd-hint{position:absolute;top:16px;left:16px;display:flex;align-items:center;gap:6px;z-index:2;pointer-events:none}.sd-preview-kbd{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 5px;border:1px solid var(--sd-preview-divider);background:#ffffff0a;color:var(--sd-preview-fg-muted);border-radius:4px;font:500 11px/1 ui-monospace,SFMono-Regular,Menlo,monospace}.sd-preview-kbd-text,.sd-preview-kbd-dot{font-size:11px;color:var(--sd-preview-fg-subtle)}.sd-preview-kbd-dot{margin:0 2px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
643
+ }
644
+ _a$1 = SdPreviewImage;
645
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdPreviewImage, decorators: [{
646
+ type: Component,
647
+ args: [{ selector: 'sd-preview-image', standalone: true, imports: [CommonModule, MatIconModule, TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, host: {
648
+ tabindex: '0',
649
+ class: 'sd-preview-image-host',
650
+ // WHY data-theme attribute (not class): SCSS uses :host([data-theme="light"])
651
+ // to swap CSS custom properties — keeps the theme variant declarative and
652
+ // visible in devtools without polluting the class list.
653
+ '[attr.data-theme]': 'theme()',
654
+ '[class.sd-preview-image-host--fullscreen]': 'isFullscreen()',
655
+ }, template: "<!--\n Self-contained viewer shell. Component sizes to its parent (width:100%;\n height:100%) \u2014 consumer decides whether to wrap in <sd-modal>,\n <sd-side-drawer>, a page section, or use inline.\n-->\n<div [ngClass]=\"shellClass()\" role=\"region\">\n\n <!-- ============================================\n HEADER BAR\n ============================================ -->\n @if (stage() !== 'empty' && activeImage(); as img) {\n <div class=\"sd-preview-header\">\n <div class=\"sd-preview-header__left\">\n <mat-icon class=\"sd-preview-header__icon\" fontIcon=\"image\"></mat-icon>\n <div class=\"sd-preview-header__meta\">\n <div class=\"sd-preview-header__name T14M\" [title]=\"img.name\">{{ img.name }}</div>\n @if (img.size) {\n <div class=\"sd-preview-header__sub\">\n {{ formatBytes(img.size) }}\n </div>\n }\n </div>\n </div>\n\n @if (images().length > 1) {\n <div class=\"sd-preview-header__counter\" aria-live=\"polite\">\n {{ activeIndex() + 1 }} / {{ images().length }}\n </div>\n }\n </div>\n } @else if (title()) {\n <!-- Optional override title even on empty state -->\n <div class=\"sd-preview-header sd-preview-header--simple\">\n <div class=\"sd-preview-header__name T14M\">{{ title() }}</div>\n </div>\n }\n\n <!-- ============================================\n BODY (stage + thumbnails per layout)\n ============================================ -->\n <div class=\"sd-preview-body\">\n\n <!-- STAGE ............................................................... -->\n <div\n class=\"sd-preview-stage\"\n [class.sd-preview-stage--dragging]=\"zoom() > 1\"\n (wheel)=\"onWheel($event)\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerCancel($event)\"\n (dblclick)=\"onImageDblClick()\">\n\n @switch (stage()) {\n @case ('empty') {\n <div class=\"sd-preview-status\">\n <div class=\"sd-preview-status__tile sd-preview-status__tile--muted\">\n <mat-icon fontIcon=\"image_not_supported\"></mat-icon>\n </div>\n <div class=\"sd-preview-status__title\">\n {{ 'core.component.preview-image.no-image' | translate }}\n </div>\n <div class=\"sd-preview-status__hint\">\n {{ 'core.component.preview-image.no-image-hint' | translate }}\n </div>\n </div>\n }\n @case ('loading') {\n <div class=\"sd-preview-status\">\n <div class=\"sd-preview-spinner\" role=\"progressbar\"></div>\n <div class=\"sd-preview-status__hint\">\n {{ 'core.component.preview-image.loading' | translate }}\n </div>\n </div>\n }\n @default {\n @if (activeImage(); as img) {\n @if (img.loading) {\n <!-- Per-image loading (retry inline) \u2014 gi\u1EEF nav/dots/thumbs visible -->\n <div class=\"sd-preview-image-loading\" role=\"status\"\n [attr.aria-label]=\"'core.component.preview-image.loading' | translate\">\n <div class=\"sd-preview-spinner\"></div>\n <div class=\"sd-preview-status__hint\">\n {{ 'core.component.preview-image.loading' | translate }}\n </div>\n </div>\n } @else if (img.error) {\n <!-- Inline broken-image placeholder \u2014 gi\u1EEF nav/dots/thumbs visible -->\n <div class=\"sd-preview-image-broken\" role=\"img\"\n [attr.aria-label]=\"'core.component.preview-image.load-error' | translate\">\n <div class=\"sd-preview-status__tile sd-preview-status__tile--danger\">\n <mat-icon fontIcon=\"broken_image\"></mat-icon>\n </div>\n <div class=\"sd-preview-status__title\">\n {{ 'core.component.preview-image.load-error' | translate }}\n </div>\n <div class=\"sd-preview-status__hint\">\n {{ 'core.component.preview-image.load-error-hint' | translate }}\n </div>\n <button type=\"button\" class=\"sd-preview-btn-primary\" (click)=\"retryActive()\" [attr.data-autoId]=\"autoIdRetry()\">\n <mat-icon fontIcon=\"refresh\"></mat-icon>\n <span>{{ 'core.component.preview-image.retry' | translate }}</span>\n </button>\n </div>\n } @else {\n <img\n class=\"sd-preview-image\"\n [src]=\"img.blobUrl || img.url\"\n [alt]=\"img.alt || img.name\"\n [style.transform]=\"imageTransform()\"\n (error)=\"onImageError()\"\n draggable=\"false\" />\n\n @if (img.caption) {\n <div class=\"sd-preview-caption\">{{ img.caption }}</div>\n }\n }\n }\n }\n }\n\n <!-- KEYBOARD HINT ............................................. -->\n @if (stage() === 'ready' && images().length > 1) {\n <div class=\"sd-preview-kbd-hint\" aria-hidden=\"true\">\n <span class=\"sd-preview-kbd\">\u2190</span>\n <span class=\"sd-preview-kbd\">\u2192</span>\n <span class=\"sd-preview-kbd-text\">{{ 'core.component.preview-image.kbd-hint-nav' | translate }}</span>\n </div>\n }\n\n <!-- NAV ARROWS ............................................. -->\n @if (stage() === 'ready' && images().length > 1) {\n <button\n type=\"button\"\n class=\"sd-preview-nav sd-preview-nav--prev\"\n [disabled]=\"!hasPrev()\"\n [attr.aria-label]=\"'core.component.preview-image.prev' | translate\"\n [attr.data-autoId]=\"autoIdPrev()\"\n (click)=\"updateCurrentImage(-1)\">\n <mat-icon fontIcon=\"chevron_left\"></mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"sd-preview-nav sd-preview-nav--next\"\n [disabled]=\"!hasNext()\"\n [attr.aria-label]=\"'core.component.preview-image.next' | translate\"\n [attr.data-autoId]=\"autoIdNext()\"\n (click)=\"updateCurrentImage(1)\">\n <mat-icon fontIcon=\"chevron_right\"></mat-icon>\n </button>\n }\n\n <!-- DOTS INDICATOR ......................................... -->\n @if (thumbnailPosition() === 'dots' && stage() === 'ready' && images().length > 1) {\n <div class=\"sd-preview-dots\" role=\"tablist\">\n @for (img of images(); track img.id; let i = $index) {\n <button\n type=\"button\"\n class=\"sd-preview-dot\"\n [class.sd-preview-dot--active]=\"i === activeIndex()\"\n [attr.aria-label]=\"'core.component.preview-image.next' | translate\"\n [attr.data-autoId]=\"autoIdDot(i)\"\n (click)=\"onClickThumbnailImage(i)\"></button>\n }\n </div>\n }\n\n <!-- FLOATING TOOLBAR ....................................... -->\n @if (showToolbar() && stage() === 'ready') {\n <div class=\"sd-preview-toolbar\" role=\"toolbar\">\n @if (zoomable()) {\n <button\n type=\"button\"\n class=\"sd-preview-toolbar__btn\"\n [attr.aria-label]=\"'core.component.preview-image.zoom-out' | translate\"\n [attr.title]=\"'core.component.preview-image.zoom-out' | translate\"\n [attr.data-autoId]=\"autoIdZoomOut()\"\n (click)=\"zoomOut()\">\n <mat-icon fontIcon=\"zoom_out\"></mat-icon>\n </button>\n <span class=\"sd-preview-toolbar__readout\">{{ zoomPercent() }}%</span>\n <button\n type=\"button\"\n class=\"sd-preview-toolbar__btn\"\n [attr.aria-label]=\"'core.component.preview-image.zoom-in' | translate\"\n [attr.title]=\"'core.component.preview-image.zoom-in' | translate\"\n [attr.data-autoId]=\"autoIdZoomIn()\"\n (click)=\"zoomIn()\">\n <mat-icon fontIcon=\"zoom_in\"></mat-icon>\n </button>\n <span class=\"sd-preview-toolbar__divider\"></span>\n }\n <button\n type=\"button\"\n class=\"sd-preview-toolbar__btn sd-preview-toolbar__btn--with-label\"\n [attr.aria-label]=\"'core.component.preview-image.fit' | translate\"\n [attr.data-autoId]=\"autoIdFit()\"\n (click)=\"fitToScreen()\">\n <mat-icon fontIcon=\"fit_screen\"></mat-icon>\n <span class=\"sd-preview-toolbar__btn-label\">{{ 'core.component.preview-image.fit' | translate }}</span>\n </button>\n <button\n type=\"button\"\n class=\"sd-preview-toolbar__btn\"\n [attr.aria-label]=\"'core.component.preview-image.rotate' | translate\"\n [attr.title]=\"'core.component.preview-image.rotate' | translate\"\n [attr.data-autoId]=\"autoIdRotate()\"\n (click)=\"rotate('right')\">\n <mat-icon fontIcon=\"rotate_right\"></mat-icon>\n </button>\n <span class=\"sd-preview-toolbar__divider\"></span>\n @if (downloadable()) {\n <button\n type=\"button\"\n class=\"sd-preview-toolbar__btn\"\n [attr.aria-label]=\"'core.component.preview-image.download' | translate\"\n [attr.title]=\"'core.component.preview-image.download' | translate\"\n [attr.data-autoId]=\"autoIdDownload()\"\n (click)=\"downloadCurrent()\">\n <mat-icon fontIcon=\"download\"></mat-icon>\n </button>\n }\n <button\n type=\"button\"\n class=\"sd-preview-toolbar__btn\"\n [attr.aria-label]=\"'core.component.preview-image.fullscreen' | translate\"\n [attr.title]=\"'core.component.preview-image.fullscreen' | translate\"\n [attr.data-autoId]=\"autoIdFullscreen()\"\n (click)=\"toggleFullscreen()\">\n <mat-icon [fontIcon]=\"isFullscreen() ? 'fullscreen_exit' : 'fullscreen'\"></mat-icon>\n </button>\n </div>\n }\n </div>\n\n <!-- THUMBNAIL STRIP .................................................... -->\n @if (\n stage() === 'ready' &&\n images().length > 1 &&\n thumbnailPosition() !== 'none' &&\n thumbnailPosition() !== 'dots'\n ) {\n <div\n class=\"sd-preview-thumbs\"\n [class.sd-preview-thumbs--vertical]=\"thumbnailPosition() === 'left' || thumbnailPosition() === 'right'\">\n @for (img of images(); track trackByImage($index, img); let i = $index) {\n <button\n type=\"button\"\n class=\"sd-preview-thumb\"\n [class.sd-preview-thumb--active]=\"i === activeIndex()\"\n [id]=\"'sd-preview-thumb-' + i\"\n [attr.aria-label]=\"img.name\"\n [attr.aria-current]=\"i === activeIndex() ? 'true' : null\"\n [attr.data-autoId]=\"autoIdThumb(i)\"\n (click)=\"onClickThumbnailImage(i)\">\n @if (img.error) {\n <div class=\"sd-preview-thumb__broken\">\n <mat-icon fontIcon=\"broken_image\"></mat-icon>\n </div>\n } @else {\n <img class=\"sd-preview-thumb__img\" [src]=\"img.blobUrl || img.url\" [alt]=\"img.alt || img.name\" />\n }\n <span class=\"sd-preview-thumb__badge\">{{ i + 1 }}</span>\n @if (thumbnailPosition() === 'right' || thumbnailPosition() === 'left') {\n <span class=\"sd-preview-thumb__name\" [title]=\"img.name\">{{ img.name }}</span>\n }\n </button>\n }\n </div>\n }\n </div>\n</div>\n", styles: [":host{--sd-preview-shell-bg: #0d0e10;--sd-preview-shell-bg-2: #17191c;--sd-preview-panel-bg: rgba(15, 17, 20, .92);--sd-preview-divider: rgba(255, 255, 255, .08);--sd-preview-fg: #f5f6f7;--sd-preview-fg-muted: rgba(255, 255, 255, .6);--sd-preview-fg-subtle: rgba(255, 255, 255, .4);--sd-preview-accent: var(--sd-primary, #3b82f6);--sd-preview-accent-soft: rgba(59, 130, 246, .2);--sd-preview-danger: var(--sd-error, #ef4444);--sd-preview-success: var(--sd-success, #22c55e);--sd-preview-danger-soft: rgba(239, 68, 68, .1);--sd-preview-danger-border: rgba(239, 68, 68, .3);--sd-preview-hover-bg: rgba(255, 255, 255, .1);display:block;width:100%;height:100%;min-height:320px;outline:none}:host([data-theme=light]){--sd-preview-shell-bg: #f7f8fa;--sd-preview-shell-bg-2: #ffffff;--sd-preview-panel-bg: rgba(255, 255, 255, .94);--sd-preview-divider: rgba(0, 0, 0, .08);--sd-preview-fg: #1a1c1f;--sd-preview-fg-muted: rgba(0, 0, 0, .62);--sd-preview-fg-subtle: rgba(0, 0, 0, .42);--sd-preview-accent: var(--sd-primary, #1f49bf);--sd-preview-accent-soft: rgba(31, 73, 191, .18);--sd-preview-danger: var(--sd-error, #ef4444);--sd-preview-success: var(--sd-success, #22c55e);--sd-preview-danger-soft: rgba(239, 68, 68, .08);--sd-preview-danger-border: rgba(239, 68, 68, .25);--sd-preview-hover-bg: rgba(0, 0, 0, .06)}.sd-preview-shell{position:relative;display:flex;flex-direction:column;width:100%;height:100%;background:var(--sd-preview-shell-bg);color:var(--sd-preview-fg);border-radius:12px;overflow:hidden;box-shadow:0 30px 80px #0000008c,0 0 0 1px #ffffff0a;font-family:inherit}.sd-preview-shell--fullscreen{height:100vh;width:100vw;border-radius:0}:host(.sd-preview-image-host--fullscreen){width:100vw;height:100vh;min-height:0}.sd-preview-header{position:relative;display:flex;align-items:center;height:68px;padding:0 16px;background:var(--sd-preview-panel-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--sd-preview-divider);flex:0 0 auto;z-index:2}.sd-preview-header--simple{justify-content:space-between}.sd-preview-header__left{display:flex;align-items:center;gap:12px;min-width:0;flex:1 1 auto}.sd-preview-header__icon{color:var(--sd-preview-fg-muted);font-size:20px;width:20px;height:20px}.sd-preview-header__meta{display:flex;flex-direction:column;min-width:0}.sd-preview-header__name{color:var(--sd-preview-fg);max-width:360px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-preview-header__sub{font-size:12px;color:var(--sd-preview-fg-muted);margin-top:2px}.sd-preview-header__counter{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-size:13px;font-weight:500;color:var(--sd-preview-fg-muted);background:#ffffff14;padding:4px 12px;border-radius:999px;font-variant-numeric:tabular-nums}.sd-preview-body{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow:hidden}.sd-preview-shell--pos-bottom .sd-preview-body{flex-direction:column}.sd-preview-shell--pos-top .sd-preview-body{flex-direction:column-reverse}.sd-preview-shell--pos-right .sd-preview-body{flex-direction:row}.sd-preview-shell--pos-left .sd-preview-body{flex-direction:row-reverse}.sd-preview-shell--pos-dots .sd-preview-body,.sd-preview-shell--pos-none .sd-preview-body{flex-direction:column}.sd-preview-stage{position:relative;flex:1 1 auto;min-height:0;min-width:0;background:var(--sd-preview-shell-bg);background-image:linear-gradient(45deg,rgba(255,255,255,.02) 25%,transparent 25%),linear-gradient(-45deg,rgba(255,255,255,.02) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(255,255,255,.02) 75%),linear-gradient(-45deg,transparent 75%,rgba(255,255,255,.02) 75%);background-size:24px 24px;background-position:0 0,0 12px,12px -12px,-12px 0;display:flex;align-items:center;justify-content:center;overflow:hidden;touch-action:none;-webkit-user-select:none;user-select:none}.sd-preview-stage--dragging{cursor:grab}:host([data-theme=light]) .sd-preview-stage{background-image:linear-gradient(45deg,rgba(0,0,0,.015) 25%,transparent 25%),linear-gradient(-45deg,rgba(0,0,0,.015) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(0,0,0,.015) 75%),linear-gradient(-45deg,transparent 75%,rgba(0,0,0,.015) 75%)}.sd-preview-image{max-width:100%;max-height:100%;object-fit:contain;transform-origin:center center;transition:transform .2s ease;will-change:transform;pointer-events:none}.sd-preview-caption{position:absolute;bottom:96px;left:50%;transform:translate(-50%);max-width:70%;padding:6px 12px;background:#0f1114b3;color:var(--sd-preview-fg);border-radius:8px;font-size:13px;text-align:center}.sd-preview-image-broken,.sd-preview-image-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;width:100%;max-width:480px;padding:24px;text-align:center}.sd-preview-nav{position:absolute;top:50%;transform:translateY(-50%);width:48px;height:48px;border-radius:999px;border:none;background:#14141699;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);color:var(--sd-preview-fg);display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 4px 16px #0000004d;transition:background .12s;z-index:3}.sd-preview-nav mat-icon{font-size:28px;width:28px;height:28px}.sd-preview-nav:hover:not(:disabled){background:#ffffff2e}.sd-preview-nav:focus-visible{outline:2px solid var(--sd-preview-accent);outline-offset:2px}.sd-preview-nav:disabled{background:#ffffff0a;opacity:.35;cursor:not-allowed}.sd-preview-nav--prev{left:16px}.sd-preview-nav--next{right:16px}.sd-preview-toolbar{position:absolute;bottom:24px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:4px;padding:6px 8px;background:var(--sd-preview-panel-bg);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border:1px solid var(--sd-preview-divider);border-radius:12px;box-shadow:0 8px 30px #0006;z-index:3}.sd-preview-toolbar__btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-width:36px;height:36px;padding:0 8px;background:transparent;border:none;color:var(--sd-preview-fg);border-radius:8px;cursor:pointer;transition:background .12s}.sd-preview-toolbar__btn mat-icon{font-size:20px;width:20px;height:20px}.sd-preview-toolbar__btn--with-label{padding:0 12px}.sd-preview-toolbar__btn:hover:not(:disabled){background:var(--sd-preview-hover-bg)}.sd-preview-toolbar__btn:focus-visible{outline:2px solid var(--sd-preview-accent);outline-offset:2px}.sd-preview-toolbar__btn-label{font-size:13px;font-weight:500}.sd-preview-toolbar__readout{min-width:56px;text-align:center;color:var(--sd-preview-fg);font-size:13px;font-weight:500;font-variant-numeric:tabular-nums}.sd-preview-toolbar__divider{width:1px;height:20px;background:var(--sd-preview-divider);margin:0 4px}.sd-preview-thumbs{display:flex;gap:8px;padding:12px;background:var(--sd-preview-shell-bg-2);overflow:auto;flex:0 0 auto;flex-direction:row;height:116px}.sd-preview-thumbs--vertical{flex-direction:column;width:152px;height:100%}.sd-preview-thumb{position:relative;flex:0 0 auto;width:120px;height:76px;padding:0;border:none;background:transparent;border-radius:8px;outline:1px solid var(--sd-preview-divider);outline-offset:-1px;opacity:.7;overflow:hidden;cursor:pointer;transition:opacity .12s,outline-color .12s}.sd-preview-thumb:hover{opacity:1}.sd-preview-thumb--active{opacity:1;outline:2px solid var(--sd-preview-accent);outline-offset:-2px;box-shadow:inset 0 0 0 2px var(--sd-preview-accent)}.sd-preview-thumb:focus-visible{outline:2px solid var(--sd-preview-accent);outline-offset:2px}.sd-preview-thumb__img{width:100%;height:100%;object-fit:cover;display:block;pointer-events:none}.sd-preview-thumb__broken{display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:var(--sd-preview-danger-soft);color:var(--sd-preview-danger);pointer-events:none}.sd-preview-thumb__broken mat-icon{font-size:24px;width:24px;height:24px}.sd-preview-thumb__badge{position:absolute;top:4px;right:4px;background:#000000b3;color:#fff;font-size:10px;font-weight:600;padding:1px 5px;border-radius:4px;pointer-events:none}.sd-preview-thumb__name{position:absolute;left:0;right:0;bottom:0;padding:14px 6px 4px;color:var(--sd-preview-fg);font-size:11px;line-height:1.3;background:linear-gradient(to top,rgba(0,0,0,.85),transparent);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;pointer-events:none}.sd-preview-thumbs--vertical .sd-preview-thumb{width:128px}.sd-preview-dots{position:absolute;bottom:24px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:6px;z-index:3}.sd-preview-dot{width:8px;height:8px;border:none;background:#ffffff4d;border-radius:999px;padding:0;cursor:pointer;transition:width .18s,background .18s}.sd-preview-dot--active{width:24px;background:#f5f6f7}.sd-preview-dot:focus-visible{outline:2px solid var(--sd-preview-accent);outline-offset:2px}.sd-preview-status{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;text-align:center;padding:24px}.sd-preview-status__tile{width:96px;height:96px;border-radius:24px;display:flex;align-items:center;justify-content:center;border:1px solid var(--sd-preview-divider);background:#ffffff0d}.sd-preview-status__tile mat-icon{font-size:48px;width:48px;height:48px;color:var(--sd-preview-fg-subtle)}.sd-preview-status__tile--muted{background:#ffffff0d;border-color:var(--sd-preview-divider)}.sd-preview-status__tile--danger{background:var(--sd-preview-danger-soft);border-color:var(--sd-preview-danger-border)}.sd-preview-status__tile--danger mat-icon{color:var(--sd-preview-danger)}.sd-preview-status__title{font-size:16px;font-weight:500;color:var(--sd-preview-fg)}.sd-preview-status__hint{font-size:13px;color:var(--sd-preview-fg-muted);max-width:320px}.sd-preview-spinner{width:56px;height:56px;border-radius:999px;border:3px solid rgba(255,255,255,.08);border-top-color:var(--sd-preview-accent);animation:sd-preview-spin .9s linear infinite}@keyframes sd-preview-spin{to{transform:rotate(360deg)}}.sd-preview-btn-primary{display:inline-flex;align-items:center;gap:6px;height:36px;padding:0 16px;background:var(--sd-preview-accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:13px;font-weight:500;transition:filter .12s}.sd-preview-btn-primary mat-icon{font-size:18px;width:18px;height:18px}.sd-preview-btn-primary:hover{filter:brightness(1.1)}.sd-preview-btn-primary:focus-visible{outline:2px solid #fff;outline-offset:2px}.sd-preview-kbd-hint{position:absolute;top:16px;left:16px;display:flex;align-items:center;gap:6px;z-index:2;pointer-events:none}.sd-preview-kbd{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 5px;border:1px solid var(--sd-preview-divider);background:#ffffff0a;color:var(--sd-preview-fg-muted);border-radius:4px;font:500 11px/1 ui-monospace,SFMono-Regular,Menlo,monospace}.sd-preview-kbd-text,.sd-preview-kbd-dot{font-size:11px;color:var(--sd-preview-fg-subtle)}.sd-preview-kbd-dot{margin:0 2px}\n"] }]
656
+ }], ctorParameters: () => [], propDecorators: { onKeyDown: [{
657
+ type: HostListener,
658
+ args: ['keydown', ['$event']]
659
+ }] } });
660
+
661
+ var _a;
662
+ const SD_PDFJS_LIB = new InjectionToken('SD_PDFJS_LIB', {
663
+ providedIn: 'root',
664
+ factory: () => {
665
+ // Try the worker URL one time at first-injection. Guarded so the
666
+ // component still imports cleanly in environments (tests, SSR) where
667
+ // import.meta.url can't be resolved or the worker bundle isn't shipped.
668
+ try {
669
+ if (pdfjsLib?.GlobalWorkerOptions && !pdfjsLib.GlobalWorkerOptions.workerSrc) {
670
+ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
671
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
672
+ 'pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
673
+ }
674
+ }
675
+ catch {
676
+ // Consumer app must set workerSrc manually (documented in sd-preview.md).
677
+ }
678
+ return pdfjsLib;
679
+ },
680
+ });
681
+ class SdPreviewPdf {
682
+ // ==========================================
683
+ // CONSTANTS
684
+ // ==========================================
685
+ // Dải zoom hữu dụng cho viewer PDF. Dưới 25% chữ thành chấm, trên 400% là
686
+ // raster bị pixelated nặng (canvas + DPR vẫn có giới hạn). Trùng dải của
687
+ // preview-image để consumer làm quen với 1 spec duy nhất.
688
+ static MIN_ZOOM = 0.25;
689
+ static MAX_ZOOM = 4;
690
+ static ZOOM_STEP = 0.1;
691
+ // ==========================================
692
+ // DI
693
+ // ==========================================
694
+ #hostEl = inject(ElementRef);
695
+ #destroyRef = inject(DestroyRef);
696
+ #pdfjs = inject(SD_PDFJS_LIB);
697
+ // ==========================================
698
+ // VIEW CHILDREN
699
+ // WHY `protected readonly` (not `#private`): Angular's `viewChild()` query
700
+ // function emits a decorator-equivalent metadata that the compiler can't
701
+ // see through ES private fields (NG1053). `protected` keeps them invisible
702
+ // to consumers but accessible to the template.
703
+ // ==========================================
704
+ pageCanvasRef = viewChild('pageCanvas');
705
+ stageEl = viewChild('stageRef');
706
+ pageInputRef = viewChild('pageInput');
707
+ searchInputRef = viewChild('searchInput');
708
+ // Every thumbnail <canvas> in the sidebar — populated when sidebar is open +
709
+ // mode==='thumbnails'. We feed these to the IntersectionObserver below so a
710
+ // thumb only renders when it scrolls into the viewport (lazy fill).
711
+ thumbCanvases = viewChildren('thumbCanvas');
712
+ // ==========================================
713
+ // INPUTS (signal-based, Angular 19 style)
714
+ // ==========================================
715
+ // autoId prefix `preview-pdf-`. Per-element child autoIds derive `${autoId}-{suffix}`.
716
+ autoIdInput = input(undefined, { alias: 'autoId' });
717
+ autoId = computed(() => (this.autoIdInput() ? `components-preview-pdf-${this.autoIdInput()}` : undefined));
718
+ #childAutoId = (suffix) => {
719
+ return this.autoId() ? `${this.autoId()}-${suffix}` : undefined;
720
+ };
721
+ // Page navigation
722
+ autoIdFirst = computed(() => this.#childAutoId('first'));
723
+ autoIdPrev = computed(() => this.#childAutoId('prev'));
724
+ autoIdNext = computed(() => this.#childAutoId('next'));
725
+ autoIdLast = computed(() => this.#childAutoId('last'));
726
+ autoIdPageInput = computed(() => this.#childAutoId('page-input'));
727
+ // Zoom
728
+ autoIdZoomIn = computed(() => this.#childAutoId('zoom-in'));
729
+ autoIdZoomOut = computed(() => this.#childAutoId('zoom-out'));
730
+ autoIdZoomReadout = computed(() => this.#childAutoId('zoom-readout'));
731
+ // Modes
732
+ autoIdFitPage = computed(() => this.#childAutoId('fit-page'));
733
+ autoIdFitWidth = computed(() => this.#childAutoId('fit-width'));
734
+ autoIdRotate = computed(() => this.#childAutoId('rotate'));
735
+ autoIdScrollMode = computed(() => this.#childAutoId('scroll-mode'));
736
+ // Header actions
737
+ autoIdSearchToggle = computed(() => this.#childAutoId('search'));
738
+ autoIdPrint = computed(() => this.#childAutoId('print'));
739
+ autoIdDownload = computed(() => this.#childAutoId('download'));
740
+ autoIdFullscreen = computed(() => this.#childAutoId('fullscreen'));
741
+ autoIdSidebarToggle = computed(() => this.#childAutoId('sidebar-toggle'));
742
+ // Sidebar tabs
743
+ autoIdTabThumbnails = computed(() => this.#childAutoId('tab-thumbnails'));
744
+ autoIdTabOutline = computed(() => this.#childAutoId('tab-outline'));
745
+ autoIdTabSearch = computed(() => this.#childAutoId('tab-search'));
746
+ // Search bar
747
+ autoIdSearchInput = computed(() => this.#childAutoId('search-input'));
748
+ autoIdSearchNext = computed(() => this.#childAutoId('search-next'));
749
+ autoIdSearchPrev = computed(() => this.#childAutoId('search-prev'));
750
+ autoIdSearchCase = computed(() => this.#childAutoId('search-case'));
751
+ autoIdSearchWhole = computed(() => this.#childAutoId('search-whole'));
752
+ autoIdSearchClose = computed(() => this.#childAutoId('search-close'));
753
+ // Retry (error state)
754
+ autoIdRetry = computed(() => this.#childAutoId('retry'));
755
+ // Per-index helpers (thumb + search result)
756
+ autoIdThumb = (i) => this.#childAutoId(`thumb-${i}`);
757
+ autoIdResult = (i) => this.#childAutoId(`result-${i}`);
758
+ source = input(null);
759
+ title = input(undefined);
760
+ startPage = input(1);
761
+ initialZoom = input('page-fit');
762
+ sidebar = input('thumbnails');
763
+ sidebarOpen = input(true);
764
+ scrollMode = input('page');
765
+ showToolbar = input(true);
766
+ downloadable = input(true);
767
+ password = input(undefined);
768
+ httpHeaders = input(undefined);
769
+ // Theme variant — drives [data-theme] host attribute. Default 'dark'
770
+ // preserves the original visual contract; 'light' flips token set via SCSS.
771
+ theme = input('dark');
772
+ // ==========================================
773
+ // OUTPUTS
774
+ // ==========================================
775
+ close = output();
776
+ loaded = output();
777
+ pageChange = output();
778
+ zoomChange = output();
779
+ download = output();
780
+ loadError = output();
781
+ // Fired whenever the search term, results, or active index changes — gives
782
+ // the consumer a stable hook for analytics / sticky highlight bars without
783
+ // poking at the internal state signal.
784
+ searchChange = output();
785
+ // ==========================================
786
+ // STATE (signals)
787
+ // ==========================================
788
+ #stage = signal('empty');
789
+ #pdfDoc = signal(null);
790
+ #meta = signal(null);
791
+ #activePage = signal(1);
792
+ #zoom = signal(1);
793
+ #zoomMode = signal('page-fit');
794
+ #rotation = signal(0);
795
+ #sidebarOpenInternal = signal(true);
796
+ #sidebarModeInternal = signal('thumbnails');
797
+ #scrollModeInternal = signal('page');
798
+ #loadProgress = signal({ loaded: 0, total: 0 });
799
+ #loadError = signal(null);
800
+ #isFullscreen = signal(false);
801
+ #filename = signal('document.pdf');
802
+ #fileSize = signal(0);
803
+ // Thumbnail cache: page number → data URL of mini-render. Lazy-populated as
804
+ // the user opens the sidebar / scrolls thumbnails into view.
805
+ #thumbCache = signal({});
806
+ // ==========================================
807
+ // SEARCH STATE
808
+ // ==========================================
809
+ // Whether the search bar (between header + body) is visible. Toggled via
810
+ // Ctrl+F, the sidebar tab, or `openSearch()` / `closeSearch()`. Independent
811
+ // from the sidebar's `'search'` tab — the bar can stay open with the
812
+ // sidebar closed (Ctrl+F flow), and the tab can be open without the bar.
813
+ #searchBarOpen = signal(false);
814
+ // Persisted across `clearSearch()` so user toggles don't reset on each new
815
+ // term — mirrors Acrobat / Chrome's "Find in page" behaviour.
816
+ #searchCaseSensitive = signal(false);
817
+ #searchWholeWord = signal(false);
818
+ #searchTerm = signal('');
819
+ #searchResults = signal([]);
820
+ // -1 sentinel means "no result focused yet" (e.g. right after clearSearch
821
+ // or before the first searchNext). Highlight rendering uses this to skip
822
+ // the `--active` class altogether.
823
+ #searchActiveIndex = signal(-1);
824
+ // Page-text cache: pageNum → fully-joined plain text. We pay the
825
+ // `getTextContent()` cost once per page per document, then reuse across
826
+ // re-runs (e.g. toggling case-sensitive). Cleared on every source change.
827
+ #pageTextCache = new Map();
828
+ // Token to abort an in-flight `search()` call when a newer one supersedes it
829
+ // — same pattern as #loadToken / #renderToken upstream.
830
+ #searchToken = 0;
831
+ // The IntersectionObserver instance that drives lazy thumb rendering. Lives
832
+ // for the component's lifetime; we just rebind to the latest canvas refs
833
+ // when the sidebar tab switches.
834
+ #thumbObserver = null;
835
+ // Pages currently being rendered as thumbs — guards against double work
836
+ // when a thumb scrolls in/out/in rapidly.
837
+ #thumbsRendering = new Set();
838
+ // Cached resolved source object that getDocument actually consumed — used
839
+ // for download fallback when caller passed a non-URL source.
840
+ #lastBlobUrl = null;
841
+ // Track ALL blob URLs we created so we can revoke on source change + destroy.
842
+ #ownedBlobUrls = new Set();
843
+ // Race guards mirror preview-image's #loadToken pattern.
844
+ #loadToken = 0;
845
+ #currentRenderTask = null;
846
+ #renderToken = 0;
847
+ // ==========================================
848
+ // COMPUTED (template-readable)
849
+ // ==========================================
850
+ stage = this.#stage.asReadonly();
851
+ activePage = this.#activePage.asReadonly();
852
+ zoom = this.#zoom.asReadonly();
853
+ zoomMode = this.#zoomMode.asReadonly();
854
+ rotation = this.#rotation.asReadonly();
855
+ sidebarMode = this.#sidebarModeInternal.asReadonly();
856
+ scrollModeCurrent = this.#scrollModeInternal.asReadonly();
857
+ isFullscreen = this.#isFullscreen.asReadonly();
858
+ meta = this.#meta.asReadonly();
859
+ filename = this.#filename.asReadonly();
860
+ fileSize = this.#fileSize.asReadonly();
861
+ loadError$ = this.#loadError.asReadonly();
862
+ thumbCache = this.#thumbCache.asReadonly();
863
+ numPages = computed(() => this.#meta()?.numPages ?? 0);
864
+ canPrev = computed(() => this.#activePage() > 1);
865
+ canNext = computed(() => {
866
+ const n = this.numPages();
867
+ return n > 0 && this.#activePage() < n;
868
+ });
869
+ zoomPercent = computed(() => Math.round(this.#zoom() * 100));
870
+ loadPercent = computed(() => {
871
+ const p = this.#loadProgress();
872
+ if (p.total <= 0)
873
+ return 0;
874
+ return Math.min(100, Math.round((p.loaded / p.total) * 100));
875
+ });
876
+ isSidebarOpen = computed(() => this.#sidebarOpenInternal() && this.#sidebarModeInternal() !== 'none');
877
+ pageList = computed(() => {
878
+ const n = this.numPages();
879
+ return n > 0 ? Array.from({ length: n }, (_, i) => i + 1) : [];
880
+ });
881
+ // Alias used by the template's thumbnail @for — keeps the JSX-y `pageNumbers`
882
+ // name from the design handoff while reusing the same computed.
883
+ pageNumbers = this.pageList;
884
+ // Search readonly accessors for the template + tests. Read-only by design;
885
+ // mutations go through `search()` / `searchNext()` etc.
886
+ searchBarOpen = this.#searchBarOpen.asReadonly();
887
+ searchTerm = this.#searchTerm.asReadonly();
888
+ searchResults = this.#searchResults.asReadonly();
889
+ searchActiveIndex = this.#searchActiveIndex.asReadonly();
890
+ searchCaseSensitive = this.#searchCaseSensitive.asReadonly();
891
+ searchWholeWord = this.#searchWholeWord.asReadonly();
892
+ searchTotal = computed(() => this.#searchResults().length);
893
+ // 1-based "X of N" indicator for the search bar counter. Reads 0 when
894
+ // there are no results so the template can hide / dim the counter.
895
+ searchCurrent = computed(() => {
896
+ const idx = this.#searchActiveIndex();
897
+ return idx < 0 ? 0 : idx + 1;
898
+ });
899
+ // Currently-active hit (or null when nothing is focused) — drives the
900
+ // in-page `<mark class="--active">` highlight + auto-scroll effect below.
901
+ searchActiveResult = computed(() => {
902
+ const idx = this.#searchActiveIndex();
903
+ const results = this.#searchResults();
904
+ return idx >= 0 && idx < results.length ? results[idx] : null;
905
+ });
906
+ // Snapshot of the full search state — handed to consumer via
907
+ // `getSearchState()` for parity with `searchChange` event shape.
908
+ searchState = computed(() => ({
909
+ term: this.#searchTerm(),
910
+ caseSensitive: this.#searchCaseSensitive(),
911
+ wholeWord: this.#searchWholeWord(),
912
+ results: this.#searchResults(),
913
+ activeIndex: this.#searchActiveIndex(),
914
+ }));
915
+ // ==========================================
916
+ // CONSTRUCTOR — declarative wiring
917
+ // ==========================================
918
+ constructor() {
919
+ // Initialize internal mirrors from inputs once. WHY mirrors: user actions
920
+ // (toggleSidebar / setSidebarMode / setZoom) must modify state that the
921
+ // template reads; signal inputs are read-only.
922
+ effect(() => {
923
+ this.#sidebarOpenInternal.set(this.sidebarOpen());
924
+ });
925
+ effect(() => {
926
+ const m = this.sidebar();
927
+ // 'outline' + 'search' are deferred — selecting them through the input
928
+ // is allowed (we still toggle the tab UI) but the body shows the deferred
929
+ // placeholder. Don't force-fallback to 'thumbnails' so the input value
930
+ // is honored visibly.
931
+ this.#sidebarModeInternal.set(m);
932
+ });
933
+ effect(() => {
934
+ const m = this.scrollMode();
935
+ // Continuous scroll is deferred — warn once if requested but keep the
936
+ // input value so consumer code is forward-compatible.
937
+ if (m === 'continuous') {
938
+ console.warn(
939
+ // eslint-disable-next-line max-len
940
+ '[sd-preview-pdf] scrollMode="continuous" is deferred; falling back to single-page rendering.');
941
+ }
942
+ this.#scrollModeInternal.set(m);
943
+ });
944
+ effect(() => {
945
+ // Reset zoom mode when initialZoom input changes — only fires when the
946
+ // input itself updates (not when user clicks zoom +/-).
947
+ this.#zoomMode.set(this.initialZoom());
948
+ });
949
+ // Sync fullscreen state from the browser (works with Esc, F11, programmatic exit).
950
+ const onFullscreenChange = () => {
951
+ this.#isFullscreen.set(document.fullscreenElement === this.#hostEl.nativeElement);
952
+ };
953
+ document.addEventListener('fullscreenchange', onFullscreenChange);
954
+ this.#destroyRef.onDestroy(() => {
955
+ document.removeEventListener('fullscreenchange', onFullscreenChange);
956
+ });
957
+ // React to source changes — destroy previous doc, load new one.
958
+ // WHY untracked-via-queueMicrotask: `effect()` tracks every signal READ
959
+ // sync-ly inside the callback. `#loadDocument` reads internal signals
960
+ // (#pdfDoc via #destroyDoc, plus #zoom on the cleanup path). Letting the
961
+ // effect track those would re-fire whenever we updated them inside the
962
+ // load flow → infinite loop. Reading only `source()` here and deferring
963
+ // the actual load to a microtask scopes the tracking to ONLY `source()`.
964
+ effect(() => {
965
+ const src = this.source();
966
+ queueMicrotask(() => this.#loadDocument(src));
967
+ });
968
+ // Auto-focus host after first render so keyboard works immediately.
969
+ afterNextRender(() => {
970
+ try {
971
+ this.#hostEl.nativeElement.focus({ preventScroll: true });
972
+ }
973
+ catch {
974
+ // ignore — focus() can throw on detached elements.
975
+ }
976
+ });
977
+ // Drive lazy thumbnail rendering — every time the list of thumbnail
978
+ // <canvas> refs changes (sidebar opens, mode flips, doc reloads) we
979
+ // disconnect any prior observer and start watching the new set.
980
+ // WHY effect (not afterNextRender): viewChildren() emits a signal that
981
+ // updates whenever the template's @for collection mutates.
982
+ effect(() => {
983
+ const refs = this.thumbCanvases();
984
+ this.#syncThumbObserver(refs.map(r => r.nativeElement));
985
+ });
986
+ // Re-apply highlights when the rendered page changes OR when the search
987
+ // term / active hit changes. WHY a separate effect: text-layer overlay
988
+ // doesn't exist in this commit, so we hook the page canvas wrapper and
989
+ // sprinkle `<mark>` siblings via DOM. Effect re-runs are cheap; the
990
+ // helper is a no-op when there's nothing to highlight.
991
+ effect(() => {
992
+ // Track these signals so the effect re-fires:
993
+ this.#searchTerm();
994
+ this.#searchActiveIndex();
995
+ this.activePage();
996
+ // Defer to a microtask so the canvas render finished writing — keeps
997
+ // us out of the page render's await chain.
998
+ queueMicrotask(() => this.#applyHighlightsForActivePage());
999
+ });
1000
+ // Cleanup on destroy.
1001
+ this.#destroyRef.onDestroy(() => {
1002
+ this.#cancelRender();
1003
+ this.#destroyDoc();
1004
+ this.#revokeAllBlobs();
1005
+ this.#thumbObserver?.disconnect();
1006
+ this.#thumbObserver = null;
1007
+ this.#thumbsRendering.clear();
1008
+ this.#pageTextCache.clear();
1009
+ if (document.fullscreenElement === this.#hostEl.nativeElement) {
1010
+ document.exitFullscreen?.().catch(() => undefined);
1011
+ }
1012
+ });
1013
+ }
1014
+ ngOnDestroy() {
1015
+ // Cleanup logic dời vào DestroyRef.onDestroy ở constructor — giữ hook để
1016
+ // tương thích với `implements OnDestroy` (giúp test code `spyOn(comp, 'ngOnDestroy')`).
1017
+ }
1018
+ // ==========================================
1019
+ // PUBLIC API
1020
+ // ==========================================
1021
+ goToPage(page) {
1022
+ const n = this.numPages();
1023
+ if (n <= 0)
1024
+ return;
1025
+ const target = Math.max(1, Math.min(page, n));
1026
+ if (target === this.#activePage())
1027
+ return;
1028
+ this.#activePage.set(target);
1029
+ this.pageChange.emit(target);
1030
+ this.#renderActivePage();
1031
+ }
1032
+ nextPage() { this.goToPage(this.#activePage() + 1); }
1033
+ prevPage() { this.goToPage(this.#activePage() - 1); }
1034
+ firstPage() { this.goToPage(1); }
1035
+ lastPage() { this.goToPage(this.numPages()); }
1036
+ zoomIn() {
1037
+ this.#zoomMode.set(this.#zoom() + _a.ZOOM_STEP);
1038
+ this.#setZoom(this.#zoom() + _a.ZOOM_STEP);
1039
+ }
1040
+ zoomOut() {
1041
+ this.#zoomMode.set(this.#zoom() - _a.ZOOM_STEP);
1042
+ this.#setZoom(this.#zoom() - _a.ZOOM_STEP);
1043
+ }
1044
+ setZoom(mode) {
1045
+ this.#zoomMode.set(mode);
1046
+ if (typeof mode === 'number') {
1047
+ this.#setZoom(mode);
1048
+ }
1049
+ else {
1050
+ // Fit modes recompute against stage size + page natural size — done in
1051
+ // #renderActivePage which reads zoomMode().
1052
+ this.#renderActivePage();
1053
+ }
1054
+ }
1055
+ rotate(direction) {
1056
+ const delta = direction === 'right' ? 90 : -90;
1057
+ this.#rotation.update(r => ((r + delta) % 360 + 360) % 360);
1058
+ this.#renderActivePage();
1059
+ }
1060
+ toggleSidebar() {
1061
+ this.#sidebarOpenInternal.update(v => !v);
1062
+ }
1063
+ setSidebarMode(mode) {
1064
+ this.#sidebarModeInternal.set(mode);
1065
+ if (mode !== 'none' && !this.#sidebarOpenInternal()) {
1066
+ this.#sidebarOpenInternal.set(true);
1067
+ }
1068
+ // Selecting the 'search' tab implicitly opens the search bar so the user
1069
+ // can start typing right away — matches Acrobat's flyout behaviour.
1070
+ if (mode === 'search') {
1071
+ this.openSearch();
1072
+ }
1073
+ }
1074
+ // TODO(preview-pdf): setScrollMode is signature-only; continuous + horizontal
1075
+ // layout deferred. Currently this just updates internal state + emits a
1076
+ // warning when consumer asks for the deferred 'continuous' mode.
1077
+ setScrollMode(mode) {
1078
+ if (mode === 'continuous') {
1079
+ console.warn(
1080
+ // eslint-disable-next-line max-len
1081
+ '[sd-preview-pdf] setScrollMode("continuous") is deferred; staying in single-page mode.');
1082
+ }
1083
+ this.#scrollModeInternal.set(mode);
1084
+ }
1085
+ downloadFile() {
1086
+ if (!this.downloadable())
1087
+ return;
1088
+ const src = this.source();
1089
+ const filename = this.#filename();
1090
+ // Strategy: if caller passed a URL, anchor download honors server's
1091
+ // Content-Disposition; for File/Blob/buffer sources we re-emit the blob.
1092
+ let href = null;
1093
+ if (typeof src === 'string') {
1094
+ href = src;
1095
+ }
1096
+ else if (src instanceof File || src instanceof Blob) {
1097
+ href = URL.createObjectURL(src);
1098
+ this.#ownedBlobUrls.add(href);
1099
+ }
1100
+ else if (this.#lastBlobUrl) {
1101
+ href = this.#lastBlobUrl;
1102
+ }
1103
+ else if (src && typeof src === 'object' && 'url' in src && typeof src.url === 'string') {
1104
+ href = src.url;
1105
+ }
1106
+ else if (src instanceof ArrayBuffer || src instanceof Uint8Array) {
1107
+ const blob = new Blob([src], { type: 'application/pdf' });
1108
+ href = URL.createObjectURL(blob);
1109
+ this.#ownedBlobUrls.add(href);
1110
+ }
1111
+ if (!href)
1112
+ return;
1113
+ const a = document.createElement('a');
1114
+ a.href = href;
1115
+ a.download = filename;
1116
+ document.body.appendChild(a);
1117
+ a.click();
1118
+ a.remove();
1119
+ this.download.emit({ filename });
1120
+ }
1121
+ toggleFullscreen() {
1122
+ if (!document.fullscreenElement) {
1123
+ this.#hostEl.nativeElement.requestFullscreen?.().catch(() => undefined);
1124
+ }
1125
+ else {
1126
+ document.exitFullscreen?.().catch(() => undefined);
1127
+ }
1128
+ }
1129
+ /** Programmatic equivalent of clicking the X — emits the close output. */
1130
+ requestClose() {
1131
+ this.close.emit();
1132
+ }
1133
+ /** Retry the active load attempt (called from the error state retry button). */
1134
+ retryLoad() {
1135
+ this.#loadDocument(this.source());
1136
+ }
1137
+ // TODO(preview-pdf): printFile + scroll-mode continuous remain deferred.
1138
+ // Search is now implemented below.
1139
+ printFile() {
1140
+ console.warn('[sd-preview-pdf] printFile() is deferred to a follow-up commit.');
1141
+ }
1142
+ /**
1143
+ * Run a full-document search for `term`. Returns the result count.
1144
+ *
1145
+ * Options:
1146
+ * - `caseSensitive`: default off; matches independent of capitalisation.
1147
+ * - `wholeWord`: default off; requires Unicode word-boundary on both sides
1148
+ * so `cat` does NOT match `category` (uses `/\b...\b/u` so Vietnamese
1149
+ * diacritic-bearing letters still participate as word characters).
1150
+ *
1151
+ * Implementation: iterate every page once (cached), build a flat plain-text
1152
+ * string per page (joining textItem.str), and slice ~30 chars of context
1153
+ * around each match. We deliberately use plain JS regex rather than
1154
+ * pdf.js's `pdfFindController` — it's <40 lines, fully testable, and
1155
+ * sidesteps the find-controller's dependency on a real text layer (which
1156
+ * we don't render).
1157
+ */
1158
+ async search(term, options) {
1159
+ const token = ++this.#searchToken;
1160
+ const cleanTerm = (term ?? '').trim();
1161
+ if (options?.caseSensitive !== undefined) {
1162
+ this.#searchCaseSensitive.set(options.caseSensitive);
1163
+ }
1164
+ if (options?.wholeWord !== undefined) {
1165
+ this.#searchWholeWord.set(options.wholeWord);
1166
+ }
1167
+ this.#searchTerm.set(cleanTerm);
1168
+ if (!cleanTerm) {
1169
+ this.#searchResults.set([]);
1170
+ this.#searchActiveIndex.set(-1);
1171
+ this.#emitSearchChange();
1172
+ return 0;
1173
+ }
1174
+ const doc = this.#pdfDoc();
1175
+ if (!doc) {
1176
+ this.#searchResults.set([]);
1177
+ this.#searchActiveIndex.set(-1);
1178
+ this.#emitSearchChange();
1179
+ return 0;
1180
+ }
1181
+ const re = this.#buildSearchRegex(cleanTerm, this.#searchCaseSensitive(), this.#searchWholeWord());
1182
+ if (!re) {
1183
+ this.#searchResults.set([]);
1184
+ this.#searchActiveIndex.set(-1);
1185
+ this.#emitSearchChange();
1186
+ return 0;
1187
+ }
1188
+ const results = [];
1189
+ for (let p = 1; p <= doc.numPages; p++) {
1190
+ if (token !== this.#searchToken)
1191
+ return 0; // newer search superseded us
1192
+ const text = await this.#getPageText(doc, p);
1193
+ if (token !== this.#searchToken)
1194
+ return 0;
1195
+ // re is /g — must be re-created or `lastIndex` reset between pages.
1196
+ re.lastIndex = 0;
1197
+ let m;
1198
+ while ((m = re.exec(text)) !== null) {
1199
+ const start = m.index;
1200
+ const matched = m[0];
1201
+ const end = start + matched.length;
1202
+ const before = text.slice(Math.max(0, start - 30), start);
1203
+ const after = text.slice(end, end + 30);
1204
+ results.push({ page: p, before, term: matched, after });
1205
+ // Defensive: zero-length match would loop forever.
1206
+ if (matched.length === 0)
1207
+ re.lastIndex++;
1208
+ }
1209
+ }
1210
+ if (token !== this.#searchToken)
1211
+ return 0;
1212
+ this.#searchResults.set(results);
1213
+ // Activate the first result so the user sees something immediately;
1214
+ // tests can assert this via `searchActiveIndex()`. Skip when empty.
1215
+ this.#searchActiveIndex.set(results.length > 0 ? 0 : -1);
1216
+ if (results.length > 0) {
1217
+ // Auto-jump to the first hit's page.
1218
+ this.goToPage(results[0].page);
1219
+ }
1220
+ this.#emitSearchChange();
1221
+ return results.length;
1222
+ }
1223
+ searchNext() {
1224
+ const results = this.#searchResults();
1225
+ if (results.length === 0)
1226
+ return;
1227
+ const next = (this.#searchActiveIndex() + 1) % results.length;
1228
+ this.#searchActiveIndex.set(next);
1229
+ this.goToPage(results[next].page);
1230
+ this.#emitSearchChange();
1231
+ }
1232
+ searchPrev() {
1233
+ const results = this.#searchResults();
1234
+ if (results.length === 0)
1235
+ return;
1236
+ const cur = this.#searchActiveIndex();
1237
+ const prev = cur <= 0 ? results.length - 1 : cur - 1;
1238
+ this.#searchActiveIndex.set(prev);
1239
+ this.goToPage(results[prev].page);
1240
+ this.#emitSearchChange();
1241
+ }
1242
+ clearSearch() {
1243
+ this.#searchToken++;
1244
+ this.#searchTerm.set('');
1245
+ this.#searchResults.set([]);
1246
+ this.#searchActiveIndex.set(-1);
1247
+ this.#emitSearchChange();
1248
+ }
1249
+ /** Open the search bar and focus its input. Idempotent. */
1250
+ openSearch() {
1251
+ this.#searchBarOpen.set(true);
1252
+ // Defer focus a tick so the *ngIf-rendered input exists in the DOM.
1253
+ queueMicrotask(() => {
1254
+ this.searchInputRef()?.nativeElement?.focus();
1255
+ this.searchInputRef()?.nativeElement?.select();
1256
+ });
1257
+ }
1258
+ /** Close the search bar AND clear results (mirrors Acrobat behaviour). */
1259
+ closeSearch() {
1260
+ this.#searchBarOpen.set(false);
1261
+ this.clearSearch();
1262
+ }
1263
+ toggleSearchCaseSensitive() {
1264
+ this.#searchCaseSensitive.update(v => !v);
1265
+ // Re-run with the persisted term so the result set reflects the new flag.
1266
+ if (this.#searchTerm()) {
1267
+ void this.search(this.#searchTerm());
1268
+ }
1269
+ }
1270
+ toggleSearchWholeWord() {
1271
+ this.#searchWholeWord.update(v => !v);
1272
+ if (this.#searchTerm()) {
1273
+ void this.search(this.#searchTerm());
1274
+ }
1275
+ }
1276
+ /** Live binding from the search bar's `(input)` event. */
1277
+ onSearchInput(value) {
1278
+ void this.search(value);
1279
+ }
1280
+ // ==========================================
1281
+ // KEYBOARD — bound to HOST (not document)
1282
+ // ==========================================
1283
+ onKeyDown(event) {
1284
+ const target = event.target;
1285
+ const isSearchInput = !!target && target === this.searchInputRef()?.nativeElement;
1286
+ const isOtherEditable = !!target &&
1287
+ !isSearchInput &&
1288
+ (target.tagName === 'INPUT' ||
1289
+ target.tagName === 'TEXTAREA' ||
1290
+ target.isContentEditable);
1291
+ // Ctrl/Cmd+F: open the search bar from anywhere (even when input has
1292
+ // focus, e.g. re-focus the search bar from the page input). Handled
1293
+ // before the input-target guard so it always works.
1294
+ if ((event.ctrlKey || event.metaKey) && (event.key === 'f' || event.key === 'F')) {
1295
+ event.preventDefault();
1296
+ this.openSearch();
1297
+ return;
1298
+ }
1299
+ // Keys while the search input is focused: Enter / Shift+Enter, F3 / Shift+F3
1300
+ // cycle results; Esc closes the bar. Everything else (typing) passes through.
1301
+ if (isSearchInput) {
1302
+ if (event.key === 'Escape') {
1303
+ event.preventDefault();
1304
+ this.closeSearch();
1305
+ // Re-focus the host so subsequent shortcuts work without a click.
1306
+ this.#hostEl.nativeElement.focus();
1307
+ return;
1308
+ }
1309
+ if (event.key === 'Enter') {
1310
+ event.preventDefault();
1311
+ if (event.shiftKey) {
1312
+ this.searchPrev();
1313
+ }
1314
+ else {
1315
+ this.searchNext();
1316
+ }
1317
+ return;
1318
+ }
1319
+ if (event.key === 'F3') {
1320
+ event.preventDefault();
1321
+ if (event.shiftKey) {
1322
+ this.searchPrev();
1323
+ }
1324
+ else {
1325
+ this.searchNext();
1326
+ }
1327
+ return;
1328
+ }
1329
+ return; // typing → let it pass through
1330
+ }
1331
+ // Other editable targets (page-number input etc.) shouldn't trigger
1332
+ // viewer shortcuts — but Esc should still close an open search bar.
1333
+ if (isOtherEditable) {
1334
+ if (event.key === 'Escape' && this.#searchBarOpen()) {
1335
+ event.preventDefault();
1336
+ this.closeSearch();
1337
+ }
1338
+ return;
1339
+ }
1340
+ // F3 outside the search input still cycles results (matches browser
1341
+ // Find-in-page convention). Only meaningful when the bar is open OR
1342
+ // there are stale results — we keep it ungated for parity.
1343
+ if (event.key === 'F3') {
1344
+ event.preventDefault();
1345
+ if (event.shiftKey) {
1346
+ this.searchPrev();
1347
+ }
1348
+ else {
1349
+ this.searchNext();
1350
+ }
1351
+ return;
1352
+ }
1353
+ switch (event.key) {
1354
+ case 'ArrowLeft':
1355
+ case 'PageUp':
1356
+ event.preventDefault();
1357
+ this.prevPage();
1358
+ break;
1359
+ case 'ArrowRight':
1360
+ case 'PageDown':
1361
+ event.preventDefault();
1362
+ this.nextPage();
1363
+ break;
1364
+ case 'Home':
1365
+ event.preventDefault();
1366
+ this.firstPage();
1367
+ break;
1368
+ case 'End':
1369
+ event.preventDefault();
1370
+ this.lastPage();
1371
+ break;
1372
+ case '+':
1373
+ case '=':
1374
+ event.preventDefault();
1375
+ this.zoomIn();
1376
+ break;
1377
+ case '-':
1378
+ event.preventDefault();
1379
+ this.zoomOut();
1380
+ break;
1381
+ case '0':
1382
+ event.preventDefault();
1383
+ this.setZoom('page-fit');
1384
+ break;
1385
+ case 'r':
1386
+ event.preventDefault();
1387
+ this.rotate('right');
1388
+ break;
1389
+ case 'R':
1390
+ event.preventDefault();
1391
+ this.rotate('left');
1392
+ break;
1393
+ case 'f':
1394
+ event.preventDefault();
1395
+ this.toggleFullscreen();
1396
+ break;
1397
+ case 'Escape':
1398
+ // Esc closes the search bar (when open). NEVER emits `close` on the
1399
+ // component — that remains the consumer's responsibility per the
1400
+ // standalone-embed pattern.
1401
+ if (this.#searchBarOpen()) {
1402
+ event.preventDefault();
1403
+ this.closeSearch();
1404
+ }
1405
+ break;
1406
+ default:
1407
+ break;
1408
+ }
1409
+ }
1410
+ // ==========================================
1411
+ // TEMPLATE HANDLERS
1412
+ // ==========================================
1413
+ onPageInputEnter(event) {
1414
+ const input = event.target;
1415
+ const value = Number(input.value);
1416
+ if (Number.isFinite(value)) {
1417
+ this.goToPage(value);
1418
+ }
1419
+ // Snap input back to the actual current page after clamping.
1420
+ input.value = String(this.#activePage());
1421
+ }
1422
+ onPageInputBlur(event) {
1423
+ const input = event.target;
1424
+ input.value = String(this.#activePage());
1425
+ }
1426
+ onWheel(event) {
1427
+ // Ctrl/Cmd + wheel = zoom (mirrors PDF readers + most image viewers).
1428
+ // Plain wheel = let browser scroll the stage (especially when zoomed in).
1429
+ if (event.ctrlKey || event.metaKey) {
1430
+ event.preventDefault();
1431
+ const factor = event.deltaY < 0 ? 1 + _a.ZOOM_STEP : 1 - _a.ZOOM_STEP;
1432
+ this.#zoomMode.set(this.#zoom() * factor);
1433
+ this.#setZoom(this.#zoom() * factor);
1434
+ }
1435
+ }
1436
+ trackByPage(_i, page) {
1437
+ return page;
1438
+ }
1439
+ // ==========================================
1440
+ // INTERNALS
1441
+ // ==========================================
1442
+ async #loadDocument(src) {
1443
+ const token = ++this.#loadToken;
1444
+ this.#cancelRender();
1445
+ await this.#destroyDoc();
1446
+ this.#revokeAllBlobs();
1447
+ this.#thumbCache.set({});
1448
+ this.#pageTextCache.clear();
1449
+ this.#thumbsRendering.clear();
1450
+ // Reset search state — results are per-document, but keep the term so a
1451
+ // newly-loaded source with the same term gets re-indexed automatically
1452
+ // on the next user keystroke. Active index resets unconditionally.
1453
+ this.#searchResults.set([]);
1454
+ this.#searchActiveIndex.set(-1);
1455
+ this.#loadError.set(null);
1456
+ this.#loadProgress.set({ loaded: 0, total: 0 });
1457
+ if (!src) {
1458
+ this.#stage.set('empty');
1459
+ this.#pdfDoc.set(null);
1460
+ this.#meta.set(null);
1461
+ this.#activePage.set(1);
1462
+ return;
1463
+ }
1464
+ this.#stage.set('loading');
1465
+ let spec;
1466
+ try {
1467
+ spec = await this.#normalizeSource(src);
1468
+ }
1469
+ catch (err) {
1470
+ if (token !== this.#loadToken)
1471
+ return;
1472
+ this.#emitError(this.#classifyError(err), this.#errorMessage(err));
1473
+ return;
1474
+ }
1475
+ if (this.password()) {
1476
+ spec['password'] = this.password();
1477
+ }
1478
+ let task;
1479
+ try {
1480
+ task = this.#pdfjs.getDocument(spec);
1481
+ // pdfjs's loadingTask exposes onProgress as an assignable property.
1482
+ task.onProgress = (p) => {
1483
+ if (token !== this.#loadToken)
1484
+ return;
1485
+ this.#loadProgress.set({ loaded: p.loaded ?? 0, total: p.total ?? 0 });
1486
+ };
1487
+ }
1488
+ catch (err) {
1489
+ this.#emitError(this.#classifyError(err), this.#errorMessage(err));
1490
+ return;
1491
+ }
1492
+ let pdfDoc;
1493
+ try {
1494
+ pdfDoc = (await task.promise);
1495
+ }
1496
+ catch (err) {
1497
+ if (token !== this.#loadToken)
1498
+ return;
1499
+ this.#emitError(this.#classifyError(err), this.#errorMessage(err));
1500
+ return;
1501
+ }
1502
+ if (token !== this.#loadToken) {
1503
+ // A newer source replaced us mid-load. Cleanup this stale doc.
1504
+ try {
1505
+ await pdfDoc.destroy();
1506
+ }
1507
+ catch { /* ignore */ }
1508
+ return;
1509
+ }
1510
+ let meta;
1511
+ try {
1512
+ const m = await pdfDoc.getMetadata();
1513
+ const info = m?.info ?? {};
1514
+ meta = {
1515
+ title: info.Title,
1516
+ author: info.Author,
1517
+ subject: info.Subject,
1518
+ numPages: pdfDoc.numPages,
1519
+ };
1520
+ }
1521
+ catch {
1522
+ meta = { numPages: pdfDoc.numPages };
1523
+ }
1524
+ this.#pdfDoc.set(pdfDoc);
1525
+ this.#meta.set(meta);
1526
+ this.#activePage.set(Math.max(1, Math.min(this.startPage(), pdfDoc.numPages)));
1527
+ this.#stage.set('ready');
1528
+ this.#zoom.set(1);
1529
+ this.#rotation.set(0);
1530
+ this.#zoomMode.set(this.initialZoom());
1531
+ this.loaded.emit({ totalPages: pdfDoc.numPages, meta });
1532
+ this.pageChange.emit(this.#activePage());
1533
+ await this.#renderActivePage();
1534
+ }
1535
+ async #renderActivePage() {
1536
+ const pdfDoc = this.#pdfDoc();
1537
+ if (!pdfDoc)
1538
+ return;
1539
+ const token = ++this.#renderToken;
1540
+ const pageNum = this.#activePage();
1541
+ let page;
1542
+ try {
1543
+ page = await pdfDoc.getPage(pageNum);
1544
+ }
1545
+ catch {
1546
+ return;
1547
+ }
1548
+ if (token !== this.#renderToken)
1549
+ return;
1550
+ const canvas = this.pageCanvasRef()?.nativeElement;
1551
+ if (!canvas)
1552
+ return;
1553
+ // Resolve scale from current zoom mode.
1554
+ const rotation = this.#rotation();
1555
+ const baseViewport = page.getViewport({ scale: 1, rotation });
1556
+ const scale = this.#resolveScale(baseViewport);
1557
+ this.#zoom.set(scale);
1558
+ const viewport = page.getViewport({ scale, rotation });
1559
+ const ctx = canvas.getContext('2d');
1560
+ if (!ctx)
1561
+ return;
1562
+ canvas.width = Math.floor(viewport.width);
1563
+ canvas.height = Math.floor(viewport.height);
1564
+ canvas.style.width = `${viewport.width}px`;
1565
+ canvas.style.height = `${viewport.height}px`;
1566
+ this.#cancelRender();
1567
+ const renderTask = page.render({ canvasContext: ctx, viewport });
1568
+ this.#currentRenderTask = renderTask;
1569
+ try {
1570
+ await renderTask.promise;
1571
+ }
1572
+ catch {
1573
+ // Cancelled mid-flight when user navigated to a new page — fine.
1574
+ return;
1575
+ }
1576
+ finally {
1577
+ if (this.#currentRenderTask === renderTask) {
1578
+ this.#currentRenderTask = null;
1579
+ }
1580
+ }
1581
+ page.cleanup();
1582
+ this.zoomChange.emit(scale);
1583
+ }
1584
+ #resolveScale(baseViewport) {
1585
+ const mode = this.#zoomMode();
1586
+ if (typeof mode === 'number') {
1587
+ return Math.min(_a.MAX_ZOOM, Math.max(_a.MIN_ZOOM, mode));
1588
+ }
1589
+ if (mode === 'page-actual')
1590
+ return 1;
1591
+ const stage = this.stageEl()?.nativeElement;
1592
+ if (!stage)
1593
+ return 1;
1594
+ const stageRect = stage.getBoundingClientRect();
1595
+ // 32px horizontal + 96px bottom padding lives in the SCSS — subtract it so
1596
+ // the page fits without overlapping the floating toolbar.
1597
+ const availW = Math.max(80, stageRect.width - 48);
1598
+ const availH = Math.max(80, stageRect.height - 120);
1599
+ if (mode === 'page-width') {
1600
+ return availW / baseViewport.width;
1601
+ }
1602
+ // page-fit
1603
+ return Math.min(availW / baseViewport.width, availH / baseViewport.height);
1604
+ }
1605
+ #cancelRender() {
1606
+ if (this.#currentRenderTask) {
1607
+ try {
1608
+ this.#currentRenderTask.cancel();
1609
+ }
1610
+ catch { /* ignore */ }
1611
+ this.#currentRenderTask = null;
1612
+ }
1613
+ }
1614
+ async #destroyDoc() {
1615
+ const doc = this.#pdfDoc();
1616
+ if (!doc)
1617
+ return;
1618
+ this.#pdfDoc.set(null);
1619
+ try {
1620
+ await doc.destroy();
1621
+ }
1622
+ catch { /* ignore */ }
1623
+ }
1624
+ #setZoom(value) {
1625
+ const clamped = Math.min(_a.MAX_ZOOM, Math.max(_a.MIN_ZOOM, value));
1626
+ this.#zoom.set(clamped);
1627
+ this.#renderActivePage();
1628
+ }
1629
+ async #normalizeSource(src) {
1630
+ if (typeof src === 'string') {
1631
+ this.#filename.set(this.#guessName(src));
1632
+ const headers = this.httpHeaders();
1633
+ const spec = { url: src };
1634
+ if (headers)
1635
+ spec['httpHeaders'] = headers;
1636
+ return spec;
1637
+ }
1638
+ if (src instanceof File) {
1639
+ this.#filename.set(src.name);
1640
+ this.#fileSize.set(src.size);
1641
+ const buf = await src.arrayBuffer();
1642
+ return { data: new Uint8Array(buf) };
1643
+ }
1644
+ if (src instanceof Blob) {
1645
+ this.#filename.set('document.pdf');
1646
+ this.#fileSize.set(src.size);
1647
+ const buf = await src.arrayBuffer();
1648
+ return { data: new Uint8Array(buf) };
1649
+ }
1650
+ if (src instanceof ArrayBuffer) {
1651
+ this.#filename.set('document.pdf');
1652
+ this.#fileSize.set(src.byteLength);
1653
+ return { data: new Uint8Array(src) };
1654
+ }
1655
+ if (src instanceof Uint8Array) {
1656
+ this.#filename.set('document.pdf');
1657
+ this.#fileSize.set(src.byteLength);
1658
+ return { data: src };
1659
+ }
1660
+ if (src && typeof src === 'object') {
1661
+ if ('url' in src && typeof src.url === 'string') {
1662
+ this.#filename.set(this.#guessName(src.url));
1663
+ const spec = { url: src.url };
1664
+ // Caller-provided headers win over the [httpHeaders] input.
1665
+ const headers = src.httpHeaders ?? this.httpHeaders();
1666
+ if (headers)
1667
+ spec['httpHeaders'] = headers;
1668
+ if (src.withCredentials)
1669
+ spec['withCredentials'] = true;
1670
+ return spec;
1671
+ }
1672
+ if ('data' in src && (src.data instanceof ArrayBuffer || src.data instanceof Uint8Array)) {
1673
+ this.#filename.set('document.pdf');
1674
+ const data = src.data instanceof ArrayBuffer ? new Uint8Array(src.data) : src.data;
1675
+ this.#fileSize.set(data.byteLength);
1676
+ return { data };
1677
+ }
1678
+ }
1679
+ throw new Error('Unsupported PDF source');
1680
+ }
1681
+ #guessName(url) {
1682
+ const path = url.split('?')[0];
1683
+ const last = path.substring(path.lastIndexOf('/') + 1);
1684
+ return last || 'document.pdf';
1685
+ }
1686
+ /** Map any thrown value into a {reason} we can render. */
1687
+ #classifyError(err) {
1688
+ if (!err)
1689
+ return 'unknown';
1690
+ // pdfjs throws named exception classes — we sniff by name to avoid
1691
+ // importing the constructors (keeps the bundle smaller).
1692
+ const name = err.name;
1693
+ if (name === 'PasswordException')
1694
+ return 'password';
1695
+ if (name === 'InvalidPDFException')
1696
+ return 'invalid';
1697
+ if (name === 'MissingPDFException' || name === 'UnexpectedResponseException') {
1698
+ return 'network';
1699
+ }
1700
+ if (err instanceof Error && /network|fetch|cors/i.test(err.message)) {
1701
+ return 'network';
1702
+ }
1703
+ return 'unknown';
1704
+ }
1705
+ #errorMessage(err) {
1706
+ if (err instanceof Error)
1707
+ return err.message;
1708
+ return undefined;
1709
+ }
1710
+ #emitError(reason, message) {
1711
+ const ev = { reason, message };
1712
+ this.#loadError.set(ev);
1713
+ this.#stage.set('error');
1714
+ this.loadError.emit(ev);
1715
+ }
1716
+ #revokeAllBlobs() {
1717
+ for (const url of this.#ownedBlobUrls) {
1718
+ URL.revokeObjectURL(url);
1719
+ }
1720
+ this.#ownedBlobUrls.clear();
1721
+ this.#lastBlobUrl = null;
1722
+ }
1723
+ // ==========================================
1724
+ // SEARCH HELPERS
1725
+ // ==========================================
1726
+ /**
1727
+ * Build the search regex. We escape every metachar so the term is matched
1728
+ * literally (so `foo.bar` doesn't treat `.` as "any char"). Unicode flag
1729
+ * `u` makes `\b` understand Vietnamese diacritic-bearing letters as word
1730
+ * characters — without it `\b` would split `đ` at the d, breaking
1731
+ * whole-word matches on Vietnamese terms.
1732
+ */
1733
+ #buildSearchRegex(term, caseSensitive, wholeWord) {
1734
+ if (!term)
1735
+ return null;
1736
+ const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1737
+ const flags = (caseSensitive ? 'g' : 'gi') + 'u';
1738
+ const pattern = wholeWord ? `\\b${escaped}\\b` : escaped;
1739
+ try {
1740
+ return new RegExp(pattern, flags);
1741
+ }
1742
+ catch {
1743
+ return null;
1744
+ }
1745
+ }
1746
+ /**
1747
+ * Fetch (and cache) the plain text of a single page. We normalize to NFC
1748
+ * so the same Vietnamese letter encoded as composed vs decomposed sequences
1749
+ * (NFD) matches the same search term — pdfjs has been seen to return NFD
1750
+ * in some PDFs.
1751
+ */
1752
+ async #getPageText(doc, pageNum) {
1753
+ const cached = this.#pageTextCache.get(pageNum);
1754
+ if (cached !== undefined)
1755
+ return cached;
1756
+ let page;
1757
+ try {
1758
+ page = await doc.getPage(pageNum);
1759
+ }
1760
+ catch {
1761
+ return '';
1762
+ }
1763
+ let text = '';
1764
+ try {
1765
+ const tc = (await page.getTextContent());
1766
+ const items = Array.isArray(tc?.items) ? tc.items : [];
1767
+ const parts = [];
1768
+ for (const it of items) {
1769
+ const str = it?.str;
1770
+ if (typeof str === 'string')
1771
+ parts.push(str);
1772
+ }
1773
+ // Space-join so multi-word matches across textItem boundaries work.
1774
+ text = parts.join(' ').normalize('NFC');
1775
+ }
1776
+ catch {
1777
+ text = '';
1778
+ }
1779
+ this.#pageTextCache.set(pageNum, text);
1780
+ return text;
1781
+ }
1782
+ #emitSearchChange() {
1783
+ this.searchChange.emit({
1784
+ term: this.#searchTerm(),
1785
+ total: this.#searchResults().length,
1786
+ current: this.#searchActiveIndex() + 1, // 1-based; 0 when no active
1787
+ });
1788
+ }
1789
+ /**
1790
+ * Paint highlight marks over the current page's canvas wrapper. WHY DOM
1791
+ * append (not text layer): rendering the proper pdf.js text layer is a
1792
+ * larger change — out-of-scope here. Instead we drop floating `<mark>`
1793
+ * pills near the top of the page; they're a visual cue that the page
1794
+ * contains a hit + which one is active, even when we can't place them
1795
+ * pixel-accurately. The result-list cards still drive precise navigation.
1796
+ *
1797
+ * Tradeoff documented in sd-preview.md.
1798
+ */
1799
+ #applyHighlightsForActivePage() {
1800
+ const wrap = this.#hostEl.nativeElement.querySelector('.sd-preview-pdf-page-wrap');
1801
+ if (!wrap)
1802
+ return;
1803
+ // Clear prior overlay.
1804
+ const prior = wrap.querySelector('.sd-pdf-search-overlay');
1805
+ if (prior)
1806
+ prior.remove();
1807
+ const term = this.#searchTerm();
1808
+ const results = this.#searchResults();
1809
+ const activeIdx = this.#searchActiveIndex();
1810
+ const activePage = this.activePage();
1811
+ if (!term || results.length === 0)
1812
+ return;
1813
+ const pageHits = results
1814
+ .map((r, i) => ({ r, i }))
1815
+ .filter(({ r }) => r.page === activePage);
1816
+ if (pageHits.length === 0)
1817
+ return;
1818
+ const overlay = document.createElement('div');
1819
+ overlay.className = 'sd-pdf-search-overlay';
1820
+ for (const { r, i } of pageHits) {
1821
+ const m = document.createElement('mark');
1822
+ m.className =
1823
+ i === activeIdx ? 'sd-pdf-search-hi sd-pdf-search-hi--active' : 'sd-pdf-search-hi';
1824
+ m.textContent = r.term;
1825
+ overlay.appendChild(m);
1826
+ }
1827
+ wrap.appendChild(overlay);
1828
+ // Scroll the active mark into view if it lives on this page.
1829
+ if (activeIdx >= 0 && results[activeIdx]?.page === activePage) {
1830
+ const activeMark = overlay.querySelector('.sd-pdf-search-hi--active');
1831
+ try {
1832
+ activeMark?.scrollIntoView({ block: 'center', behavior: 'smooth' });
1833
+ }
1834
+ catch {
1835
+ // older Safari throws on smooth — fall back silently
1836
+ }
1837
+ }
1838
+ }
1839
+ // ==========================================
1840
+ // THUMBNAIL HELPERS
1841
+ // ==========================================
1842
+ /**
1843
+ * Rebind the IntersectionObserver to whatever set of thumbnail canvases is
1844
+ * currently mounted. Called from an effect that tracks `thumbCanvases()`.
1845
+ */
1846
+ #syncThumbObserver(canvases) {
1847
+ // Disconnect prior observer — refs are stale once the @for re-renders.
1848
+ this.#thumbObserver?.disconnect();
1849
+ if (canvases.length === 0) {
1850
+ this.#thumbObserver = null;
1851
+ return;
1852
+ }
1853
+ // Lazily create an IntersectionObserver scoped to the sidebar scroll
1854
+ // container so we only render thumbs the user can actually see. Falls
1855
+ // back to "render everything" when IntersectionObserver isn't available
1856
+ // (very old runtime / SSR shim).
1857
+ if (typeof IntersectionObserver !== 'function') {
1858
+ for (const c of canvases) {
1859
+ const n = Number(c.getAttribute('data-page'));
1860
+ if (Number.isFinite(n))
1861
+ void this.#renderThumbnail(n, c);
1862
+ }
1863
+ return;
1864
+ }
1865
+ this.#thumbObserver = new IntersectionObserver(entries => {
1866
+ for (const entry of entries) {
1867
+ if (!entry.isIntersecting)
1868
+ continue;
1869
+ const c = entry.target;
1870
+ const n = Number(c.getAttribute('data-page'));
1871
+ if (!Number.isFinite(n))
1872
+ continue;
1873
+ this.#thumbObserver?.unobserve(c); // one-shot — cached after first paint
1874
+ void this.#renderThumbnail(n, c);
1875
+ }
1876
+ }, { rootMargin: '120px 0px 120px 0px', threshold: 0.01 });
1877
+ for (const c of canvases)
1878
+ this.#thumbObserver.observe(c);
1879
+ }
1880
+ /**
1881
+ * Render a single page into a sidebar thumbnail canvas at ~140px width.
1882
+ * Cached via `#thumbsRendering` so rapid scroll bounces don't double-render.
1883
+ *
1884
+ * Visible for testing — tests can stub `getPage()` on the fake doc to
1885
+ * assert this method calls `render(...)` with the right page argument.
1886
+ */
1887
+ async renderThumbnailForPage(pageNum) {
1888
+ const canvas = this.thumbCanvases()
1889
+ .map(r => r.nativeElement)
1890
+ .find(c => Number(c.getAttribute('data-page')) === pageNum);
1891
+ if (!canvas)
1892
+ return;
1893
+ return this.#renderThumbnail(pageNum, canvas);
1894
+ }
1895
+ async #renderThumbnail(pageNum, canvas) {
1896
+ const doc = this.#pdfDoc();
1897
+ if (!doc)
1898
+ return;
1899
+ if (this.#thumbsRendering.has(pageNum))
1900
+ return;
1901
+ if (this.#thumbCache()[pageNum]) {
1902
+ // Already cached → just blit the dataURL onto the canvas.
1903
+ this.#paintCachedThumb(canvas, this.#thumbCache()[pageNum]);
1904
+ return;
1905
+ }
1906
+ this.#thumbsRendering.add(pageNum);
1907
+ try {
1908
+ const page = await doc.getPage(pageNum);
1909
+ // Target ~140px wide for sidebar thumbs (handoff says 152px, leave a
1910
+ // small margin so the canvas itself can fit inside the page wrapper's
1911
+ // outline). PDF page viewport at scale=1 ≈ 612 wide; pick scale so the
1912
+ // result lands near 140px.
1913
+ const baseViewport = page.getViewport({ scale: 1 });
1914
+ const scale = 140 / Math.max(1, baseViewport.width);
1915
+ const viewport = page.getViewport({ scale });
1916
+ canvas.width = Math.floor(viewport.width);
1917
+ canvas.height = Math.floor(viewport.height);
1918
+ canvas.style.width = `${viewport.width}px`;
1919
+ canvas.style.height = `${viewport.height}px`;
1920
+ const ctx = canvas.getContext('2d');
1921
+ if (!ctx)
1922
+ return;
1923
+ const renderTask = page.render({ canvasContext: ctx, viewport });
1924
+ try {
1925
+ await renderTask.promise;
1926
+ }
1927
+ catch {
1928
+ return; // cancelled / failed silently — keep the blank thumb
1929
+ }
1930
+ try {
1931
+ const dataUrl = canvas.toDataURL('image/png');
1932
+ this.#thumbCache.update(prev => ({ ...prev, [pageNum]: dataUrl }));
1933
+ }
1934
+ catch {
1935
+ // Tainted canvas (e.g. dev mock) — just skip caching, keep visual.
1936
+ }
1937
+ page.cleanup();
1938
+ }
1939
+ catch {
1940
+ // Page load failed — leave the thumb blank rather than throwing.
1941
+ }
1942
+ finally {
1943
+ this.#thumbsRendering.delete(pageNum);
1944
+ }
1945
+ }
1946
+ /**
1947
+ * Paint a previously-rendered dataURL onto a freshly-mounted canvas so the
1948
+ * mini-page reappears without re-rasterizing.
1949
+ */
1950
+ #paintCachedThumb(canvas, dataUrl) {
1951
+ const img = new Image();
1952
+ img.onload = () => {
1953
+ canvas.width = img.naturalWidth;
1954
+ canvas.height = img.naturalHeight;
1955
+ canvas.style.width = `${img.naturalWidth}px`;
1956
+ canvas.style.height = `${img.naturalHeight}px`;
1957
+ const ctx = canvas.getContext('2d');
1958
+ ctx?.drawImage(img, 0, 0);
1959
+ };
1960
+ img.src = dataUrl;
1961
+ }
1962
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdPreviewPdf, deps: [], target: i0.ɵɵFactoryTarget.Component });
1963
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.21", type: SdPreviewPdf, isStandalone: true, selector: "sd-preview-pdf", inputs: { autoIdInput: { classPropertyName: "autoIdInput", publicName: "autoId", isSignal: true, isRequired: false, transformFunction: null }, source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, startPage: { classPropertyName: "startPage", publicName: "startPage", isSignal: true, isRequired: false, transformFunction: null }, initialZoom: { classPropertyName: "initialZoom", publicName: "initialZoom", isSignal: true, isRequired: false, transformFunction: null }, sidebar: { classPropertyName: "sidebar", publicName: "sidebar", isSignal: true, isRequired: false, transformFunction: null }, sidebarOpen: { classPropertyName: "sidebarOpen", publicName: "sidebarOpen", isSignal: true, isRequired: false, transformFunction: null }, scrollMode: { classPropertyName: "scrollMode", publicName: "scrollMode", isSignal: true, isRequired: false, transformFunction: null }, showToolbar: { classPropertyName: "showToolbar", publicName: "showToolbar", isSignal: true, isRequired: false, transformFunction: null }, downloadable: { classPropertyName: "downloadable", publicName: "downloadable", isSignal: true, isRequired: false, transformFunction: null }, password: { classPropertyName: "password", publicName: "password", isSignal: true, isRequired: false, transformFunction: null }, httpHeaders: { classPropertyName: "httpHeaders", publicName: "httpHeaders", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { close: "close", loaded: "loaded", pageChange: "pageChange", zoomChange: "zoomChange", download: "download", loadError: "loadError", searchChange: "searchChange" }, host: { attributes: { "tabindex": "0" }, listeners: { "keydown": "onKeyDown($event)" }, properties: { "attr.data-theme": "theme()", "class.sd-preview-pdf-host--fullscreen": "isFullscreen()" }, classAttribute: "sd-preview-pdf-host" }, viewQueries: [{ propertyName: "pageCanvasRef", first: true, predicate: ["pageCanvas"], descendants: true, isSignal: true }, { propertyName: "stageEl", first: true, predicate: ["stageRef"], descendants: true, isSignal: true }, { propertyName: "pageInputRef", first: true, predicate: ["pageInput"], descendants: true, isSignal: true }, { propertyName: "searchInputRef", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }, { propertyName: "thumbCanvases", predicate: ["thumbCanvas"], descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n sd-preview-pdf shell. Self-contained \u2014 sizes to its parent. Consumer wraps\n in <sd-modal>/<sd-side-drawer>/page section. Header + sidebar + stage +\n floating toolbar mirror Artboards A / C / E from the design handoff.\n-->\n<div class=\"sd-preview-pdf-shell\" role=\"region\">\n\n <!-- ============================================\n HEADER BAR \u2014 hidden in empty + loading per design\n ============================================ -->\n @if (stage() === 'ready') {\n <div class=\"sd-preview-pdf-header\">\n <div class=\"sd-preview-pdf-header__left\">\n <button\n type=\"button\"\n class=\"sd-preview-pdf-iconbtn\"\n [class.sd-preview-pdf-iconbtn--active]=\"isSidebarOpen()\"\n [attr.aria-label]=\"'core.component.preview-pdf.toggle-sidebar' | translate\"\n [attr.title]=\"'core.component.preview-pdf.toggle-sidebar' | translate\"\n [attr.data-autoId]=\"autoIdSidebarToggle()\"\n (click)=\"toggleSidebar()\">\n <mat-icon fontIcon=\"menu\"></mat-icon>\n </button>\n <span class=\"sd-preview-pdf-header__divider\"></span>\n <div class=\"sd-preview-pdf-fileicon\" aria-hidden=\"true\">PDF</div>\n <div class=\"sd-preview-pdf-header__meta\">\n <div class=\"sd-preview-pdf-header__name\" [title]=\"title() || filename()\">\n {{ title() || filename() }}\n </div>\n <div class=\"sd-preview-pdf-header__sub\">\n {{ 'core.component.preview-pdf.pages-count' | translate: { count: numPages() } }}\n </div>\n </div>\n </div>\n\n <div class=\"sd-preview-pdf-header__right\">\n <button\n type=\"button\"\n class=\"sd-preview-pdf-iconbtn\"\n [class.sd-preview-pdf-iconbtn--active]=\"searchBarOpen()\"\n [attr.aria-label]=\"'core.component.preview-pdf.search' | translate\"\n [attr.title]=\"'core.component.preview-pdf.search' | translate\"\n [attr.data-autoId]=\"autoIdSearchToggle()\"\n (click)=\"searchBarOpen() ? closeSearch() : openSearch()\">\n <mat-icon fontIcon=\"search\"></mat-icon>\n </button>\n @if (downloadable()) {\n <button\n type=\"button\"\n class=\"sd-preview-pdf-iconbtn\"\n [attr.aria-label]=\"'core.component.preview-pdf.download' | translate\"\n [attr.title]=\"'core.component.preview-pdf.download' | translate\"\n [attr.data-autoId]=\"autoIdDownload()\"\n (click)=\"downloadFile()\">\n <mat-icon fontIcon=\"download\"></mat-icon>\n </button>\n }\n <button\n type=\"button\"\n class=\"sd-preview-pdf-iconbtn\"\n [attr.aria-label]=\"'core.component.preview-pdf.fullscreen' | translate\"\n [attr.title]=\"'core.component.preview-pdf.fullscreen' | translate\"\n [attr.data-autoId]=\"autoIdFullscreen()\"\n (click)=\"toggleFullscreen()\">\n <mat-icon [fontIcon]=\"isFullscreen() ? 'fullscreen_exit' : 'fullscreen'\"></mat-icon>\n </button>\n </div>\n </div>\n\n <!-- ============================================\n SEARCH BAR \u2014 between header + body, only when bar is open\n ============================================ -->\n @if (searchBarOpen()) {\n <div class=\"sd-preview-pdf-searchbar\" role=\"search\">\n <mat-icon class=\"sd-preview-pdf-searchbar__icon\" fontIcon=\"search\"></mat-icon>\n <input\n #searchInput\n type=\"text\"\n class=\"sd-preview-pdf-searchbar__input\"\n [attr.placeholder]=\"'core.component.preview-pdf.search-placeholder' | translate\"\n [value]=\"searchTerm()\"\n [attr.data-autoId]=\"autoIdSearchInput()\"\n (input)=\"onSearchInput($any($event.target).value)\" />\n <span class=\"sd-preview-pdf-searchbar__counter\" aria-live=\"polite\">\n {{ searchCurrent() }} / {{ searchTotal() }}\n </span>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-iconbtn\"\n [disabled]=\"searchTotal() === 0\"\n [attr.aria-label]=\"'core.component.preview-pdf.search-prev' | translate\"\n [attr.title]=\"'core.component.preview-pdf.search-prev' | translate\"\n [attr.data-autoId]=\"autoIdSearchPrev()\"\n (click)=\"searchPrev()\">\n <mat-icon fontIcon=\"keyboard_arrow_up\"></mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-iconbtn\"\n [disabled]=\"searchTotal() === 0\"\n [attr.aria-label]=\"'core.component.preview-pdf.search-next' | translate\"\n [attr.title]=\"'core.component.preview-pdf.search-next' | translate\"\n [attr.data-autoId]=\"autoIdSearchNext()\"\n (click)=\"searchNext()\">\n <mat-icon fontIcon=\"keyboard_arrow_down\"></mat-icon>\n </button>\n <span class=\"sd-preview-pdf-searchbar__divider\"></span>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-iconbtn\"\n [class.sd-preview-pdf-iconbtn--active]=\"searchCaseSensitive()\"\n [attr.aria-label]=\"'core.component.preview-pdf.search-case' | translate\"\n [attr.title]=\"'core.component.preview-pdf.search-case' | translate\"\n [attr.data-autoId]=\"autoIdSearchCase()\"\n (click)=\"toggleSearchCaseSensitive()\">\n <mat-icon fontIcon=\"match_case\"></mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-iconbtn\"\n [class.sd-preview-pdf-iconbtn--active]=\"searchWholeWord()\"\n [attr.aria-label]=\"'core.component.preview-pdf.search-whole' | translate\"\n [attr.title]=\"'core.component.preview-pdf.search-whole' | translate\"\n [attr.data-autoId]=\"autoIdSearchWhole()\"\n (click)=\"toggleSearchWholeWord()\">\n <mat-icon fontIcon=\"text_fields\"></mat-icon>\n </button>\n <span class=\"sd-preview-pdf-searchbar__spacer\"></span>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-iconbtn\"\n [attr.aria-label]=\"'core.component.preview-pdf.search-close' | translate\"\n [attr.title]=\"'core.component.preview-pdf.search-close' | translate\"\n [attr.data-autoId]=\"autoIdSearchClose()\"\n (click)=\"closeSearch()\">\n <mat-icon fontIcon=\"close\"></mat-icon>\n </button>\n </div>\n }\n }\n\n <!-- ============================================\n BODY \u2014 sidebar + stage\n ============================================ -->\n <div class=\"sd-preview-pdf-body\">\n\n <!-- SIDEBAR ............................................................. -->\n @if (stage() === 'ready' && isSidebarOpen()) {\n <aside class=\"sd-preview-pdf-sidebar\" role=\"complementary\">\n <div class=\"sd-preview-pdf-sidebar__tabs\" role=\"tablist\">\n <button\n type=\"button\"\n class=\"sd-preview-pdf-sidebar__tab\"\n role=\"tab\"\n [class.sd-preview-pdf-sidebar__tab--active]=\"sidebarMode() === 'thumbnails'\"\n [attr.data-autoId]=\"autoIdTabThumbnails()\"\n (click)=\"setSidebarMode('thumbnails')\">\n <mat-icon fontIcon=\"view_carousel\"></mat-icon>\n <span>{{ 'core.component.preview-pdf.tab.thumbnails' | translate }}</span>\n </button>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-sidebar__tab\"\n role=\"tab\"\n [class.sd-preview-pdf-sidebar__tab--active]=\"sidebarMode() === 'outline'\"\n [attr.data-autoId]=\"autoIdTabOutline()\"\n (click)=\"setSidebarMode('outline')\">\n <mat-icon fontIcon=\"list\"></mat-icon>\n <span>{{ 'core.component.preview-pdf.tab.outline' | translate }}</span>\n </button>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-sidebar__tab\"\n role=\"tab\"\n [class.sd-preview-pdf-sidebar__tab--active]=\"sidebarMode() === 'search'\"\n [attr.data-autoId]=\"autoIdTabSearch()\"\n (click)=\"setSidebarMode('search')\">\n <mat-icon fontIcon=\"search\"></mat-icon>\n <span>{{ 'core.component.preview-pdf.tab.search' | translate }}</span>\n </button>\n </div>\n\n <div class=\"sd-preview-pdf-sidebar__content\">\n @switch (sidebarMode()) {\n @case ('thumbnails') {\n <ul class=\"sd-preview-pdf-thumb-list\">\n @for (p of pageNumbers(); track trackByPage($index, p); let pi = $index) {\n <li>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-thumb\"\n [class.sd-preview-pdf-thumb--active]=\"p === activePage()\"\n [attr.aria-current]=\"p === activePage() ? 'page' : null\"\n [attr.aria-label]=\"('core.component.preview-pdf.page-of' | translate: { current: p, total: numPages() })\"\n [attr.data-autoId]=\"autoIdThumb(pi)\"\n (click)=\"goToPage(p)\">\n <div class=\"sd-preview-pdf-thumb__page\">\n <canvas\n #thumbCanvas\n class=\"sd-preview-pdf-thumb__canvas\"\n [attr.data-page]=\"p\"\n aria-hidden=\"true\"></canvas>\n </div>\n <span class=\"sd-preview-pdf-thumb__label\">{{ p }}</span>\n </button>\n </li>\n }\n </ul>\n }\n @case ('outline') {\n <!-- TODO(preview-pdf): outline tree (pdfDoc.getOutline()) deferred to follow-up commit. -->\n <div class=\"sd-preview-pdf-sidebar__deferred\">\n <mat-icon fontIcon=\"list\"></mat-icon>\n <p>{{ 'core.component.preview-pdf.deferred.outline' | translate }}</p>\n </div>\n }\n @case ('search') {\n @if (!searchTerm()) {\n <div class=\"sd-preview-pdf-sidebar__deferred\">\n <mat-icon fontIcon=\"search\"></mat-icon>\n <p>{{ 'core.component.preview-pdf.search-placeholder' | translate }}</p>\n </div>\n } @else if (searchTotal() === 0) {\n <div class=\"sd-preview-pdf-sidebar__deferred\">\n <mat-icon fontIcon=\"search_off\"></mat-icon>\n <p>{{ 'core.component.preview-pdf.search-no-results' | translate }}</p>\n </div>\n } @else {\n <ul class=\"sd-preview-pdf-search-list\">\n @for (r of searchResults(); track $index; let i = $index) {\n <li>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-search-result\"\n [class.sd-preview-pdf-search-result--active]=\"i === searchActiveIndex()\"\n [attr.data-autoId]=\"autoIdResult(i)\"\n (click)=\"goToPage(r.page)\">\n <span class=\"sd-preview-pdf-search-result__snippet\">\n {{ r.before }}<mark>{{ r.term }}</mark>{{ r.after }}\n </span>\n <span class=\"sd-preview-pdf-search-result__page\">{{ r.page }}</span>\n </button>\n </li>\n }\n </ul>\n }\n }\n @default {\n <div class=\"sd-preview-pdf-sidebar__deferred\"></div>\n }\n }\n </div>\n </aside>\n }\n\n <!-- STAGE ............................................................... -->\n <div class=\"sd-preview-pdf-stage\" #stageRef (wheel)=\"onWheel($event)\">\n @switch (stage()) {\n @case ('empty') {\n <div class=\"sd-preview-pdf-status\">\n <div class=\"sd-preview-pdf-status__tile sd-preview-pdf-status__tile--muted\">\n <mat-icon fontIcon=\"picture_as_pdf\"></mat-icon>\n </div>\n <div class=\"sd-preview-pdf-status__title\">\n {{ 'core.component.preview-pdf.empty.title' | translate }}\n </div>\n <div class=\"sd-preview-pdf-status__hint\">\n {{ 'core.component.preview-pdf.empty.subtitle' | translate }}\n </div>\n </div>\n }\n @case ('loading') {\n <div class=\"sd-preview-pdf-status\">\n <div class=\"sd-preview-pdf-spinner\" role=\"progressbar\"></div>\n <div class=\"sd-preview-pdf-status__title\">\n {{ 'core.component.preview-pdf.loading' | translate }}\n @if (loadPercent() > 0) {\n <span class=\"sd-preview-pdf-progress-pct\">&nbsp;{{ loadPercent() }}%</span>\n }\n </div>\n <div class=\"sd-preview-pdf-progress\">\n <div class=\"sd-preview-pdf-progress__fill\" [style.width.%]=\"loadPercent()\"></div>\n </div>\n </div>\n }\n @case ('error') {\n @if (loadError$(); as err) {\n <div class=\"sd-preview-pdf-status\">\n <div class=\"sd-preview-pdf-status__tile sd-preview-pdf-status__tile--danger\">\n <mat-icon fontIcon=\"error_outline\"></mat-icon>\n </div>\n <div class=\"sd-preview-pdf-status__title\">\n {{ ('core.component.preview-pdf.error.' + err.reason + '.title') | translate }}\n </div>\n <div class=\"sd-preview-pdf-status__hint\">\n {{ ('core.component.preview-pdf.error.' + err.reason + '.body') | translate }}\n </div>\n <div class=\"sd-preview-pdf-status__actions\">\n <button type=\"button\" class=\"sd-preview-pdf-btn-primary\" (click)=\"retryLoad()\" [attr.data-autoId]=\"autoIdRetry()\">\n <mat-icon fontIcon=\"refresh\"></mat-icon>\n <span>{{ 'core.component.preview-pdf.error.retry' | translate }}</span>\n </button>\n @if (downloadable()) {\n <button type=\"button\" class=\"sd-preview-pdf-btn-secondary\" (click)=\"downloadFile()\">\n <mat-icon fontIcon=\"download\"></mat-icon>\n <span>{{ 'core.component.preview-pdf.error.download-raw' | translate }}</span>\n </button>\n }\n </div>\n </div>\n }\n }\n @default {\n <div class=\"sd-preview-pdf-page-wrap\">\n <canvas\n #pageCanvas\n class=\"sd-preview-pdf-page\"\n role=\"img\"\n [attr.aria-label]=\"('core.component.preview-pdf.page-of' | translate: { current: activePage(), total: numPages() })\">\n </canvas>\n </div>\n }\n }\n\n <!-- FLOATING TOOLBAR ........................................ -->\n @if (showToolbar() && stage() === 'ready') {\n <div class=\"sd-preview-pdf-toolbar\" role=\"toolbar\">\n <button\n type=\"button\"\n class=\"sd-preview-pdf-toolbar__btn\"\n [disabled]=\"!canPrev()\"\n [attr.aria-label]=\"'core.component.preview-pdf.first-page' | translate\"\n [attr.title]=\"'core.component.preview-pdf.first-page' | translate\"\n [attr.data-autoId]=\"autoIdFirst()\"\n (click)=\"firstPage()\">\n <mat-icon fontIcon=\"first_page\"></mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-toolbar__btn\"\n [disabled]=\"!canPrev()\"\n [attr.aria-label]=\"'core.component.preview-pdf.prev-page' | translate\"\n [attr.title]=\"'core.component.preview-pdf.prev-page' | translate\"\n [attr.data-autoId]=\"autoIdPrev()\"\n (click)=\"prevPage()\">\n <mat-icon fontIcon=\"chevron_left\"></mat-icon>\n </button>\n <span class=\"sd-preview-pdf-toolbar__page\">\n <input\n type=\"text\"\n inputmode=\"numeric\"\n class=\"sd-preview-pdf-toolbar__page-input\"\n [value]=\"activePage()\"\n #pageInput\n (keydown.enter)=\"onPageInputEnter($event)\"\n (blur)=\"onPageInputBlur($event)\"\n [attr.aria-label]=\"'core.component.preview-pdf.page-of' | translate: { current: activePage(), total: numPages() }\"\n [attr.data-autoId]=\"autoIdPageInput()\" />\n <span class=\"sd-preview-pdf-toolbar__page-total\">/ {{ numPages() }}</span>\n </span>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-toolbar__btn\"\n [disabled]=\"!canNext()\"\n [attr.aria-label]=\"'core.component.preview-pdf.next-page' | translate\"\n [attr.title]=\"'core.component.preview-pdf.next-page' | translate\"\n [attr.data-autoId]=\"autoIdNext()\"\n (click)=\"nextPage()\">\n <mat-icon fontIcon=\"chevron_right\"></mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-toolbar__btn\"\n [disabled]=\"!canNext()\"\n [attr.aria-label]=\"'core.component.preview-pdf.last-page' | translate\"\n [attr.title]=\"'core.component.preview-pdf.last-page' | translate\"\n [attr.data-autoId]=\"autoIdLast()\"\n (click)=\"lastPage()\">\n <mat-icon fontIcon=\"last_page\"></mat-icon>\n </button>\n\n <span class=\"sd-preview-pdf-toolbar__divider\"></span>\n\n <button\n type=\"button\"\n class=\"sd-preview-pdf-toolbar__btn\"\n [attr.aria-label]=\"'core.component.preview-pdf.zoom-out' | translate\"\n [attr.title]=\"'core.component.preview-pdf.zoom-out' | translate\"\n [attr.data-autoId]=\"autoIdZoomOut()\"\n (click)=\"zoomOut()\">\n <mat-icon fontIcon=\"remove\"></mat-icon>\n </button>\n <span class=\"sd-preview-pdf-toolbar__zoom\" aria-live=\"polite\" [attr.data-autoId]=\"autoIdZoomReadout()\">{{ zoomPercent() }}%</span>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-toolbar__btn\"\n [attr.aria-label]=\"'core.component.preview-pdf.zoom-in' | translate\"\n [attr.title]=\"'core.component.preview-pdf.zoom-in' | translate\"\n [attr.data-autoId]=\"autoIdZoomIn()\"\n (click)=\"zoomIn()\">\n <mat-icon fontIcon=\"add\"></mat-icon>\n </button>\n\n <span class=\"sd-preview-pdf-toolbar__divider\"></span>\n\n <button\n type=\"button\"\n class=\"sd-preview-pdf-toolbar__btn sd-preview-pdf-toolbar__btn--with-label\"\n [class.sd-preview-pdf-toolbar__btn--active]=\"zoomMode() === 'page-fit'\"\n [attr.data-autoId]=\"autoIdFitPage()\"\n (click)=\"setZoom('page-fit')\">\n <mat-icon fontIcon=\"fit_screen\"></mat-icon>\n <span>{{ 'core.component.preview-pdf.fit-page' | translate }}</span>\n </button>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-toolbar__btn sd-preview-pdf-toolbar__btn--with-label\"\n [class.sd-preview-pdf-toolbar__btn--active]=\"zoomMode() === 'page-width'\"\n [attr.data-autoId]=\"autoIdFitWidth()\"\n (click)=\"setZoom('page-width')\">\n <mat-icon fontIcon=\"swap_horiz\"></mat-icon>\n <span>{{ 'core.component.preview-pdf.fit-width' | translate }}</span>\n </button>\n\n <span class=\"sd-preview-pdf-toolbar__divider\"></span>\n\n <button\n type=\"button\"\n class=\"sd-preview-pdf-toolbar__btn\"\n [attr.aria-label]=\"'core.component.preview-pdf.rotate' | translate\"\n [attr.title]=\"'core.component.preview-pdf.rotate' | translate\"\n [attr.data-autoId]=\"autoIdRotate()\"\n (click)=\"rotate('right')\">\n <mat-icon fontIcon=\"rotate_right\"></mat-icon>\n </button>\n\n <!-- TODO(preview-pdf): scroll mode toggle is signature-only;\n clicking switches the state but rendering stays single-page\n until the continuous scroll feature lands. -->\n <button\n type=\"button\"\n class=\"sd-preview-pdf-toolbar__btn sd-preview-pdf-toolbar__btn--with-label\"\n [attr.data-autoId]=\"autoIdScrollMode()\"\n (click)=\"setScrollMode(scrollModeCurrent() === 'page' ? 'continuous' : 'page')\">\n <mat-icon [fontIcon]=\"scrollModeCurrent() === 'page' ? 'description' : 'view_day'\"></mat-icon>\n <span>\n {{ (scrollModeCurrent() === 'page'\n ? 'core.component.preview-pdf.scroll-page'\n : 'core.component.preview-pdf.scroll-continuous') | translate }}\n </span>\n </button>\n </div>\n }\n </div>\n </div>\n</div>\n", styles: [":host{--sd-pdf-shell-bg: #1a1c1f;--sd-pdf-sidebar-bg: #22252a;--sd-pdf-toolbar-bg: rgba(20, 22, 25, .94);--sd-pdf-page-bg: #ffffff;--sd-pdf-page-shadow: 0 4px 16px rgba(0, 0, 0, .4);--sd-pdf-divider: rgba(255, 255, 255, .08);--sd-pdf-fg: #f5f6f7;--sd-pdf-fg-muted: rgba(255, 255, 255, .6);--sd-pdf-fg-subtle: rgba(255, 255, 255, .4);--sd-pdf-accent: var(--sd-primary, #3b82f6);--sd-pdf-accent-soft: rgba(59, 130, 246, .2);--sd-pdf-danger: var(--sd-error, #ef4444);--sd-pdf-success: var(--sd-success, #22c55e);--sd-pdf-danger-soft: rgba(239, 68, 68, .1);--sd-pdf-danger-border: rgba(239, 68, 68, .3);--sd-pdf-danger-tint: rgba(239, 68, 68, .15);--sd-pdf-hover-bg: rgba(255, 255, 255, .1);--sd-pdf-search-hi: rgba(250, 204, 21, .55);--sd-pdf-search-hi-active: rgba(249, 115, 22, .85);display:block;width:100%;height:100%;min-height:320px;outline:none}:host([data-theme=light]){--sd-pdf-shell-bg: #f7f8fa;--sd-pdf-sidebar-bg: #ffffff;--sd-pdf-toolbar-bg: rgba(255, 255, 255, .94);--sd-pdf-page-bg: #ffffff;--sd-pdf-page-shadow: 0 4px 16px rgba(0, 0, 0, .12);--sd-pdf-divider: rgba(0, 0, 0, .08);--sd-pdf-fg: #1a1c1f;--sd-pdf-fg-muted: rgba(0, 0, 0, .62);--sd-pdf-fg-subtle: rgba(0, 0, 0, .42);--sd-pdf-accent: var(--sd-primary, #1f49bf);--sd-pdf-accent-soft: rgba(31, 73, 191, .18);--sd-pdf-danger: var(--sd-error, #ef4444);--sd-pdf-success: var(--sd-success, #22c55e);--sd-pdf-danger-soft: rgba(239, 68, 68, .08);--sd-pdf-danger-border: rgba(239, 68, 68, .25);--sd-pdf-danger-tint: rgba(239, 68, 68, .12);--sd-pdf-hover-bg: rgba(0, 0, 0, .06);--sd-pdf-search-hi: rgba(250, 204, 21, .55);--sd-pdf-search-hi-active: rgba(250, 204, 21, .9)}:host(.sd-preview-pdf-host--fullscreen){width:100vw;height:100vh;min-height:0}.sd-preview-pdf-shell{position:relative;display:flex;flex-direction:column;width:100%;height:100%;background:var(--sd-pdf-shell-bg);color:var(--sd-pdf-fg);border-radius:12px;overflow:hidden;box-shadow:0 30px 80px #0000008c,0 0 0 1px #ffffff0a;font-family:inherit}.sd-preview-pdf-header{display:flex;align-items:center;justify-content:space-between;gap:12px;height:57px;padding:0 12px;background:var(--sd-pdf-toolbar-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--sd-pdf-divider);flex:0 0 auto;z-index:2}.sd-preview-pdf-header__left{display:flex;align-items:center;gap:8px;min-width:0;flex:1 1 auto}.sd-preview-pdf-header__right{display:flex;align-items:center;gap:4px;flex-shrink:0}.sd-preview-pdf-header__divider{display:inline-block;width:1px;height:20px;background:var(--sd-pdf-divider)}.sd-preview-pdf-header__meta{display:flex;flex-direction:column;min-width:0}.sd-preview-pdf-header__name{font-size:14px;font-weight:500;color:var(--sd-pdf-fg);max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-preview-pdf-header__sub{font-size:11px;color:var(--sd-pdf-fg-muted);margin-top:2px}.sd-preview-pdf-fileicon{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:6px;background:var(--sd-pdf-danger-tint);border:1px solid var(--sd-pdf-danger-border);color:var(--sd-error-light, #f87171);font:700 10px/1 ui-monospace,SFMono-Regular,Menlo,monospace}.sd-preview-pdf-iconbtn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-width:32px;height:32px;padding:0 8px;background:transparent;border:none;color:var(--sd-pdf-fg);border-radius:6px;cursor:pointer;transition:background .12s,color .12s;white-space:nowrap;flex-shrink:0}.sd-preview-pdf-iconbtn mat-icon{font-size:18px;width:18px;height:18px}.sd-preview-pdf-iconbtn__label{font-size:13px;font-weight:500}.sd-preview-pdf-iconbtn:hover:not(:disabled){background:var(--sd-pdf-hover-bg)}.sd-preview-pdf-iconbtn:focus-visible{outline:2px solid var(--sd-pdf-accent);outline-offset:2px}.sd-preview-pdf-iconbtn:disabled{opacity:.35;cursor:not-allowed}.sd-preview-pdf-iconbtn--active{background:var(--sd-pdf-accent-soft);color:var(--sd-pdf-accent)}.sd-preview-pdf-searchbar{display:flex;align-items:center;gap:6px;height:46px;padding:0 12px;background:var(--sd-pdf-toolbar-bg);border-bottom:1px solid var(--sd-pdf-divider);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);flex:0 0 auto;z-index:2}.sd-preview-pdf-searchbar__icon{color:var(--sd-pdf-fg-muted);font-size:18px;width:18px;height:18px;flex-shrink:0}.sd-preview-pdf-searchbar__input{flex:0 1 360px;min-width:160px;height:30px;background:#ffffff0f;border:1px solid rgba(255,255,255,.08);color:var(--sd-pdf-fg);border-radius:6px;padding:0 10px;font-size:13px;outline:none}.sd-preview-pdf-searchbar__input::placeholder{color:var(--sd-pdf-fg-subtle)}.sd-preview-pdf-searchbar__input:focus{border-color:var(--sd-pdf-accent);background:#ffffff17}.sd-preview-pdf-searchbar__counter{color:var(--sd-pdf-fg-muted);font-size:12px;font-variant-numeric:tabular-nums;min-width:48px;text-align:center;flex-shrink:0}.sd-preview-pdf-searchbar__divider{width:1px;height:20px;background:var(--sd-pdf-divider);flex-shrink:0}.sd-preview-pdf-searchbar__spacer{flex:1 1 auto}.sd-pdf-search-overlay{position:absolute;top:8px;left:50%;transform:translate(-50%);display:flex;flex-wrap:wrap;gap:4px;max-width:80%;pointer-events:none;z-index:5}.sd-pdf-search-hi{display:inline-block;padding:1px 4px;border-radius:2px;background:var(--sd-pdf-search-hi);color:#000;font-size:12px;font-weight:500}.sd-pdf-search-hi--active{background:var(--sd-pdf-search-hi-active);color:#fff;box-shadow:0 0 0 2px #fff6}.sd-preview-pdf-body{position:relative;flex:1 1 auto;min-height:0;display:flex;flex-direction:row;overflow:hidden}.sd-preview-pdf-sidebar{width:240px;display:flex;flex-direction:column;background:var(--sd-pdf-sidebar-bg);border-right:1px solid var(--sd-pdf-divider);flex-shrink:0;min-height:0}.sd-preview-pdf-sidebar__tabs{display:flex;gap:4px;padding:8px;border-bottom:1px solid var(--sd-pdf-divider)}.sd-preview-pdf-sidebar__tab{flex:1 1 0;display:inline-flex;align-items:center;justify-content:center;gap:4px;height:32px;padding:0 4px;background:transparent;border:none;color:var(--sd-pdf-fg-muted);border-radius:6px;cursor:pointer;font-size:12px;font-weight:500;white-space:nowrap;flex-shrink:0;transition:background .12s,color .12s}.sd-preview-pdf-sidebar__tab mat-icon{font-size:14px;width:14px;height:14px}.sd-preview-pdf-sidebar__tab:hover{background:#ffffff0f;color:var(--sd-pdf-fg)}.sd-preview-pdf-sidebar__tab--active{background:var(--sd-pdf-accent-soft);color:var(--sd-pdf-accent)}.sd-preview-pdf-sidebar__content{flex:1 1 auto;overflow:auto;padding:12px}.sd-preview-pdf-sidebar__deferred{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:32px 16px;text-align:center;color:var(--sd-pdf-fg-subtle)}.sd-preview-pdf-sidebar__deferred mat-icon{font-size:28px;width:28px;height:28px;opacity:.6}.sd-preview-pdf-sidebar__deferred p{margin:0;font-size:12px;line-height:1.5}.sd-preview-pdf-thumb-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:12px}.sd-preview-pdf-thumb{display:flex;flex-direction:column;align-items:center;gap:4px;width:100%;padding:0;background:transparent;border:none;cursor:pointer}.sd-preview-pdf-thumb__page{width:152px;aspect-ratio:612/792;background:var(--sd-pdf-page-bg);border-radius:2px;outline:1px solid rgba(255,255,255,.08);outline-offset:-1px;box-shadow:0 2px 6px #0000004d;display:flex;align-items:center;justify-content:center;overflow:hidden;-webkit-user-select:none;user-select:none;background-image:linear-gradient(90deg,#0000,#0000000a,#0000);background-size:200% 100%;animation:sd-preview-pdf-thumb-skel 1.6s ease-in-out infinite}.sd-preview-pdf-thumb__canvas{display:block;max-width:100%;max-height:100%;background:var(--sd-pdf-page-bg)}.sd-preview-pdf-thumb__label{font-size:11px;color:var(--sd-pdf-fg-muted);font-variant-numeric:tabular-nums}.sd-preview-pdf-thumb--active .sd-preview-pdf-thumb__page{outline:2px solid var(--sd-pdf-accent);outline-offset:-2px}.sd-preview-pdf-thumb--active .sd-preview-pdf-thumb__label{color:var(--sd-pdf-accent);font-weight:600}.sd-preview-pdf-thumb:focus-visible .sd-preview-pdf-thumb__page{outline:2px solid var(--sd-pdf-accent);outline-offset:2px}.sd-preview-pdf-stage{position:relative;flex:1 1 auto;min-width:0;min-height:0;background:var(--sd-pdf-shell-bg);background-image:linear-gradient(45deg,rgba(255,255,255,.015) 25%,transparent 25%),linear-gradient(-45deg,rgba(255,255,255,.015) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(255,255,255,.015) 75%),linear-gradient(-45deg,transparent 75%,rgba(255,255,255,.015) 75%);background-size:24px 24px;background-position:0 0,0 12px,12px -12px,-12px 0;overflow:auto;padding:32px 24px 96px;display:flex;justify-content:center;align-items:flex-start}:host([data-theme=light]) .sd-preview-pdf-stage{background-image:linear-gradient(45deg,rgba(0,0,0,.015) 25%,transparent 25%),linear-gradient(-45deg,rgba(0,0,0,.015) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(0,0,0,.015) 75%),linear-gradient(-45deg,transparent 75%,rgba(0,0,0,.015) 75%)}.sd-preview-pdf-page-wrap{display:inline-block;position:relative}.sd-preview-pdf-page{display:block;background:var(--sd-pdf-page-bg);border-radius:2px;box-shadow:var(--sd-pdf-page-shadow);max-width:100%}.sd-preview-pdf-toolbar{position:absolute;bottom:20px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:2px;padding:5px 6px;background:var(--sd-pdf-toolbar-bg);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border:1px solid var(--sd-pdf-divider);border-radius:10px;box-shadow:0 8px 30px #0006;z-index:4;max-width:calc(100% - 32px);overflow-x:auto}.sd-preview-pdf-toolbar__btn{display:inline-flex;align-items:center;justify-content:center;gap:4px;min-width:30px;height:28px;padding:0 6px;background:transparent;border:none;color:var(--sd-pdf-fg);border-radius:6px;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:background .12s,color .12s}.sd-preview-pdf-toolbar__btn mat-icon{font-size:18px;width:18px;height:18px}.sd-preview-pdf-toolbar__btn span{font-size:13px;font-weight:500}.sd-preview-pdf-toolbar__btn--with-label{padding:0 10px}.sd-preview-pdf-toolbar__btn--active{background:var(--sd-pdf-accent-soft);color:var(--sd-pdf-accent)}.sd-preview-pdf-toolbar__btn:hover:not(:disabled):not(.sd-preview-pdf-toolbar__btn--active){background:var(--sd-pdf-hover-bg)}.sd-preview-pdf-toolbar__btn:focus-visible{outline:2px solid var(--sd-pdf-accent);outline-offset:2px}.sd-preview-pdf-toolbar__btn:disabled{opacity:.35;cursor:not-allowed}.sd-preview-pdf-toolbar__page{display:inline-flex;align-items:center;gap:4px;padding:0 6px;flex-shrink:0}.sd-preview-pdf-toolbar__page-input{width:36px;height:24px;background:#ffffff14;border:1px solid rgba(255,255,255,.08);color:var(--sd-pdf-fg);border-radius:4px;text-align:center;font-size:13px;font-variant-numeric:tabular-nums;outline:none}.sd-preview-pdf-toolbar__page-input:focus{border-color:var(--sd-pdf-accent)}.sd-preview-pdf-toolbar__page-total{font-size:13px;color:var(--sd-pdf-fg-muted);font-variant-numeric:tabular-nums}.sd-preview-pdf-toolbar__zoom{min-width:56px;text-align:center;color:var(--sd-pdf-fg);font-size:13px;font-weight:500;font-variant-numeric:tabular-nums;background:#ffffff14;border-radius:4px;padding:2px 8px}.sd-preview-pdf-toolbar__divider{width:1px;height:20px;background:var(--sd-pdf-divider);margin:0 4px;flex-shrink:0}.sd-preview-pdf-status{margin:auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;text-align:center;padding:32px;max-width:420px}.sd-preview-pdf-status__tile{width:96px;height:96px;border-radius:24px;display:flex;align-items:center;justify-content:center;border:1px solid var(--sd-pdf-divider)}.sd-preview-pdf-status__tile mat-icon{font-size:48px;width:48px;height:48px}.sd-preview-pdf-status__tile--muted{background:#ffffff0d}.sd-preview-pdf-status__tile--muted mat-icon{color:var(--sd-pdf-fg-subtle)}.sd-preview-pdf-status__tile--danger{background:var(--sd-pdf-danger-soft);border-color:var(--sd-pdf-danger-border)}.sd-preview-pdf-status__tile--danger mat-icon{color:var(--sd-pdf-danger)}.sd-preview-pdf-status__title{font-size:16px;font-weight:500;color:var(--sd-pdf-fg)}.sd-preview-pdf-status__hint{font-size:13px;color:var(--sd-pdf-fg-muted);line-height:1.5}.sd-preview-pdf-status__actions{display:flex;gap:8px;margin-top:4px}.sd-preview-pdf-progress-pct{font-variant-numeric:tabular-nums;color:var(--sd-pdf-fg-muted)}.sd-preview-pdf-progress{width:240px;height:4px;background:#ffffff14;border-radius:999px;overflow:hidden}.sd-preview-pdf-progress__fill{height:100%;background:var(--sd-pdf-accent);transition:width .12s linear}.sd-preview-pdf-spinner{width:56px;height:56px;border-radius:999px;border:3px solid rgba(255,255,255,.08);border-top-color:var(--sd-pdf-accent);animation:sd-preview-pdf-spin .9s linear infinite}@keyframes sd-preview-pdf-spin{to{transform:rotate(360deg)}}@keyframes sd-preview-pdf-thumb-skel{0%{background-position:200% 0}to{background-position:-200% 0}}.sd-preview-pdf-search-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}.sd-preview-pdf-search-result{width:100%;display:flex;align-items:flex-start;gap:8px;padding:8px 10px;background:#ffffff0a;border:1px solid transparent;border-radius:6px;color:var(--sd-pdf-fg);cursor:pointer;text-align:left;transition:background .12s,border-color .12s}.sd-preview-pdf-search-result:hover{background:#ffffff12}.sd-preview-pdf-search-result--active{background:var(--sd-pdf-accent-soft);border-color:#3b82f673}.sd-preview-pdf-search-result__snippet{flex:1 1 auto;font-size:12px;line-height:1.4;color:var(--sd-pdf-fg);word-break:break-word}.sd-preview-pdf-search-result__snippet mark{background:var(--sd-pdf-search-hi);color:#000;padding:0 2px;border-radius:2px}.sd-preview-pdf-search-result__page{flex:0 0 auto;font-size:11px;font-variant-numeric:tabular-nums;color:var(--sd-pdf-fg-muted);background:#ffffff0f;padding:2px 6px;border-radius:4px}.sd-preview-pdf-btn-primary{display:inline-flex;align-items:center;gap:6px;height:36px;padding:0 16px;background:var(--sd-pdf-accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:13px;font-weight:500;transition:filter .12s}.sd-preview-pdf-btn-primary mat-icon{font-size:18px;width:18px;height:18px}.sd-preview-pdf-btn-primary:hover{filter:brightness(1.1)}.sd-preview-pdf-btn-primary:focus-visible{outline:2px solid #fff;outline-offset:2px}.sd-preview-pdf-btn-secondary{display:inline-flex;align-items:center;gap:6px;height:36px;padding:0 16px;background:transparent;color:var(--sd-pdf-fg);border:1px solid var(--sd-pdf-divider);border-radius:8px;cursor:pointer;font-size:13px;font-weight:500;transition:background .12s}.sd-preview-pdf-btn-secondary mat-icon{font-size:18px;width:18px;height:18px}.sd-preview-pdf-btn-secondary:hover{background:#ffffff0f}.sd-preview-pdf-btn-secondary:focus-visible{outline:2px solid var(--sd-pdf-accent);outline-offset:2px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1964
+ }
1965
+ _a = SdPreviewPdf;
1966
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdPreviewPdf, decorators: [{
1967
+ type: Component,
1968
+ args: [{ selector: 'sd-preview-pdf', standalone: true, imports: [CommonModule, FormsModule, MatIconModule, TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, host: {
1969
+ tabindex: '0',
1970
+ class: 'sd-preview-pdf-host',
1971
+ // WHY data-theme attribute: SCSS uses :host([data-theme="light"]) to swap
1972
+ // CSS custom properties — declarative, devtools-visible, no class-name
1973
+ // collision with consumer styling.
1974
+ '[attr.data-theme]': 'theme()',
1975
+ '[class.sd-preview-pdf-host--fullscreen]': 'isFullscreen()',
1976
+ }, template: "<!--\n sd-preview-pdf shell. Self-contained \u2014 sizes to its parent. Consumer wraps\n in <sd-modal>/<sd-side-drawer>/page section. Header + sidebar + stage +\n floating toolbar mirror Artboards A / C / E from the design handoff.\n-->\n<div class=\"sd-preview-pdf-shell\" role=\"region\">\n\n <!-- ============================================\n HEADER BAR \u2014 hidden in empty + loading per design\n ============================================ -->\n @if (stage() === 'ready') {\n <div class=\"sd-preview-pdf-header\">\n <div class=\"sd-preview-pdf-header__left\">\n <button\n type=\"button\"\n class=\"sd-preview-pdf-iconbtn\"\n [class.sd-preview-pdf-iconbtn--active]=\"isSidebarOpen()\"\n [attr.aria-label]=\"'core.component.preview-pdf.toggle-sidebar' | translate\"\n [attr.title]=\"'core.component.preview-pdf.toggle-sidebar' | translate\"\n [attr.data-autoId]=\"autoIdSidebarToggle()\"\n (click)=\"toggleSidebar()\">\n <mat-icon fontIcon=\"menu\"></mat-icon>\n </button>\n <span class=\"sd-preview-pdf-header__divider\"></span>\n <div class=\"sd-preview-pdf-fileicon\" aria-hidden=\"true\">PDF</div>\n <div class=\"sd-preview-pdf-header__meta\">\n <div class=\"sd-preview-pdf-header__name\" [title]=\"title() || filename()\">\n {{ title() || filename() }}\n </div>\n <div class=\"sd-preview-pdf-header__sub\">\n {{ 'core.component.preview-pdf.pages-count' | translate: { count: numPages() } }}\n </div>\n </div>\n </div>\n\n <div class=\"sd-preview-pdf-header__right\">\n <button\n type=\"button\"\n class=\"sd-preview-pdf-iconbtn\"\n [class.sd-preview-pdf-iconbtn--active]=\"searchBarOpen()\"\n [attr.aria-label]=\"'core.component.preview-pdf.search' | translate\"\n [attr.title]=\"'core.component.preview-pdf.search' | translate\"\n [attr.data-autoId]=\"autoIdSearchToggle()\"\n (click)=\"searchBarOpen() ? closeSearch() : openSearch()\">\n <mat-icon fontIcon=\"search\"></mat-icon>\n </button>\n @if (downloadable()) {\n <button\n type=\"button\"\n class=\"sd-preview-pdf-iconbtn\"\n [attr.aria-label]=\"'core.component.preview-pdf.download' | translate\"\n [attr.title]=\"'core.component.preview-pdf.download' | translate\"\n [attr.data-autoId]=\"autoIdDownload()\"\n (click)=\"downloadFile()\">\n <mat-icon fontIcon=\"download\"></mat-icon>\n </button>\n }\n <button\n type=\"button\"\n class=\"sd-preview-pdf-iconbtn\"\n [attr.aria-label]=\"'core.component.preview-pdf.fullscreen' | translate\"\n [attr.title]=\"'core.component.preview-pdf.fullscreen' | translate\"\n [attr.data-autoId]=\"autoIdFullscreen()\"\n (click)=\"toggleFullscreen()\">\n <mat-icon [fontIcon]=\"isFullscreen() ? 'fullscreen_exit' : 'fullscreen'\"></mat-icon>\n </button>\n </div>\n </div>\n\n <!-- ============================================\n SEARCH BAR \u2014 between header + body, only when bar is open\n ============================================ -->\n @if (searchBarOpen()) {\n <div class=\"sd-preview-pdf-searchbar\" role=\"search\">\n <mat-icon class=\"sd-preview-pdf-searchbar__icon\" fontIcon=\"search\"></mat-icon>\n <input\n #searchInput\n type=\"text\"\n class=\"sd-preview-pdf-searchbar__input\"\n [attr.placeholder]=\"'core.component.preview-pdf.search-placeholder' | translate\"\n [value]=\"searchTerm()\"\n [attr.data-autoId]=\"autoIdSearchInput()\"\n (input)=\"onSearchInput($any($event.target).value)\" />\n <span class=\"sd-preview-pdf-searchbar__counter\" aria-live=\"polite\">\n {{ searchCurrent() }} / {{ searchTotal() }}\n </span>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-iconbtn\"\n [disabled]=\"searchTotal() === 0\"\n [attr.aria-label]=\"'core.component.preview-pdf.search-prev' | translate\"\n [attr.title]=\"'core.component.preview-pdf.search-prev' | translate\"\n [attr.data-autoId]=\"autoIdSearchPrev()\"\n (click)=\"searchPrev()\">\n <mat-icon fontIcon=\"keyboard_arrow_up\"></mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-iconbtn\"\n [disabled]=\"searchTotal() === 0\"\n [attr.aria-label]=\"'core.component.preview-pdf.search-next' | translate\"\n [attr.title]=\"'core.component.preview-pdf.search-next' | translate\"\n [attr.data-autoId]=\"autoIdSearchNext()\"\n (click)=\"searchNext()\">\n <mat-icon fontIcon=\"keyboard_arrow_down\"></mat-icon>\n </button>\n <span class=\"sd-preview-pdf-searchbar__divider\"></span>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-iconbtn\"\n [class.sd-preview-pdf-iconbtn--active]=\"searchCaseSensitive()\"\n [attr.aria-label]=\"'core.component.preview-pdf.search-case' | translate\"\n [attr.title]=\"'core.component.preview-pdf.search-case' | translate\"\n [attr.data-autoId]=\"autoIdSearchCase()\"\n (click)=\"toggleSearchCaseSensitive()\">\n <mat-icon fontIcon=\"match_case\"></mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-iconbtn\"\n [class.sd-preview-pdf-iconbtn--active]=\"searchWholeWord()\"\n [attr.aria-label]=\"'core.component.preview-pdf.search-whole' | translate\"\n [attr.title]=\"'core.component.preview-pdf.search-whole' | translate\"\n [attr.data-autoId]=\"autoIdSearchWhole()\"\n (click)=\"toggleSearchWholeWord()\">\n <mat-icon fontIcon=\"text_fields\"></mat-icon>\n </button>\n <span class=\"sd-preview-pdf-searchbar__spacer\"></span>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-iconbtn\"\n [attr.aria-label]=\"'core.component.preview-pdf.search-close' | translate\"\n [attr.title]=\"'core.component.preview-pdf.search-close' | translate\"\n [attr.data-autoId]=\"autoIdSearchClose()\"\n (click)=\"closeSearch()\">\n <mat-icon fontIcon=\"close\"></mat-icon>\n </button>\n </div>\n }\n }\n\n <!-- ============================================\n BODY \u2014 sidebar + stage\n ============================================ -->\n <div class=\"sd-preview-pdf-body\">\n\n <!-- SIDEBAR ............................................................. -->\n @if (stage() === 'ready' && isSidebarOpen()) {\n <aside class=\"sd-preview-pdf-sidebar\" role=\"complementary\">\n <div class=\"sd-preview-pdf-sidebar__tabs\" role=\"tablist\">\n <button\n type=\"button\"\n class=\"sd-preview-pdf-sidebar__tab\"\n role=\"tab\"\n [class.sd-preview-pdf-sidebar__tab--active]=\"sidebarMode() === 'thumbnails'\"\n [attr.data-autoId]=\"autoIdTabThumbnails()\"\n (click)=\"setSidebarMode('thumbnails')\">\n <mat-icon fontIcon=\"view_carousel\"></mat-icon>\n <span>{{ 'core.component.preview-pdf.tab.thumbnails' | translate }}</span>\n </button>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-sidebar__tab\"\n role=\"tab\"\n [class.sd-preview-pdf-sidebar__tab--active]=\"sidebarMode() === 'outline'\"\n [attr.data-autoId]=\"autoIdTabOutline()\"\n (click)=\"setSidebarMode('outline')\">\n <mat-icon fontIcon=\"list\"></mat-icon>\n <span>{{ 'core.component.preview-pdf.tab.outline' | translate }}</span>\n </button>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-sidebar__tab\"\n role=\"tab\"\n [class.sd-preview-pdf-sidebar__tab--active]=\"sidebarMode() === 'search'\"\n [attr.data-autoId]=\"autoIdTabSearch()\"\n (click)=\"setSidebarMode('search')\">\n <mat-icon fontIcon=\"search\"></mat-icon>\n <span>{{ 'core.component.preview-pdf.tab.search' | translate }}</span>\n </button>\n </div>\n\n <div class=\"sd-preview-pdf-sidebar__content\">\n @switch (sidebarMode()) {\n @case ('thumbnails') {\n <ul class=\"sd-preview-pdf-thumb-list\">\n @for (p of pageNumbers(); track trackByPage($index, p); let pi = $index) {\n <li>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-thumb\"\n [class.sd-preview-pdf-thumb--active]=\"p === activePage()\"\n [attr.aria-current]=\"p === activePage() ? 'page' : null\"\n [attr.aria-label]=\"('core.component.preview-pdf.page-of' | translate: { current: p, total: numPages() })\"\n [attr.data-autoId]=\"autoIdThumb(pi)\"\n (click)=\"goToPage(p)\">\n <div class=\"sd-preview-pdf-thumb__page\">\n <canvas\n #thumbCanvas\n class=\"sd-preview-pdf-thumb__canvas\"\n [attr.data-page]=\"p\"\n aria-hidden=\"true\"></canvas>\n </div>\n <span class=\"sd-preview-pdf-thumb__label\">{{ p }}</span>\n </button>\n </li>\n }\n </ul>\n }\n @case ('outline') {\n <!-- TODO(preview-pdf): outline tree (pdfDoc.getOutline()) deferred to follow-up commit. -->\n <div class=\"sd-preview-pdf-sidebar__deferred\">\n <mat-icon fontIcon=\"list\"></mat-icon>\n <p>{{ 'core.component.preview-pdf.deferred.outline' | translate }}</p>\n </div>\n }\n @case ('search') {\n @if (!searchTerm()) {\n <div class=\"sd-preview-pdf-sidebar__deferred\">\n <mat-icon fontIcon=\"search\"></mat-icon>\n <p>{{ 'core.component.preview-pdf.search-placeholder' | translate }}</p>\n </div>\n } @else if (searchTotal() === 0) {\n <div class=\"sd-preview-pdf-sidebar__deferred\">\n <mat-icon fontIcon=\"search_off\"></mat-icon>\n <p>{{ 'core.component.preview-pdf.search-no-results' | translate }}</p>\n </div>\n } @else {\n <ul class=\"sd-preview-pdf-search-list\">\n @for (r of searchResults(); track $index; let i = $index) {\n <li>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-search-result\"\n [class.sd-preview-pdf-search-result--active]=\"i === searchActiveIndex()\"\n [attr.data-autoId]=\"autoIdResult(i)\"\n (click)=\"goToPage(r.page)\">\n <span class=\"sd-preview-pdf-search-result__snippet\">\n {{ r.before }}<mark>{{ r.term }}</mark>{{ r.after }}\n </span>\n <span class=\"sd-preview-pdf-search-result__page\">{{ r.page }}</span>\n </button>\n </li>\n }\n </ul>\n }\n }\n @default {\n <div class=\"sd-preview-pdf-sidebar__deferred\"></div>\n }\n }\n </div>\n </aside>\n }\n\n <!-- STAGE ............................................................... -->\n <div class=\"sd-preview-pdf-stage\" #stageRef (wheel)=\"onWheel($event)\">\n @switch (stage()) {\n @case ('empty') {\n <div class=\"sd-preview-pdf-status\">\n <div class=\"sd-preview-pdf-status__tile sd-preview-pdf-status__tile--muted\">\n <mat-icon fontIcon=\"picture_as_pdf\"></mat-icon>\n </div>\n <div class=\"sd-preview-pdf-status__title\">\n {{ 'core.component.preview-pdf.empty.title' | translate }}\n </div>\n <div class=\"sd-preview-pdf-status__hint\">\n {{ 'core.component.preview-pdf.empty.subtitle' | translate }}\n </div>\n </div>\n }\n @case ('loading') {\n <div class=\"sd-preview-pdf-status\">\n <div class=\"sd-preview-pdf-spinner\" role=\"progressbar\"></div>\n <div class=\"sd-preview-pdf-status__title\">\n {{ 'core.component.preview-pdf.loading' | translate }}\n @if (loadPercent() > 0) {\n <span class=\"sd-preview-pdf-progress-pct\">&nbsp;{{ loadPercent() }}%</span>\n }\n </div>\n <div class=\"sd-preview-pdf-progress\">\n <div class=\"sd-preview-pdf-progress__fill\" [style.width.%]=\"loadPercent()\"></div>\n </div>\n </div>\n }\n @case ('error') {\n @if (loadError$(); as err) {\n <div class=\"sd-preview-pdf-status\">\n <div class=\"sd-preview-pdf-status__tile sd-preview-pdf-status__tile--danger\">\n <mat-icon fontIcon=\"error_outline\"></mat-icon>\n </div>\n <div class=\"sd-preview-pdf-status__title\">\n {{ ('core.component.preview-pdf.error.' + err.reason + '.title') | translate }}\n </div>\n <div class=\"sd-preview-pdf-status__hint\">\n {{ ('core.component.preview-pdf.error.' + err.reason + '.body') | translate }}\n </div>\n <div class=\"sd-preview-pdf-status__actions\">\n <button type=\"button\" class=\"sd-preview-pdf-btn-primary\" (click)=\"retryLoad()\" [attr.data-autoId]=\"autoIdRetry()\">\n <mat-icon fontIcon=\"refresh\"></mat-icon>\n <span>{{ 'core.component.preview-pdf.error.retry' | translate }}</span>\n </button>\n @if (downloadable()) {\n <button type=\"button\" class=\"sd-preview-pdf-btn-secondary\" (click)=\"downloadFile()\">\n <mat-icon fontIcon=\"download\"></mat-icon>\n <span>{{ 'core.component.preview-pdf.error.download-raw' | translate }}</span>\n </button>\n }\n </div>\n </div>\n }\n }\n @default {\n <div class=\"sd-preview-pdf-page-wrap\">\n <canvas\n #pageCanvas\n class=\"sd-preview-pdf-page\"\n role=\"img\"\n [attr.aria-label]=\"('core.component.preview-pdf.page-of' | translate: { current: activePage(), total: numPages() })\">\n </canvas>\n </div>\n }\n }\n\n <!-- FLOATING TOOLBAR ........................................ -->\n @if (showToolbar() && stage() === 'ready') {\n <div class=\"sd-preview-pdf-toolbar\" role=\"toolbar\">\n <button\n type=\"button\"\n class=\"sd-preview-pdf-toolbar__btn\"\n [disabled]=\"!canPrev()\"\n [attr.aria-label]=\"'core.component.preview-pdf.first-page' | translate\"\n [attr.title]=\"'core.component.preview-pdf.first-page' | translate\"\n [attr.data-autoId]=\"autoIdFirst()\"\n (click)=\"firstPage()\">\n <mat-icon fontIcon=\"first_page\"></mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-toolbar__btn\"\n [disabled]=\"!canPrev()\"\n [attr.aria-label]=\"'core.component.preview-pdf.prev-page' | translate\"\n [attr.title]=\"'core.component.preview-pdf.prev-page' | translate\"\n [attr.data-autoId]=\"autoIdPrev()\"\n (click)=\"prevPage()\">\n <mat-icon fontIcon=\"chevron_left\"></mat-icon>\n </button>\n <span class=\"sd-preview-pdf-toolbar__page\">\n <input\n type=\"text\"\n inputmode=\"numeric\"\n class=\"sd-preview-pdf-toolbar__page-input\"\n [value]=\"activePage()\"\n #pageInput\n (keydown.enter)=\"onPageInputEnter($event)\"\n (blur)=\"onPageInputBlur($event)\"\n [attr.aria-label]=\"'core.component.preview-pdf.page-of' | translate: { current: activePage(), total: numPages() }\"\n [attr.data-autoId]=\"autoIdPageInput()\" />\n <span class=\"sd-preview-pdf-toolbar__page-total\">/ {{ numPages() }}</span>\n </span>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-toolbar__btn\"\n [disabled]=\"!canNext()\"\n [attr.aria-label]=\"'core.component.preview-pdf.next-page' | translate\"\n [attr.title]=\"'core.component.preview-pdf.next-page' | translate\"\n [attr.data-autoId]=\"autoIdNext()\"\n (click)=\"nextPage()\">\n <mat-icon fontIcon=\"chevron_right\"></mat-icon>\n </button>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-toolbar__btn\"\n [disabled]=\"!canNext()\"\n [attr.aria-label]=\"'core.component.preview-pdf.last-page' | translate\"\n [attr.title]=\"'core.component.preview-pdf.last-page' | translate\"\n [attr.data-autoId]=\"autoIdLast()\"\n (click)=\"lastPage()\">\n <mat-icon fontIcon=\"last_page\"></mat-icon>\n </button>\n\n <span class=\"sd-preview-pdf-toolbar__divider\"></span>\n\n <button\n type=\"button\"\n class=\"sd-preview-pdf-toolbar__btn\"\n [attr.aria-label]=\"'core.component.preview-pdf.zoom-out' | translate\"\n [attr.title]=\"'core.component.preview-pdf.zoom-out' | translate\"\n [attr.data-autoId]=\"autoIdZoomOut()\"\n (click)=\"zoomOut()\">\n <mat-icon fontIcon=\"remove\"></mat-icon>\n </button>\n <span class=\"sd-preview-pdf-toolbar__zoom\" aria-live=\"polite\" [attr.data-autoId]=\"autoIdZoomReadout()\">{{ zoomPercent() }}%</span>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-toolbar__btn\"\n [attr.aria-label]=\"'core.component.preview-pdf.zoom-in' | translate\"\n [attr.title]=\"'core.component.preview-pdf.zoom-in' | translate\"\n [attr.data-autoId]=\"autoIdZoomIn()\"\n (click)=\"zoomIn()\">\n <mat-icon fontIcon=\"add\"></mat-icon>\n </button>\n\n <span class=\"sd-preview-pdf-toolbar__divider\"></span>\n\n <button\n type=\"button\"\n class=\"sd-preview-pdf-toolbar__btn sd-preview-pdf-toolbar__btn--with-label\"\n [class.sd-preview-pdf-toolbar__btn--active]=\"zoomMode() === 'page-fit'\"\n [attr.data-autoId]=\"autoIdFitPage()\"\n (click)=\"setZoom('page-fit')\">\n <mat-icon fontIcon=\"fit_screen\"></mat-icon>\n <span>{{ 'core.component.preview-pdf.fit-page' | translate }}</span>\n </button>\n <button\n type=\"button\"\n class=\"sd-preview-pdf-toolbar__btn sd-preview-pdf-toolbar__btn--with-label\"\n [class.sd-preview-pdf-toolbar__btn--active]=\"zoomMode() === 'page-width'\"\n [attr.data-autoId]=\"autoIdFitWidth()\"\n (click)=\"setZoom('page-width')\">\n <mat-icon fontIcon=\"swap_horiz\"></mat-icon>\n <span>{{ 'core.component.preview-pdf.fit-width' | translate }}</span>\n </button>\n\n <span class=\"sd-preview-pdf-toolbar__divider\"></span>\n\n <button\n type=\"button\"\n class=\"sd-preview-pdf-toolbar__btn\"\n [attr.aria-label]=\"'core.component.preview-pdf.rotate' | translate\"\n [attr.title]=\"'core.component.preview-pdf.rotate' | translate\"\n [attr.data-autoId]=\"autoIdRotate()\"\n (click)=\"rotate('right')\">\n <mat-icon fontIcon=\"rotate_right\"></mat-icon>\n </button>\n\n <!-- TODO(preview-pdf): scroll mode toggle is signature-only;\n clicking switches the state but rendering stays single-page\n until the continuous scroll feature lands. -->\n <button\n type=\"button\"\n class=\"sd-preview-pdf-toolbar__btn sd-preview-pdf-toolbar__btn--with-label\"\n [attr.data-autoId]=\"autoIdScrollMode()\"\n (click)=\"setScrollMode(scrollModeCurrent() === 'page' ? 'continuous' : 'page')\">\n <mat-icon [fontIcon]=\"scrollModeCurrent() === 'page' ? 'description' : 'view_day'\"></mat-icon>\n <span>\n {{ (scrollModeCurrent() === 'page'\n ? 'core.component.preview-pdf.scroll-page'\n : 'core.component.preview-pdf.scroll-continuous') | translate }}\n </span>\n </button>\n </div>\n }\n </div>\n </div>\n</div>\n", styles: [":host{--sd-pdf-shell-bg: #1a1c1f;--sd-pdf-sidebar-bg: #22252a;--sd-pdf-toolbar-bg: rgba(20, 22, 25, .94);--sd-pdf-page-bg: #ffffff;--sd-pdf-page-shadow: 0 4px 16px rgba(0, 0, 0, .4);--sd-pdf-divider: rgba(255, 255, 255, .08);--sd-pdf-fg: #f5f6f7;--sd-pdf-fg-muted: rgba(255, 255, 255, .6);--sd-pdf-fg-subtle: rgba(255, 255, 255, .4);--sd-pdf-accent: var(--sd-primary, #3b82f6);--sd-pdf-accent-soft: rgba(59, 130, 246, .2);--sd-pdf-danger: var(--sd-error, #ef4444);--sd-pdf-success: var(--sd-success, #22c55e);--sd-pdf-danger-soft: rgba(239, 68, 68, .1);--sd-pdf-danger-border: rgba(239, 68, 68, .3);--sd-pdf-danger-tint: rgba(239, 68, 68, .15);--sd-pdf-hover-bg: rgba(255, 255, 255, .1);--sd-pdf-search-hi: rgba(250, 204, 21, .55);--sd-pdf-search-hi-active: rgba(249, 115, 22, .85);display:block;width:100%;height:100%;min-height:320px;outline:none}:host([data-theme=light]){--sd-pdf-shell-bg: #f7f8fa;--sd-pdf-sidebar-bg: #ffffff;--sd-pdf-toolbar-bg: rgba(255, 255, 255, .94);--sd-pdf-page-bg: #ffffff;--sd-pdf-page-shadow: 0 4px 16px rgba(0, 0, 0, .12);--sd-pdf-divider: rgba(0, 0, 0, .08);--sd-pdf-fg: #1a1c1f;--sd-pdf-fg-muted: rgba(0, 0, 0, .62);--sd-pdf-fg-subtle: rgba(0, 0, 0, .42);--sd-pdf-accent: var(--sd-primary, #1f49bf);--sd-pdf-accent-soft: rgba(31, 73, 191, .18);--sd-pdf-danger: var(--sd-error, #ef4444);--sd-pdf-success: var(--sd-success, #22c55e);--sd-pdf-danger-soft: rgba(239, 68, 68, .08);--sd-pdf-danger-border: rgba(239, 68, 68, .25);--sd-pdf-danger-tint: rgba(239, 68, 68, .12);--sd-pdf-hover-bg: rgba(0, 0, 0, .06);--sd-pdf-search-hi: rgba(250, 204, 21, .55);--sd-pdf-search-hi-active: rgba(250, 204, 21, .9)}:host(.sd-preview-pdf-host--fullscreen){width:100vw;height:100vh;min-height:0}.sd-preview-pdf-shell{position:relative;display:flex;flex-direction:column;width:100%;height:100%;background:var(--sd-pdf-shell-bg);color:var(--sd-pdf-fg);border-radius:12px;overflow:hidden;box-shadow:0 30px 80px #0000008c,0 0 0 1px #ffffff0a;font-family:inherit}.sd-preview-pdf-header{display:flex;align-items:center;justify-content:space-between;gap:12px;height:57px;padding:0 12px;background:var(--sd-pdf-toolbar-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--sd-pdf-divider);flex:0 0 auto;z-index:2}.sd-preview-pdf-header__left{display:flex;align-items:center;gap:8px;min-width:0;flex:1 1 auto}.sd-preview-pdf-header__right{display:flex;align-items:center;gap:4px;flex-shrink:0}.sd-preview-pdf-header__divider{display:inline-block;width:1px;height:20px;background:var(--sd-pdf-divider)}.sd-preview-pdf-header__meta{display:flex;flex-direction:column;min-width:0}.sd-preview-pdf-header__name{font-size:14px;font-weight:500;color:var(--sd-pdf-fg);max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-preview-pdf-header__sub{font-size:11px;color:var(--sd-pdf-fg-muted);margin-top:2px}.sd-preview-pdf-fileicon{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:6px;background:var(--sd-pdf-danger-tint);border:1px solid var(--sd-pdf-danger-border);color:var(--sd-error-light, #f87171);font:700 10px/1 ui-monospace,SFMono-Regular,Menlo,monospace}.sd-preview-pdf-iconbtn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-width:32px;height:32px;padding:0 8px;background:transparent;border:none;color:var(--sd-pdf-fg);border-radius:6px;cursor:pointer;transition:background .12s,color .12s;white-space:nowrap;flex-shrink:0}.sd-preview-pdf-iconbtn mat-icon{font-size:18px;width:18px;height:18px}.sd-preview-pdf-iconbtn__label{font-size:13px;font-weight:500}.sd-preview-pdf-iconbtn:hover:not(:disabled){background:var(--sd-pdf-hover-bg)}.sd-preview-pdf-iconbtn:focus-visible{outline:2px solid var(--sd-pdf-accent);outline-offset:2px}.sd-preview-pdf-iconbtn:disabled{opacity:.35;cursor:not-allowed}.sd-preview-pdf-iconbtn--active{background:var(--sd-pdf-accent-soft);color:var(--sd-pdf-accent)}.sd-preview-pdf-searchbar{display:flex;align-items:center;gap:6px;height:46px;padding:0 12px;background:var(--sd-pdf-toolbar-bg);border-bottom:1px solid var(--sd-pdf-divider);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);flex:0 0 auto;z-index:2}.sd-preview-pdf-searchbar__icon{color:var(--sd-pdf-fg-muted);font-size:18px;width:18px;height:18px;flex-shrink:0}.sd-preview-pdf-searchbar__input{flex:0 1 360px;min-width:160px;height:30px;background:#ffffff0f;border:1px solid rgba(255,255,255,.08);color:var(--sd-pdf-fg);border-radius:6px;padding:0 10px;font-size:13px;outline:none}.sd-preview-pdf-searchbar__input::placeholder{color:var(--sd-pdf-fg-subtle)}.sd-preview-pdf-searchbar__input:focus{border-color:var(--sd-pdf-accent);background:#ffffff17}.sd-preview-pdf-searchbar__counter{color:var(--sd-pdf-fg-muted);font-size:12px;font-variant-numeric:tabular-nums;min-width:48px;text-align:center;flex-shrink:0}.sd-preview-pdf-searchbar__divider{width:1px;height:20px;background:var(--sd-pdf-divider);flex-shrink:0}.sd-preview-pdf-searchbar__spacer{flex:1 1 auto}.sd-pdf-search-overlay{position:absolute;top:8px;left:50%;transform:translate(-50%);display:flex;flex-wrap:wrap;gap:4px;max-width:80%;pointer-events:none;z-index:5}.sd-pdf-search-hi{display:inline-block;padding:1px 4px;border-radius:2px;background:var(--sd-pdf-search-hi);color:#000;font-size:12px;font-weight:500}.sd-pdf-search-hi--active{background:var(--sd-pdf-search-hi-active);color:#fff;box-shadow:0 0 0 2px #fff6}.sd-preview-pdf-body{position:relative;flex:1 1 auto;min-height:0;display:flex;flex-direction:row;overflow:hidden}.sd-preview-pdf-sidebar{width:240px;display:flex;flex-direction:column;background:var(--sd-pdf-sidebar-bg);border-right:1px solid var(--sd-pdf-divider);flex-shrink:0;min-height:0}.sd-preview-pdf-sidebar__tabs{display:flex;gap:4px;padding:8px;border-bottom:1px solid var(--sd-pdf-divider)}.sd-preview-pdf-sidebar__tab{flex:1 1 0;display:inline-flex;align-items:center;justify-content:center;gap:4px;height:32px;padding:0 4px;background:transparent;border:none;color:var(--sd-pdf-fg-muted);border-radius:6px;cursor:pointer;font-size:12px;font-weight:500;white-space:nowrap;flex-shrink:0;transition:background .12s,color .12s}.sd-preview-pdf-sidebar__tab mat-icon{font-size:14px;width:14px;height:14px}.sd-preview-pdf-sidebar__tab:hover{background:#ffffff0f;color:var(--sd-pdf-fg)}.sd-preview-pdf-sidebar__tab--active{background:var(--sd-pdf-accent-soft);color:var(--sd-pdf-accent)}.sd-preview-pdf-sidebar__content{flex:1 1 auto;overflow:auto;padding:12px}.sd-preview-pdf-sidebar__deferred{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:32px 16px;text-align:center;color:var(--sd-pdf-fg-subtle)}.sd-preview-pdf-sidebar__deferred mat-icon{font-size:28px;width:28px;height:28px;opacity:.6}.sd-preview-pdf-sidebar__deferred p{margin:0;font-size:12px;line-height:1.5}.sd-preview-pdf-thumb-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:12px}.sd-preview-pdf-thumb{display:flex;flex-direction:column;align-items:center;gap:4px;width:100%;padding:0;background:transparent;border:none;cursor:pointer}.sd-preview-pdf-thumb__page{width:152px;aspect-ratio:612/792;background:var(--sd-pdf-page-bg);border-radius:2px;outline:1px solid rgba(255,255,255,.08);outline-offset:-1px;box-shadow:0 2px 6px #0000004d;display:flex;align-items:center;justify-content:center;overflow:hidden;-webkit-user-select:none;user-select:none;background-image:linear-gradient(90deg,#0000,#0000000a,#0000);background-size:200% 100%;animation:sd-preview-pdf-thumb-skel 1.6s ease-in-out infinite}.sd-preview-pdf-thumb__canvas{display:block;max-width:100%;max-height:100%;background:var(--sd-pdf-page-bg)}.sd-preview-pdf-thumb__label{font-size:11px;color:var(--sd-pdf-fg-muted);font-variant-numeric:tabular-nums}.sd-preview-pdf-thumb--active .sd-preview-pdf-thumb__page{outline:2px solid var(--sd-pdf-accent);outline-offset:-2px}.sd-preview-pdf-thumb--active .sd-preview-pdf-thumb__label{color:var(--sd-pdf-accent);font-weight:600}.sd-preview-pdf-thumb:focus-visible .sd-preview-pdf-thumb__page{outline:2px solid var(--sd-pdf-accent);outline-offset:2px}.sd-preview-pdf-stage{position:relative;flex:1 1 auto;min-width:0;min-height:0;background:var(--sd-pdf-shell-bg);background-image:linear-gradient(45deg,rgba(255,255,255,.015) 25%,transparent 25%),linear-gradient(-45deg,rgba(255,255,255,.015) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(255,255,255,.015) 75%),linear-gradient(-45deg,transparent 75%,rgba(255,255,255,.015) 75%);background-size:24px 24px;background-position:0 0,0 12px,12px -12px,-12px 0;overflow:auto;padding:32px 24px 96px;display:flex;justify-content:center;align-items:flex-start}:host([data-theme=light]) .sd-preview-pdf-stage{background-image:linear-gradient(45deg,rgba(0,0,0,.015) 25%,transparent 25%),linear-gradient(-45deg,rgba(0,0,0,.015) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(0,0,0,.015) 75%),linear-gradient(-45deg,transparent 75%,rgba(0,0,0,.015) 75%)}.sd-preview-pdf-page-wrap{display:inline-block;position:relative}.sd-preview-pdf-page{display:block;background:var(--sd-pdf-page-bg);border-radius:2px;box-shadow:var(--sd-pdf-page-shadow);max-width:100%}.sd-preview-pdf-toolbar{position:absolute;bottom:20px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:2px;padding:5px 6px;background:var(--sd-pdf-toolbar-bg);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border:1px solid var(--sd-pdf-divider);border-radius:10px;box-shadow:0 8px 30px #0006;z-index:4;max-width:calc(100% - 32px);overflow-x:auto}.sd-preview-pdf-toolbar__btn{display:inline-flex;align-items:center;justify-content:center;gap:4px;min-width:30px;height:28px;padding:0 6px;background:transparent;border:none;color:var(--sd-pdf-fg);border-radius:6px;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:background .12s,color .12s}.sd-preview-pdf-toolbar__btn mat-icon{font-size:18px;width:18px;height:18px}.sd-preview-pdf-toolbar__btn span{font-size:13px;font-weight:500}.sd-preview-pdf-toolbar__btn--with-label{padding:0 10px}.sd-preview-pdf-toolbar__btn--active{background:var(--sd-pdf-accent-soft);color:var(--sd-pdf-accent)}.sd-preview-pdf-toolbar__btn:hover:not(:disabled):not(.sd-preview-pdf-toolbar__btn--active){background:var(--sd-pdf-hover-bg)}.sd-preview-pdf-toolbar__btn:focus-visible{outline:2px solid var(--sd-pdf-accent);outline-offset:2px}.sd-preview-pdf-toolbar__btn:disabled{opacity:.35;cursor:not-allowed}.sd-preview-pdf-toolbar__page{display:inline-flex;align-items:center;gap:4px;padding:0 6px;flex-shrink:0}.sd-preview-pdf-toolbar__page-input{width:36px;height:24px;background:#ffffff14;border:1px solid rgba(255,255,255,.08);color:var(--sd-pdf-fg);border-radius:4px;text-align:center;font-size:13px;font-variant-numeric:tabular-nums;outline:none}.sd-preview-pdf-toolbar__page-input:focus{border-color:var(--sd-pdf-accent)}.sd-preview-pdf-toolbar__page-total{font-size:13px;color:var(--sd-pdf-fg-muted);font-variant-numeric:tabular-nums}.sd-preview-pdf-toolbar__zoom{min-width:56px;text-align:center;color:var(--sd-pdf-fg);font-size:13px;font-weight:500;font-variant-numeric:tabular-nums;background:#ffffff14;border-radius:4px;padding:2px 8px}.sd-preview-pdf-toolbar__divider{width:1px;height:20px;background:var(--sd-pdf-divider);margin:0 4px;flex-shrink:0}.sd-preview-pdf-status{margin:auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;text-align:center;padding:32px;max-width:420px}.sd-preview-pdf-status__tile{width:96px;height:96px;border-radius:24px;display:flex;align-items:center;justify-content:center;border:1px solid var(--sd-pdf-divider)}.sd-preview-pdf-status__tile mat-icon{font-size:48px;width:48px;height:48px}.sd-preview-pdf-status__tile--muted{background:#ffffff0d}.sd-preview-pdf-status__tile--muted mat-icon{color:var(--sd-pdf-fg-subtle)}.sd-preview-pdf-status__tile--danger{background:var(--sd-pdf-danger-soft);border-color:var(--sd-pdf-danger-border)}.sd-preview-pdf-status__tile--danger mat-icon{color:var(--sd-pdf-danger)}.sd-preview-pdf-status__title{font-size:16px;font-weight:500;color:var(--sd-pdf-fg)}.sd-preview-pdf-status__hint{font-size:13px;color:var(--sd-pdf-fg-muted);line-height:1.5}.sd-preview-pdf-status__actions{display:flex;gap:8px;margin-top:4px}.sd-preview-pdf-progress-pct{font-variant-numeric:tabular-nums;color:var(--sd-pdf-fg-muted)}.sd-preview-pdf-progress{width:240px;height:4px;background:#ffffff14;border-radius:999px;overflow:hidden}.sd-preview-pdf-progress__fill{height:100%;background:var(--sd-pdf-accent);transition:width .12s linear}.sd-preview-pdf-spinner{width:56px;height:56px;border-radius:999px;border:3px solid rgba(255,255,255,.08);border-top-color:var(--sd-pdf-accent);animation:sd-preview-pdf-spin .9s linear infinite}@keyframes sd-preview-pdf-spin{to{transform:rotate(360deg)}}@keyframes sd-preview-pdf-thumb-skel{0%{background-position:200% 0}to{background-position:-200% 0}}.sd-preview-pdf-search-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}.sd-preview-pdf-search-result{width:100%;display:flex;align-items:flex-start;gap:8px;padding:8px 10px;background:#ffffff0a;border:1px solid transparent;border-radius:6px;color:var(--sd-pdf-fg);cursor:pointer;text-align:left;transition:background .12s,border-color .12s}.sd-preview-pdf-search-result:hover{background:#ffffff12}.sd-preview-pdf-search-result--active{background:var(--sd-pdf-accent-soft);border-color:#3b82f673}.sd-preview-pdf-search-result__snippet{flex:1 1 auto;font-size:12px;line-height:1.4;color:var(--sd-pdf-fg);word-break:break-word}.sd-preview-pdf-search-result__snippet mark{background:var(--sd-pdf-search-hi);color:#000;padding:0 2px;border-radius:2px}.sd-preview-pdf-search-result__page{flex:0 0 auto;font-size:11px;font-variant-numeric:tabular-nums;color:var(--sd-pdf-fg-muted);background:#ffffff0f;padding:2px 6px;border-radius:4px}.sd-preview-pdf-btn-primary{display:inline-flex;align-items:center;gap:6px;height:36px;padding:0 16px;background:var(--sd-pdf-accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:13px;font-weight:500;transition:filter .12s}.sd-preview-pdf-btn-primary mat-icon{font-size:18px;width:18px;height:18px}.sd-preview-pdf-btn-primary:hover{filter:brightness(1.1)}.sd-preview-pdf-btn-primary:focus-visible{outline:2px solid #fff;outline-offset:2px}.sd-preview-pdf-btn-secondary{display:inline-flex;align-items:center;gap:6px;height:36px;padding:0 16px;background:transparent;color:var(--sd-pdf-fg);border:1px solid var(--sd-pdf-divider);border-radius:8px;cursor:pointer;font-size:13px;font-weight:500;transition:background .12s}.sd-preview-pdf-btn-secondary mat-icon{font-size:18px;width:18px;height:18px}.sd-preview-pdf-btn-secondary:hover{background:#ffffff0f}.sd-preview-pdf-btn-secondary:focus-visible{outline:2px solid var(--sd-pdf-accent);outline-offset:2px}\n"] }]
1977
+ }], ctorParameters: () => [], propDecorators: { onKeyDown: [{
1978
+ type: HostListener,
1979
+ args: ['keydown', ['$event']]
1980
+ }] } });
1981
+
1982
+ /**
1983
+ * Generated bundle index. Do not edit.
1984
+ */
1985
+
1986
+ export { SD_PDFJS_LIB, SdPreviewImage, SdPreviewPdf };
1987
+ //# sourceMappingURL=sdcorejs-angular-components-preview.mjs.map