@caribeng/ui 22.4.0 → 22.6.0

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 (186) hide show
  1. package/README.md +313 -313
  2. package/fesm2022/caribeng-ui-account-picker.mjs +533 -0
  3. package/fesm2022/caribeng-ui-aging.mjs +488 -0
  4. package/fesm2022/{caribeng-ui-caribe-alert.mjs → caribeng-ui-alert.mjs} +45 -45
  5. package/fesm2022/caribeng-ui-amount.mjs +249 -0
  6. package/fesm2022/caribeng-ui-anchor.mjs +2 -2
  7. package/fesm2022/{caribeng-ui-caribe-animation-lottie.mjs → caribeng-ui-animation-lottie.mjs} +41 -41
  8. package/fesm2022/caribeng-ui-audit-log.mjs +323 -0
  9. package/fesm2022/caribeng-ui-avatar.mjs +2 -2
  10. package/fesm2022/caribeng-ui-breadcrumb.mjs +13 -3
  11. package/fesm2022/{caribeng-ui-caribe-button-group.mjs → caribeng-ui-button-group.mjs} +15 -15
  12. package/fesm2022/caribeng-ui-button.mjs +2 -2
  13. package/fesm2022/{caribeng-ui-caribe-card-content.mjs → caribeng-ui-card-content.mjs} +39 -39
  14. package/fesm2022/{caribeng-ui-caribe-card-move.mjs → caribeng-ui-card-move.mjs} +65 -65
  15. package/fesm2022/{caribeng-ui-caribe-card-primary.mjs → caribeng-ui-card-primary.mjs} +31 -31
  16. package/fesm2022/{caribeng-ui-caribe-card-select.mjs → caribeng-ui-card-select.mjs} +67 -67
  17. package/fesm2022/caribeng-ui-carousel.mjs +2 -2
  18. package/fesm2022/caribeng-ui-cascadeselect.mjs +45 -9
  19. package/fesm2022/caribeng-ui-chat.mjs +2 -2
  20. package/fesm2022/{caribeng-ui-caribe-coachmark.mjs → caribeng-ui-coachmark.mjs} +85 -85
  21. package/fesm2022/caribeng-ui-command-palette.mjs +2 -2
  22. package/fesm2022/caribeng-ui-comments.mjs +864 -0
  23. package/fesm2022/caribeng-ui-datagrid.mjs +1635 -0
  24. package/fesm2022/caribeng-ui-dataview.mjs +2 -2
  25. package/fesm2022/caribeng-ui-datepicker.mjs +2 -2
  26. package/fesm2022/caribeng-ui-document.mjs +495 -0
  27. package/fesm2022/{caribeng-ui-caribe-dots-indicator.mjs → caribeng-ui-dots-indicator.mjs} +41 -41
  28. package/fesm2022/caribeng-ui-dynamic-form.mjs +1581 -0
  29. package/fesm2022/caribeng-ui-editor.mjs +2303 -1311
  30. package/fesm2022/{caribeng-ui-caribe-expansion-panel.mjs → caribeng-ui-expansion-panel.mjs} +39 -39
  31. package/fesm2022/{caribeng-ui-caribe-fab-menu.mjs → caribeng-ui-fab-menu.mjs} +75 -75
  32. package/fesm2022/caribeng-ui-file-card.mjs +2 -2
  33. package/fesm2022/{caribeng-ui-caribe-file-download.mjs → caribeng-ui-file-download.mjs} +63 -63
  34. package/fesm2022/caribeng-ui-financial-report.mjs +380 -0
  35. package/fesm2022/{caribeng-ui-caribe-float-menu.mjs → caribeng-ui-float-menu.mjs} +31 -31
  36. package/fesm2022/caribeng-ui-floatlabel.mjs +2 -2
  37. package/fesm2022/caribeng-ui-flow.mjs +1508 -0
  38. package/fesm2022/{caribeng-ui-caribe-footer.mjs → caribeng-ui-footer.mjs} +19 -19
  39. package/fesm2022/caribeng-ui-gantt.mjs +1094 -0
  40. package/fesm2022/{caribeng-ui-caribe-header.mjs → caribeng-ui-header.mjs} +49 -49
  41. package/fesm2022/{caribeng-ui-caribe-icon-button.mjs → caribeng-ui-icon-button.mjs} +23 -23
  42. package/fesm2022/caribeng-ui-image-cropper.mjs +671 -0
  43. package/fesm2022/caribeng-ui-import-wizard.mjs +746 -0
  44. package/fesm2022/caribeng-ui-infinite-scroll.mjs +226 -0
  45. package/fesm2022/caribeng-ui-input-number.mjs +22 -6
  46. package/fesm2022/{caribeng-ui-caribe-input-search.mjs → caribeng-ui-input-search.mjs} +65 -65
  47. package/fesm2022/{caribeng-ui-caribe-input-token.mjs → caribeng-ui-input-token.mjs} +59 -59
  48. package/fesm2022/caribeng-ui-inputotp.mjs +2 -2
  49. package/fesm2022/caribeng-ui-journal-entry.mjs +340 -0
  50. package/fesm2022/caribeng-ui-kanban.mjs +6 -6
  51. package/fesm2022/caribeng-ui-kpi.mjs +189 -0
  52. package/fesm2022/{caribeng-ui-caribe-link.mjs → caribeng-ui-link.mjs} +37 -37
  53. package/fesm2022/caribeng-ui-masonry.mjs +93 -0
  54. package/fesm2022/caribeng-ui-media-player.mjs +821 -0
  55. package/fesm2022/caribeng-ui-mentions.mjs +2 -2
  56. package/fesm2022/caribeng-ui-notification-center.mjs +636 -0
  57. package/fesm2022/{caribeng-ui-caribe-off-canvas.mjs → caribeng-ui-off-canvas.mjs} +55 -55
  58. package/fesm2022/{caribeng-ui-caribe-onboarding.mjs → caribeng-ui-onboarding.mjs} +105 -105
  59. package/fesm2022/caribeng-ui-orderlist.mjs +2 -2
  60. package/fesm2022/caribeng-ui-pdfviewer.mjs +1755 -25
  61. package/fesm2022/caribeng-ui-period-picker.mjs +637 -0
  62. package/fesm2022/caribeng-ui-phone-input.mjs +2 -2
  63. package/fesm2022/caribeng-ui-picklist.mjs +2 -2
  64. package/fesm2022/caribeng-ui-pivot.mjs +419 -0
  65. package/fesm2022/caribeng-ui-pricing.mjs +2 -2
  66. package/fesm2022/caribeng-ui-rating.mjs +14 -17
  67. package/fesm2022/caribeng-ui-reconciliation.mjs +386 -0
  68. package/fesm2022/caribeng-ui-scheduler.mjs +1027 -0
  69. package/fesm2022/caribeng-ui-select.mjs +2 -2
  70. package/fesm2022/{caribeng-ui-caribe-shortcut.mjs → caribeng-ui-shortcut.mjs} +29 -29
  71. package/fesm2022/caribeng-ui-sidebar.mjs +2 -2
  72. package/fesm2022/caribeng-ui-signature-pad.mjs +654 -0
  73. package/fesm2022/{caribeng-ui-caribe-status.mjs → caribeng-ui-status.mjs} +11 -11
  74. package/fesm2022/{caribeng-ui-caribe-step-indicator.mjs → caribeng-ui-step-indicator.mjs} +53 -53
  75. package/fesm2022/caribeng-ui-stepper.mjs +2 -2
  76. package/fesm2022/caribeng-ui-steps.mjs +2 -2
  77. package/fesm2022/caribeng-ui-table.mjs +323 -15
  78. package/fesm2022/caribeng-ui-tabmenu.mjs +2 -2
  79. package/fesm2022/caribeng-ui-tabs.mjs +2 -2
  80. package/fesm2022/{caribeng-ui-caribe-tag-button.mjs → caribeng-ui-tag-button.mjs} +25 -25
  81. package/fesm2022/caribeng-ui-tag.mjs +2 -2
  82. package/fesm2022/caribeng-ui-tieredmenu.mjs +6 -6
  83. package/fesm2022/{caribeng-ui-caribe-tracker.mjs → caribeng-ui-tracker.mjs} +29 -29
  84. package/fesm2022/{caribeng-ui-caribe-transaction-status.mjs → caribeng-ui-transaction-status.mjs} +11 -11
  85. package/fesm2022/caribeng-ui-treeselect.mjs +44 -9
  86. package/fesm2022/caribeng-ui-treetable.mjs +236 -17
  87. package/fesm2022/{caribeng-ui-caribe-voucher.mjs → caribeng-ui-voucher.mjs} +45 -45
  88. package/fesm2022/caribeng-ui-xlsx.mjs +486 -0
  89. package/fesm2022/caribeng-ui.mjs +1185 -5
  90. package/package.json +217 -177
  91. package/types/caribeng-ui-account-picker.d.ts +142 -0
  92. package/types/caribeng-ui-aging.d.ts +123 -0
  93. package/types/{caribeng-ui-caribe-alert.d.ts → caribeng-ui-alert.d.ts} +2 -2
  94. package/types/caribeng-ui-amount.d.ts +82 -0
  95. package/types/{caribeng-ui-caribe-animation-lottie.d.ts → caribeng-ui-animation-lottie.d.ts} +2 -2
  96. package/types/caribeng-ui-audit-log.d.ts +86 -0
  97. package/types/caribeng-ui-breadcrumb.d.ts +2 -2
  98. package/types/{caribeng-ui-caribe-button-group.d.ts → caribeng-ui-button-group.d.ts} +2 -2
  99. package/types/{caribeng-ui-caribe-card-content.d.ts → caribeng-ui-card-content.d.ts} +2 -2
  100. package/types/{caribeng-ui-caribe-card-move.d.ts → caribeng-ui-card-move.d.ts} +1 -1
  101. package/types/{caribeng-ui-caribe-card-primary.d.ts → caribeng-ui-card-primary.d.ts} +2 -2
  102. package/types/{caribeng-ui-caribe-card-select.d.ts → caribeng-ui-card-select.d.ts} +2 -2
  103. package/types/caribeng-ui-cascadeselect.d.ts +6 -0
  104. package/types/{caribeng-ui-caribe-coachmark.d.ts → caribeng-ui-coachmark.d.ts} +2 -2
  105. package/types/caribeng-ui-comments.d.ts +214 -0
  106. package/types/caribeng-ui-datagrid.d.ts +269 -0
  107. package/types/caribeng-ui-document.d.ts +138 -0
  108. package/types/{caribeng-ui-caribe-dots-indicator.d.ts → caribeng-ui-dots-indicator.d.ts} +2 -2
  109. package/types/caribeng-ui-dynamic-form.d.ts +409 -0
  110. package/types/caribeng-ui-editor.d.ts +110 -12
  111. package/types/{caribeng-ui-caribe-expansion-panel.d.ts → caribeng-ui-expansion-panel.d.ts} +1 -1
  112. package/types/{caribeng-ui-caribe-fab-menu.d.ts → caribeng-ui-fab-menu.d.ts} +2 -2
  113. package/types/{caribeng-ui-caribe-file-download.d.ts → caribeng-ui-file-download.d.ts} +1 -1
  114. package/types/caribeng-ui-financial-report.d.ts +102 -0
  115. package/types/{caribeng-ui-caribe-float-menu.d.ts → caribeng-ui-float-menu.d.ts} +2 -2
  116. package/types/caribeng-ui-flow.d.ts +228 -0
  117. package/types/{caribeng-ui-caribe-footer.d.ts → caribeng-ui-footer.d.ts} +2 -2
  118. package/types/caribeng-ui-gantt.d.ts +244 -0
  119. package/types/{caribeng-ui-caribe-header.d.ts → caribeng-ui-header.d.ts} +2 -2
  120. package/types/{caribeng-ui-caribe-icon-button.d.ts → caribeng-ui-icon-button.d.ts} +2 -2
  121. package/types/{caribeng-ui-caribe-illustration.d.ts → caribeng-ui-illustration.d.ts} +1 -1
  122. package/types/caribeng-ui-image-cropper.d.ts +169 -0
  123. package/types/caribeng-ui-import-wizard.d.ts +149 -0
  124. package/types/caribeng-ui-infinite-scroll.d.ts +75 -0
  125. package/types/caribeng-ui-input-number.d.ts +1 -1
  126. package/types/{caribeng-ui-caribe-input-search.d.ts → caribeng-ui-input-search.d.ts} +2 -2
  127. package/types/{caribeng-ui-caribe-input-token.d.ts → caribeng-ui-input-token.d.ts} +2 -2
  128. package/types/caribeng-ui-journal-entry.d.ts +110 -0
  129. package/types/caribeng-ui-kpi.d.ts +64 -0
  130. package/types/{caribeng-ui-caribe-link.d.ts → caribeng-ui-link.d.ts} +2 -2
  131. package/types/{caribeng-ui-caribe-logo.d.ts → caribeng-ui-logo.d.ts} +1 -1
  132. package/types/caribeng-ui-masonry.d.ts +32 -0
  133. package/types/caribeng-ui-media-player.d.ts +140 -0
  134. package/types/caribeng-ui-notification-center.d.ts +153 -0
  135. package/types/{caribeng-ui-caribe-off-canvas.d.ts → caribeng-ui-off-canvas.d.ts} +2 -2
  136. package/types/{caribeng-ui-caribe-onboarding.d.ts → caribeng-ui-onboarding.d.ts} +2 -2
  137. package/types/caribeng-ui-pdfviewer.d.ts +245 -5
  138. package/types/caribeng-ui-period-picker.d.ts +148 -0
  139. package/types/caribeng-ui-pivot.d.ts +116 -0
  140. package/types/caribeng-ui-reconciliation.d.ts +109 -0
  141. package/types/caribeng-ui-scheduler.d.ts +188 -0
  142. package/types/{caribeng-ui-caribe-shortcut.d.ts → caribeng-ui-shortcut.d.ts} +2 -2
  143. package/types/caribeng-ui-signature-pad.d.ts +151 -0
  144. package/types/{caribeng-ui-caribe-status.d.ts → caribeng-ui-status.d.ts} +2 -2
  145. package/types/{caribeng-ui-caribe-step-indicator.d.ts → caribeng-ui-step-indicator.d.ts} +2 -2
  146. package/types/caribeng-ui-table.d.ts +66 -2
  147. package/types/{caribeng-ui-caribe-tag-button.d.ts → caribeng-ui-tag-button.d.ts} +2 -2
  148. package/types/{caribeng-ui-caribe-tracker.d.ts → caribeng-ui-tracker.d.ts} +2 -2
  149. package/types/{caribeng-ui-caribe-transaction-status.d.ts → caribeng-ui-transaction-status.d.ts} +2 -2
  150. package/types/caribeng-ui-treeselect.d.ts +6 -0
  151. package/types/caribeng-ui-treetable.d.ts +67 -2
  152. package/types/{caribeng-ui-caribe-voucher.d.ts → caribeng-ui-voucher.d.ts} +2 -2
  153. package/types/caribeng-ui-xlsx.d.ts +79 -0
  154. package/types/caribeng-ui.d.ts +713 -4
  155. package/fesm2022/caribeng-ui-caribe-calendar.mjs +0 -92
  156. package/fesm2022/caribeng-ui-caribe-circle-loading.mjs +0 -130
  157. package/fesm2022/caribeng-ui-caribe-data-table.mjs +0 -94
  158. package/fesm2022/caribeng-ui-caribe-file-select.mjs +0 -88
  159. package/fesm2022/caribeng-ui-caribe-inline-alert.mjs +0 -100
  160. package/fesm2022/caribeng-ui-caribe-input-date.mjs +0 -138
  161. package/fesm2022/caribeng-ui-caribe-input-select.mjs +0 -154
  162. package/fesm2022/caribeng-ui-caribe-loader.mjs +0 -48
  163. package/fesm2022/caribeng-ui-caribe-modal.mjs +0 -119
  164. package/fesm2022/caribeng-ui-caribe-pictogram.mjs +0 -52
  165. package/fesm2022/caribeng-ui-caribe-pop-over.mjs +0 -104
  166. package/fesm2022/caribeng-ui-caribe-preloader.mjs +0 -57
  167. package/fesm2022/caribeng-ui-caribe-progress-bar.mjs +0 -84
  168. package/fesm2022/caribeng-ui-caribe-stroke.mjs +0 -69
  169. package/fesm2022/caribeng-ui-caribe-switch.mjs +0 -182
  170. package/types/caribeng-ui-caribe-calendar.d.ts +0 -27
  171. package/types/caribeng-ui-caribe-circle-loading.d.ts +0 -28
  172. package/types/caribeng-ui-caribe-data-table.d.ts +0 -26
  173. package/types/caribeng-ui-caribe-file-select.d.ts +0 -31
  174. package/types/caribeng-ui-caribe-inline-alert.d.ts +0 -28
  175. package/types/caribeng-ui-caribe-input-date.d.ts +0 -40
  176. package/types/caribeng-ui-caribe-input-select.d.ts +0 -47
  177. package/types/caribeng-ui-caribe-loader.d.ts +0 -22
  178. package/types/caribeng-ui-caribe-modal.d.ts +0 -21
  179. package/types/caribeng-ui-caribe-pictogram.d.ts +0 -15
  180. package/types/caribeng-ui-caribe-pop-over.d.ts +0 -17
  181. package/types/caribeng-ui-caribe-preloader.d.ts +0 -24
  182. package/types/caribeng-ui-caribe-progress-bar.d.ts +0 -28
  183. package/types/caribeng-ui-caribe-stroke.d.ts +0 -17
  184. package/types/caribeng-ui-caribe-switch.d.ts +0 -55
  185. /package/fesm2022/{caribeng-ui-caribe-illustration.mjs → caribeng-ui-illustration.mjs} +0 -0
  186. /package/fesm2022/{caribeng-ui-caribe-logo.mjs → caribeng-ui-logo.mjs} +0 -0
@@ -0,0 +1,671 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, Injector, DestroyRef, input, model, booleanAttribute, output, signal, computed, viewChild, effect, untracked, afterNextRender, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { DOCUMENT } from '@angular/common';
4
+ import { injectCbTranslation, cbFormat } from '@caribeng/ui';
5
+ import { CbIconRegistry, CbIcon } from '@caribeng/ui/icon';
6
+
7
+ /** Tamaño mínimo del recuadro (fracción del lado). */
8
+ const MIN = 0.05;
9
+ const clamp = (n, min, max) => Math.min(max, Math.max(min, n));
10
+ /** Mantiene el recuadro dentro de la imagen y con un tamaño mínimo. */
11
+ function clampRect(rect) {
12
+ const w = clamp(rect.w, MIN, 1);
13
+ const h = clamp(rect.h, MIN, 1);
14
+ return { x: clamp(rect.x, 0, 1 - w), y: clamp(rect.y, 0, 1 - h), w, h };
15
+ }
16
+ /**
17
+ * Ajusta el recuadro a una proporción (ancho / alto en píxeles), lo más grande
18
+ * posible dentro del actual y centrado en él. `ratio` null = libre.
19
+ */
20
+ function fitRatio(rect, ratio, imageWidth, imageHeight) {
21
+ if (!ratio || !imageWidth || !imageHeight)
22
+ return clampRect(rect);
23
+ // Proporción expresada en fracciones de la imagen.
24
+ const r = ratio * (imageHeight / imageWidth);
25
+ let w = rect.w;
26
+ let h = w / r;
27
+ if (h > rect.h) {
28
+ h = rect.h;
29
+ w = h * r;
30
+ }
31
+ if (w > 1) {
32
+ w = 1;
33
+ h = w / r;
34
+ }
35
+ if (h > 1) {
36
+ h = 1;
37
+ w = h * r;
38
+ }
39
+ const cx = rect.x + rect.w / 2;
40
+ const cy = rect.y + rect.h / 2;
41
+ return clampRect({ x: cx - w / 2, y: cy - h / 2, w, h });
42
+ }
43
+ /** Recuadro inicial: el mayor posible con la proporción, centrado. */
44
+ function initialRect(ratio, imageWidth, imageHeight) {
45
+ return fitRatio({ x: 0, y: 0, w: 1, h: 1 }, ratio, imageWidth, imageHeight);
46
+ }
47
+ /**
48
+ * Redimensiona arrastrando una esquina (`nw`, `ne`, `sw`, `se`) un desplazamiento
49
+ * (dx, dy) en fracciones; con proporción, el alto sigue al ancho.
50
+ */
51
+ function resizeRect(rect, corner, dx, dy, ratio, imageWidth, imageHeight) {
52
+ let { x, y, w, h } = rect;
53
+ const right = x + w;
54
+ const bottom = y + h;
55
+ if (corner.includes('w'))
56
+ x = clamp(x + dx, 0, right - MIN);
57
+ const newRight = corner.includes('e') ? clamp(right + dx, x + MIN, 1) : right;
58
+ if (corner.includes('n'))
59
+ y = clamp(y + dy, 0, bottom - MIN);
60
+ const newBottom = corner.includes('s') ? clamp(bottom + dy, y + MIN, 1) : bottom;
61
+ w = newRight - x;
62
+ h = newBottom - y;
63
+ if (ratio && imageWidth && imageHeight) {
64
+ const r = ratio * (imageHeight / imageWidth);
65
+ h = w / r;
66
+ if (corner.includes('n'))
67
+ y = bottom - h;
68
+ if (y < 0 || y + h > 1) {
69
+ // No cabe: se limita por el alto.
70
+ h = corner.includes('n') ? bottom : 1 - y;
71
+ w = h * r;
72
+ if (corner.includes('w'))
73
+ x = right - w;
74
+ if (corner.includes('n'))
75
+ y = bottom - h;
76
+ }
77
+ }
78
+ return clampRect({ x, y, w, h });
79
+ }
80
+ /** Tamaño en píxeles del recorte, reducido para no pasar de `maxWidth` × `maxHeight`. */
81
+ function outputSize(rect, imageWidth, imageHeight, output = {}) {
82
+ const w = Math.max(1, Math.round(rect.w * imageWidth));
83
+ const h = Math.max(1, Math.round(rect.h * imageHeight));
84
+ const scale = Math.min(1, (output.maxWidth ?? Infinity) / w, (output.maxHeight ?? Infinity) / h);
85
+ return { w: Math.max(1, Math.round(w * scale)), h: Math.max(1, Math.round(h * scale)) };
86
+ }
87
+ /**
88
+ * Estilo de una vista previa del recorte hecha con CSS (sin canvas): la imagen como fondo,
89
+ * ampliada y desplazada para que solo se vea el recuadro.
90
+ */
91
+ function previewStyle(src, rect) {
92
+ const pos = (start, size) => (size >= 1 ? '0%' : `${(start / (1 - size)) * 100}%`);
93
+ return {
94
+ 'background-image': `url("${src.replace(/"/g, '%22')}")`,
95
+ 'background-size': `${100 / rect.w}% ${100 / rect.h}%`,
96
+ 'background-position': `${pos(rect.x, rect.w)} ${pos(rect.y, rect.h)}`,
97
+ 'background-repeat': 'no-repeat',
98
+ };
99
+ }
100
+ function loadImage(src) {
101
+ const image = new Image();
102
+ if (!/^(data|blob):/i.test(src))
103
+ image.crossOrigin = 'anonymous';
104
+ return new Promise((resolve, reject) => {
105
+ image.onload = () => resolve(image);
106
+ image.onerror = () => reject(new Error('No se pudo cargar la imagen'));
107
+ image.src = src;
108
+ });
109
+ }
110
+ /**
111
+ * Recorta los píxeles de la imagen. Las imágenes de otro dominio necesitan que
112
+ * su servidor permita CORS; si no, el navegador bloquea la lectura y se rechaza.
113
+ */
114
+ async function cropImage(src, rect, doc, output = {}) {
115
+ const image = await loadImage(src);
116
+ const nw = image.naturalWidth;
117
+ const nh = image.naturalHeight;
118
+ const sw = Math.max(1, Math.round(rect.w * nw));
119
+ const sh = Math.max(1, Math.round(rect.h * nh));
120
+ const size = outputSize(rect, nw, nh, output);
121
+ const canvas = doc.createElement('canvas');
122
+ canvas.width = size.w;
123
+ canvas.height = size.h;
124
+ const ctx = canvas.getContext('2d');
125
+ if (!ctx)
126
+ throw new Error('Canvas 2D no disponible');
127
+ if (output.circle) {
128
+ ctx.beginPath();
129
+ ctx.ellipse(size.w / 2, size.h / 2, size.w / 2, size.h / 2, 0, 0, Math.PI * 2);
130
+ ctx.clip();
131
+ }
132
+ ctx.imageSmoothingQuality = 'high';
133
+ ctx.drawImage(image, Math.round(rect.x * nw), Math.round(rect.y * nh), sw, sh, 0, 0, size.w, size.h);
134
+ // Un círculo necesita transparencia: siempre PNG.
135
+ const type = output.circle ? 'image/png' : (output.type ?? (/^data:image\/jpe?g|\.jpe?g(\?|$)/i.test(src) ? 'image/jpeg' : 'image/png'));
136
+ // toBlob lanza SecurityError si el canvas quedó "contaminado" (imagen sin CORS).
137
+ return new Promise((resolve, reject) => canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error('Sin datos'))), type, output.quality ?? 0.92));
138
+ }
139
+ /** Gira la imagen un cuarto de vuelta (`1` a la derecha, `-1` a la izquierda) y la devuelve como data URL PNG. */
140
+ async function rotateImage(src, turns, doc) {
141
+ const image = await loadImage(src);
142
+ const canvas = doc.createElement('canvas');
143
+ canvas.width = image.naturalHeight;
144
+ canvas.height = image.naturalWidth;
145
+ const ctx = canvas.getContext('2d');
146
+ if (!ctx)
147
+ throw new Error('Canvas 2D no disponible');
148
+ ctx.translate(canvas.width / 2, canvas.height / 2);
149
+ ctx.rotate((turns * Math.PI) / 2);
150
+ ctx.drawImage(image, -image.naturalWidth / 2, -image.naturalHeight / 2);
151
+ return canvas.toDataURL('image/png');
152
+ }
153
+ /** Lee un archivo como data URL. */
154
+ function readFileAsDataUrl(file) {
155
+ return new Promise((resolve, reject) => {
156
+ const reader = new FileReader();
157
+ reader.onload = () => resolve(String(reader.result));
158
+ reader.onerror = () => reject(reader.error);
159
+ reader.readAsDataURL(file);
160
+ });
161
+ }
162
+
163
+ let nextUid = 0;
164
+ /**
165
+ * Recortador de imagen de CaribeNG: recuadro que se mueve y redimensiona con ratón, dedo o
166
+ * teclado, proporciones fijas o libre, forma circular para fotos de perfil, giro, vista previa
167
+ * y selección de archivo (botón o arrastrar). Devuelve el recorte como `Blob` y data URL.
168
+ *
169
+ * @example
170
+ * ```html
171
+ * <cb-image-cropper [src]="foto" shape="circle" preview (cropped)="guardar($event.blob)" />
172
+ * ```
173
+ */
174
+ class CbImageCropper {
175
+ constructor() {
176
+ this.t = injectCbTranslation();
177
+ this.document = inject(DOCUMENT);
178
+ this.injector = inject(Injector);
179
+ this.destroyRef = inject(DestroyRef);
180
+ this.helpId = `cb-cropper-${++nextUid}-help`;
181
+ /** Imagen a recortar (URL o data URL). Las de otro dominio necesitan CORS para poder recortarse. */
182
+ this.src = input('', /* @ts-ignore */
183
+ ...(ngDevMode ? [{ debugName: "src" }] : /* istanbul ignore next */ []));
184
+ /** Recuadro en fracciones de la imagen (0–1). Se inicia al cargar la imagen si no se indica. */
185
+ this.rect = model(null, /* @ts-ignore */
186
+ ...(ngDevMode ? [{ debugName: "rect" }] : /* istanbul ignore next */ []));
187
+ /** Proporción ancho / alto (`null` = libre). Con `shape="circle"` siempre es 1. */
188
+ this.ratio = model(null, /* @ts-ignore */
189
+ ...(ngDevMode ? [{ debugName: "ratio" }] : /* istanbul ignore next */ []));
190
+ /** Proporciones de la barra (vacío para ocultarla). */
191
+ this.ratios = input([
192
+ { label: 'free', value: null },
193
+ { label: '1:1', value: 1 },
194
+ { label: '4:3', value: 4 / 3 },
195
+ { label: '16:9', value: 16 / 9 },
196
+ ], /* @ts-ignore */
197
+ ...(ngDevMode ? [{ debugName: "ratios" }] : /* istanbul ignore next */ []));
198
+ /** Recorte rectangular o circular (PNG con fondo transparente, para fotos de perfil). */
199
+ this.shape = input('rect', /* @ts-ignore */
200
+ ...(ngDevMode ? [{ debugName: "shape" }] : /* istanbul ignore next */ []));
201
+ /** Formato, calidad y tamaño máximo del recorte. */
202
+ this.output = input({}, /* @ts-ignore */
203
+ ...(ngDevMode ? [{ debugName: "output" }] : /* istanbul ignore next */ []));
204
+ /** Botones de girar 90°. */
205
+ this.rotatable = input(true, { ...(ngDevMode ? { debugName: "rotatable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
206
+ /** Vista previa del recorte. */
207
+ this.preview = input(false, { ...(ngDevMode ? { debugName: "preview" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
208
+ /** Botón para elegir otra imagen (y arrastrar archivos al área vacía). */
209
+ this.chooser = input(true, { ...(ngDevMode ? { debugName: "chooser" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
210
+ /** Botón Recortar (sin él, recorta llamando a `crop()`). */
211
+ this.showApply = input(true, { ...(ngDevMode ? { debugName: "showApply" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
212
+ this.accept = input('image/*', /* @ts-ignore */
213
+ ...(ngDevMode ? [{ debugName: "accept" }] : /* istanbul ignore next */ []));
214
+ /** Alto máximo de la imagen en pantalla. */
215
+ this.maxHeight = input('24rem', /* @ts-ignore */
216
+ ...(ngDevMode ? [{ debugName: "maxHeight" }] : /* istanbul ignore next */ []));
217
+ this.emptyMessage = input(undefined, /* @ts-ignore */
218
+ ...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
219
+ /** Recorte hecho con el botón Recortar o Enter. */
220
+ this.cropped = output();
221
+ /** Imagen elegida por el usuario (archivo). */
222
+ this.fileSelect = output();
223
+ /** No se pudo cargar o recortar la imagen. */
224
+ this.cropError = output();
225
+ this.corners = ['nw', 'ne', 'sw', 'se'];
226
+ /** Imagen en pantalla: `src`, el archivo elegido o la imagen girada. */
227
+ this.displaySrc = signal('', /* @ts-ignore */
228
+ ...(ngDevMode ? [{ debugName: "displaySrc" }] : /* istanbul ignore next */ []));
229
+ this.natural = signal(null, /* @ts-ignore */
230
+ ...(ngDevMode ? [{ debugName: "natural" }] : /* istanbul ignore next */ []));
231
+ this.busy = signal(false, /* @ts-ignore */
232
+ ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
233
+ this.error = signal('', /* @ts-ignore */
234
+ ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
235
+ this.dragOver = signal(false, /* @ts-ignore */
236
+ ...(ngDevMode ? [{ debugName: "dragOver" }] : /* istanbul ignore next */ []));
237
+ this.announcement = signal('', /* @ts-ignore */
238
+ ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
239
+ this.effRatio = computed(() => (this.shape() === 'circle' ? 1 : this.ratio()), /* @ts-ignore */
240
+ ...(ngDevMode ? [{ debugName: "effRatio" }] : /* istanbul ignore next */ []));
241
+ /** El recuadro vigente (`rect` es null hasta que carga la imagen). */
242
+ this.current = computed(() => this.rect() ?? { x: 0, y: 0, w: 1, h: 1 }, /* @ts-ignore */
243
+ ...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
244
+ this.valueText = computed(() => {
245
+ const r = this.current();
246
+ const n = this.natural();
247
+ const pct = (v) => `${Math.round(v * 100)} %`;
248
+ const px = n ? outputSize(r, n.w, n.h, this.output()) : { w: 0, h: 0 };
249
+ return cbFormat(this.t().cropperValue, pct(r.w), pct(r.h), px.w, px.h);
250
+ }, /* @ts-ignore */
251
+ ...(ngDevMode ? [{ debugName: "valueText" }] : /* istanbul ignore next */ []));
252
+ this.previewCss = computed(() => {
253
+ const r = this.current();
254
+ const n = this.natural();
255
+ const aspect = n ? (r.w * n.w) / (r.h * n.h) : 1;
256
+ return { ...previewStyle(this.displaySrc(), r), 'aspect-ratio': String(aspect) };
257
+ }, /* @ts-ignore */
258
+ ...(ngDevMode ? [{ debugName: "previewCss" }] : /* istanbul ignore next */ []));
259
+ this.stage = viewChild('stage', /* @ts-ignore */
260
+ ...(ngDevMode ? [{ debugName: "stage" }] : /* istanbul ignore next */ []));
261
+ this.box = viewChild('box', /* @ts-ignore */
262
+ ...(ngDevMode ? [{ debugName: "box" }] : /* istanbul ignore next */ []));
263
+ this.fileInput = viewChild.required('file');
264
+ this.announceFlip = false;
265
+ const registry = inject(CbIconRegistry);
266
+ for (const [name, body] of Object.entries(ICONS)) {
267
+ if (!registry.has(name))
268
+ registry.register({ [name]: body });
269
+ }
270
+ effect(() => {
271
+ const src = this.src();
272
+ untracked(() => this.load(src, false));
273
+ });
274
+ }
275
+ // --- API pública ---
276
+ /** Recorta y devuelve el resultado (sin emitir `cropped`). */
277
+ async crop() {
278
+ const n = this.natural();
279
+ const src = this.displaySrc();
280
+ if (!n || !src)
281
+ throw new Error('Sin imagen');
282
+ const rect = this.current();
283
+ const out = { ...this.output(), circle: this.shape() === 'circle' || this.output().circle };
284
+ const blob = await cropImage(src, rect, this.document, out);
285
+ const size = outputSize(rect, n.w, n.h, out);
286
+ return { blob, dataUrl: await readFileAsDataUrl(blob), rect, width: size.w, height: size.h };
287
+ }
288
+ /** Abre el selector de archivos. */
289
+ choose() {
290
+ this.fileInput().nativeElement.click();
291
+ }
292
+ /** Proporción fija (ancho / alto) o libre (`null`). */
293
+ setRatio(ratio) {
294
+ this.ratio.set(ratio);
295
+ const n = this.natural();
296
+ if (n)
297
+ this.rect.set(fitRatio(this.current(), this.effRatio(), n.w, n.h));
298
+ }
299
+ // --- Eventos ---
300
+ onLoad(event) {
301
+ const img = event.target;
302
+ const n = { w: img.naturalWidth || img.width || 1, h: img.naturalHeight || img.height || 1 };
303
+ this.natural.set(n);
304
+ this.error.set('');
305
+ const given = this.rect();
306
+ this.rect.set(given ? fitRatio(clampRect(given), this.effRatio(), n.w, n.h) : initialRect(this.effRatio(), n.w, n.h));
307
+ }
308
+ onImageError() {
309
+ this.natural.set(null);
310
+ this.error.set(this.t().cropperLoadError);
311
+ this.cropError.emit(new Error('No se pudo cargar la imagen'));
312
+ }
313
+ async apply() {
314
+ if (this.busy())
315
+ return;
316
+ this.busy.set(true);
317
+ this.error.set('');
318
+ try {
319
+ const result = await this.crop();
320
+ this.cropped.emit(result);
321
+ this.announce(this.t().cropperDone);
322
+ }
323
+ catch (e) {
324
+ this.error.set(this.t().cropError);
325
+ this.cropError.emit(e);
326
+ }
327
+ finally {
328
+ this.busy.set(false);
329
+ }
330
+ }
331
+ async rotate(turns) {
332
+ const src = this.displaySrc();
333
+ if (!src || this.busy())
334
+ return;
335
+ this.busy.set(true);
336
+ try {
337
+ const rotated = await rotateImage(src, turns, this.document);
338
+ this.rect.set(null);
339
+ this.load(rotated, true);
340
+ this.announce(turns === 1 ? this.t().cropperRotatedRight : this.t().cropperRotatedLeft);
341
+ }
342
+ catch (e) {
343
+ this.error.set(this.t().cropError);
344
+ this.cropError.emit(e);
345
+ }
346
+ finally {
347
+ this.busy.set(false);
348
+ }
349
+ }
350
+ async onFile(event) {
351
+ const input = event.target;
352
+ const file = input.files?.[0];
353
+ input.value = '';
354
+ if (file)
355
+ await this.useFile(file);
356
+ }
357
+ onDragOver(event) {
358
+ if (!this.chooser())
359
+ return;
360
+ event.preventDefault();
361
+ this.dragOver.set(true);
362
+ }
363
+ async onDrop(event) {
364
+ if (!this.chooser())
365
+ return;
366
+ event.preventDefault();
367
+ this.dragOver.set(false);
368
+ const file = event.dataTransfer?.files?.[0];
369
+ if (file)
370
+ await this.useFile(file);
371
+ }
372
+ /** Mueve (`corner` null) o redimensiona el recuadro arrastrando. */
373
+ startDrag(event, corner) {
374
+ const stage = this.stage()?.nativeElement;
375
+ const n = this.natural();
376
+ if (!stage || !n || (event.pointerType === 'mouse' && event.button !== 0))
377
+ return;
378
+ event.preventDefault();
379
+ event.stopPropagation();
380
+ this.box()?.nativeElement.focus({ preventScroll: true });
381
+ const bounds = stage.getBoundingClientRect();
382
+ const from = { x: event.clientX, y: event.clientY, rect: this.current() };
383
+ const move = (e) => {
384
+ const dx = (e.clientX - from.x) / (bounds.width || 1);
385
+ const dy = (e.clientY - from.y) / (bounds.height || 1);
386
+ this.rect.set(corner
387
+ ? resizeRect(from.rect, corner, dx, dy, this.effRatio(), n.w, n.h)
388
+ : clampRect({ ...from.rect, x: from.rect.x + dx, y: from.rect.y + dy }));
389
+ };
390
+ const up = () => {
391
+ this.document.removeEventListener('pointermove', move);
392
+ this.document.removeEventListener('pointerup', up);
393
+ this.document.removeEventListener('pointercancel', up);
394
+ };
395
+ this.document.addEventListener('pointermove', move);
396
+ this.document.addEventListener('pointerup', up);
397
+ this.document.addEventListener('pointercancel', up);
398
+ }
399
+ /** Flechas: mover (2 %, 10 % con Ctrl); Mayús + flechas: tamaño desde la esquina inferior derecha; Enter: recortar. */
400
+ onKeydown(event) {
401
+ const n = this.natural();
402
+ if (!n)
403
+ return;
404
+ if (event.key === 'Enter' && this.showApply()) {
405
+ event.preventDefault();
406
+ void this.apply();
407
+ return;
408
+ }
409
+ const step = event.ctrlKey || event.metaKey ? 0.1 : 0.02;
410
+ const deltas = { ArrowLeft: [-step, 0], ArrowRight: [step, 0], ArrowUp: [0, -step], ArrowDown: [0, step] };
411
+ const d = deltas[event.key];
412
+ if (!d)
413
+ return;
414
+ event.preventDefault();
415
+ const r = this.current();
416
+ this.rect.set(event.shiftKey ? resizeRect(r, 'se', d[0], d[1], this.effRatio(), n.w, n.h) : clampRect({ ...r, x: r.x + d[0], y: r.y + d[1] }));
417
+ }
418
+ // --- Internos ---
419
+ load(src, keepFocus) {
420
+ this.natural.set(null);
421
+ this.error.set('');
422
+ this.displaySrc.set(src);
423
+ if (keepFocus)
424
+ this.focusWhenReady();
425
+ }
426
+ async useFile(file) {
427
+ if (!file.type.startsWith('image/')) {
428
+ this.error.set(this.t().cropperInvalidFile);
429
+ return;
430
+ }
431
+ this.fileSelect.emit(file);
432
+ this.rect.set(null);
433
+ this.load(await readFileAsDataUrl(file), true);
434
+ }
435
+ /** El recuadro aparece cuando carga la imagen: entonces recibe el foco. */
436
+ focusWhenReady() {
437
+ const stop = effect(() => {
438
+ if (this.natural()) {
439
+ untracked(() => afterNextRender(() => this.box()?.nativeElement.focus(), { injector: this.injector }));
440
+ stop.destroy();
441
+ }
442
+ }, { ...(ngDevMode ? { debugName: "stop" } : /* istanbul ignore next */ {}), injector: this.injector });
443
+ }
444
+ /** Un espacio duro alterno hace que el mismo texto cuente como cambio y se vuelva a anunciar. */
445
+ announce(text) {
446
+ this.announceFlip = !this.announceFlip;
447
+ this.announcement.set(this.announceFlip ? text : `${text} `);
448
+ }
449
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbImageCropper, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
450
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: CbImageCropper, isStandalone: true, selector: "cb-image-cropper", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, rect: { classPropertyName: "rect", publicName: "rect", isSignal: true, isRequired: false, transformFunction: null }, ratio: { classPropertyName: "ratio", publicName: "ratio", isSignal: true, isRequired: false, transformFunction: null }, ratios: { classPropertyName: "ratios", publicName: "ratios", isSignal: true, isRequired: false, transformFunction: null }, shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, output: { classPropertyName: "output", publicName: "output", isSignal: true, isRequired: false, transformFunction: null }, rotatable: { classPropertyName: "rotatable", publicName: "rotatable", isSignal: true, isRequired: false, transformFunction: null }, preview: { classPropertyName: "preview", publicName: "preview", isSignal: true, isRequired: false, transformFunction: null }, chooser: { classPropertyName: "chooser", publicName: "chooser", isSignal: true, isRequired: false, transformFunction: null }, showApply: { classPropertyName: "showApply", publicName: "showApply", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rect: "rectChange", ratio: "ratioChange", cropped: "cropped", fileSelect: "fileSelect", cropError: "cropError" }, host: { classAttribute: "cb-image-cropper" }, viewQueries: [{ propertyName: "stage", first: true, predicate: ["stage"], descendants: true, isSignal: true }, { propertyName: "box", first: true, predicate: ["box"], descendants: true, isSignal: true }, { propertyName: "fileInput", first: true, predicate: ["file"], descendants: true, isSignal: true }], ngImport: i0, template: `
451
+ @if (displaySrc()) {
452
+ <div class="cb-cropper__main">
453
+ <div class="cb-cropper__stage" #stage>
454
+ <img
455
+ class="cb-cropper__img"
456
+ [src]="displaySrc()"
457
+ alt=""
458
+ draggable="false"
459
+ [style.max-height]="maxHeight()"
460
+ (load)="onLoad($event)"
461
+ (error)="onImageError()"
462
+ />
463
+ @if (natural()) {
464
+ <div
465
+ #box
466
+ class="cb-cropper__box"
467
+ [class.cb-cropper__box--circle]="shape() === 'circle'"
468
+ role="slider"
469
+ tabindex="0"
470
+ [attr.aria-label]="t().cropArea"
471
+ [attr.aria-valuetext]="valueText()"
472
+ [attr.aria-describedby]="helpId"
473
+ [style.left.%]="current().x * 100"
474
+ [style.top.%]="current().y * 100"
475
+ [style.width.%]="current().w * 100"
476
+ [style.height.%]="current().h * 100"
477
+ (pointerdown)="startDrag($event, null)"
478
+ (keydown)="onKeydown($event)"
479
+ >
480
+ <span class="cb-cropper__grid" aria-hidden="true"></span>
481
+ @for (corner of corners; track corner) {
482
+ <span [class]="'cb-cropper__handle cb-cropper__handle--' + corner" aria-hidden="true" (pointerdown)="startDrag($event, corner)"></span>
483
+ }
484
+ </div>
485
+ }
486
+ </div>
487
+
488
+ @if (preview() && natural()) {
489
+ <div class="cb-cropper__preview-wrap">
490
+ <span class="cb-cropper__preview-label">{{ t().cropperPreview }}</span>
491
+ <div
492
+ class="cb-cropper__preview"
493
+ [class.cb-cropper__preview--circle]="shape() === 'circle'"
494
+ role="img"
495
+ [attr.aria-label]="t().cropperPreview"
496
+ [style]="previewCss()"
497
+ ></div>
498
+ </div>
499
+ }
500
+ </div>
501
+
502
+ <p class="cb-cropper__help" [id]="helpId">{{ t().cropInstructions }}</p>
503
+
504
+ <div class="cb-cropper__bar">
505
+ @if (ratios().length && shape() !== 'circle') {
506
+ <div class="cb-cropper__ratios" role="group" [attr.aria-label]="t().cropperRatio">
507
+ @for (r of ratios(); track r.label) {
508
+ <button type="button" class="cb-cropper__btn" [attr.aria-pressed]="ratio() === r.value" (click)="setRatio(r.value)">
509
+ {{ r.value === null ? t().cropFree : r.label }}
510
+ </button>
511
+ }
512
+ </div>
513
+ }
514
+ @if (rotatable()) {
515
+ <button type="button" class="cb-cropper__icon-btn" [attr.aria-label]="t().cropperRotateLeft" [attr.title]="t().cropperRotateLeft" [disabled]="busy()" (click)="rotate(-1)">
516
+ <cb-icon name="rotate-ccw" [size]="16" aria-hidden="true" />
517
+ </button>
518
+ <button type="button" class="cb-cropper__icon-btn" [attr.aria-label]="t().cropperRotateRight" [attr.title]="t().cropperRotateRight" [disabled]="busy()" (click)="rotate(1)">
519
+ <cb-icon name="rotate-cw" [size]="16" aria-hidden="true" />
520
+ </button>
521
+ }
522
+ <span class="cb-cropper__spacer"></span>
523
+ @if (chooser()) {
524
+ <button type="button" class="cb-cropper__btn" (click)="choose()">{{ t().cropperChange }}</button>
525
+ }
526
+ @if (showApply()) {
527
+ <button type="button" class="cb-cropper__btn cb-cropper__btn--primary" [disabled]="busy() || !natural()" (click)="apply()">{{ t().cropImage }}</button>
528
+ }
529
+ </div>
530
+ } @else {
531
+ <div
532
+ class="cb-cropper__empty"
533
+ [class.cb-cropper__empty--over]="dragOver()"
534
+ (dragover)="onDragOver($event)"
535
+ (dragleave)="dragOver.set(false)"
536
+ (drop)="onDrop($event)"
537
+ >
538
+ <cb-icon name="cb-image" [size]="32" aria-hidden="true" />
539
+ <p>{{ emptyMessage() ?? t().cropperEmpty }}</p>
540
+ @if (chooser()) {
541
+ <button type="button" class="cb-cropper__btn cb-cropper__btn--primary" (click)="choose()">{{ t().cropperChoose }}</button>
542
+ }
543
+ </div>
544
+ }
545
+
546
+ @if (error(); as e) {
547
+ <p class="cb-cropper__error" role="alert">{{ e }}</p>
548
+ }
549
+ <span class="cb-cropper__sr" aria-live="polite">{{ announcement() }}</span>
550
+ <input #file type="file" class="cb-cropper__file" [accept]="accept()" tabindex="-1" aria-hidden="true" (change)="onFile($event)" />
551
+ `, isInline: true, styles: [":host{display:flex;flex-direction:column;gap:.75rem;color:var(--cb-color-text);font-family:var(--cb-font-sans);font-size:var(--cb-text-sm, .875rem)}.cb-cropper__main{display:flex;flex-wrap:wrap;align-items:flex-start;gap:1rem}.cb-cropper__stage{position:relative;max-width:100%;overflow:hidden;border-radius:var(--cb-radius-md, 6px);background:repeating-conic-gradient(#e2e8f0 0% 25%,#f8fafc 0% 50%) 0 0 / 16px 16px;-webkit-user-select:none;user-select:none;touch-action:none}.cb-cropper__img{display:block;max-width:100%}.cb-cropper__box{position:absolute;box-sizing:border-box;border:2px solid #fff;box-shadow:0 0 0 9999px #0000008c;cursor:move;outline:none}.cb-cropper__box--circle{border-radius:50%}.cb-cropper__box:focus-visible{border-color:var(--cb-color-focus-ring);box-shadow:0 0 0 2px #fff,0 0 0 9999px #0000008c}.cb-cropper__grid{position:absolute;inset:0;background:linear-gradient(to right,transparent calc(33.33% - .5px),rgb(255 255 255 / .5) calc(33.33% - .5px),rgb(255 255 255 / .5) calc(33.33% + .5px),transparent calc(33.33% + .5px),transparent calc(66.66% - .5px),rgb(255 255 255 / .5) calc(66.66% - .5px),rgb(255 255 255 / .5) calc(66.66% + .5px),transparent calc(66.66% + .5px)),linear-gradient(to bottom,transparent calc(33.33% - .5px),rgb(255 255 255 / .5) calc(33.33% - .5px),rgb(255 255 255 / .5) calc(33.33% + .5px),transparent calc(33.33% + .5px),transparent calc(66.66% - .5px),rgb(255 255 255 / .5) calc(66.66% - .5px),rgb(255 255 255 / .5) calc(66.66% + .5px),transparent calc(66.66% + .5px));border-radius:inherit;pointer-events:none}.cb-cropper__handle{position:absolute;width:14px;height:14px;box-sizing:border-box;border:2px solid var(--cb-color-primary);border-radius:2px;background:#fff}@media(pointer:coarse){.cb-cropper__handle{width:22px;height:22px}}.cb-cropper__handle--nw{left:-8px;top:-8px;cursor:nwse-resize}.cb-cropper__handle--ne{right:-8px;top:-8px;cursor:nesw-resize}.cb-cropper__handle--sw{left:-8px;bottom:-8px;cursor:nesw-resize}.cb-cropper__handle--se{right:-8px;bottom:-8px;cursor:nwse-resize}.cb-cropper__preview-wrap{display:flex;flex-direction:column;gap:.375rem}.cb-cropper__preview-label{color:var(--cb-color-text-muted);font-size:var(--cb-text-xs, .75rem)}.cb-cropper__preview{width:8rem;max-height:12rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md, 6px)}.cb-cropper__preview--circle{border-radius:50%}.cb-cropper__help{margin:0;color:var(--cb-color-text-muted);font-size:var(--cb-text-xs, .75rem)}.cb-cropper__bar{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.cb-cropper__ratios{display:inline-flex;flex-wrap:wrap;gap:.25rem}.cb-cropper__spacer{flex:1}.cb-cropper__btn{min-height:2rem;padding:0 .75rem;border:1px solid var(--cb-color-border-strong, var(--cb-color-border));border-radius:var(--cb-radius-md, 6px);background:transparent;color:var(--cb-color-text);font:inherit;font-weight:var(--cb-font-weight-medium, 500);cursor:pointer}.cb-cropper__btn:hover:not(:disabled),.cb-cropper__icon-btn:hover:not(:disabled){background:var(--cb-color-surface-hover)}.cb-cropper__btn[aria-pressed=true]{border-color:var(--cb-color-primary);background:color-mix(in srgb,var(--cb-color-primary) 14%,transparent)}.cb-cropper__btn--primary{border-color:var(--cb-color-primary);background:var(--cb-color-primary);color:var(--cb-color-primary-contrast, #fff)}.cb-cropper__btn--primary:hover:not(:disabled){background:var(--cb-color-primary-700, var(--cb-color-primary))}.cb-cropper__btn:disabled,.cb-cropper__icon-btn:disabled{opacity:.5;cursor:not-allowed}.cb-cropper__icon-btn{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border:1px solid var(--cb-color-border-strong, var(--cb-color-border));border-radius:var(--cb-radius-md, 6px);background:transparent;color:var(--cb-color-text);cursor:pointer}.cb-cropper__btn:focus-visible,.cb-cropper__icon-btn:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:1px}.cb-cropper__empty{display:flex;flex-direction:column;align-items:center;gap:.75rem;padding:2.5rem 1rem;border:2px dashed var(--cb-color-border-strong, var(--cb-color-border));border-radius:var(--cb-radius-lg, 8px);color:var(--cb-color-text-muted);text-align:center}.cb-cropper__empty p{margin:0}.cb-cropper__empty--over{border-color:var(--cb-color-primary);background:color-mix(in srgb,var(--cb-color-primary) 6%,transparent)}.cb-cropper__error{margin:0;color:var(--cb-color-danger)}.cb-cropper__file{display:none}.cb-cropper__sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "component", type: CbIcon, selector: "cb-icon", inputs: ["name", "size", "spin", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
552
+ }
553
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbImageCropper, decorators: [{
554
+ type: Component,
555
+ args: [{ selector: 'cb-image-cropper', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, imports: [CbIcon], host: { class: 'cb-image-cropper' }, template: `
556
+ @if (displaySrc()) {
557
+ <div class="cb-cropper__main">
558
+ <div class="cb-cropper__stage" #stage>
559
+ <img
560
+ class="cb-cropper__img"
561
+ [src]="displaySrc()"
562
+ alt=""
563
+ draggable="false"
564
+ [style.max-height]="maxHeight()"
565
+ (load)="onLoad($event)"
566
+ (error)="onImageError()"
567
+ />
568
+ @if (natural()) {
569
+ <div
570
+ #box
571
+ class="cb-cropper__box"
572
+ [class.cb-cropper__box--circle]="shape() === 'circle'"
573
+ role="slider"
574
+ tabindex="0"
575
+ [attr.aria-label]="t().cropArea"
576
+ [attr.aria-valuetext]="valueText()"
577
+ [attr.aria-describedby]="helpId"
578
+ [style.left.%]="current().x * 100"
579
+ [style.top.%]="current().y * 100"
580
+ [style.width.%]="current().w * 100"
581
+ [style.height.%]="current().h * 100"
582
+ (pointerdown)="startDrag($event, null)"
583
+ (keydown)="onKeydown($event)"
584
+ >
585
+ <span class="cb-cropper__grid" aria-hidden="true"></span>
586
+ @for (corner of corners; track corner) {
587
+ <span [class]="'cb-cropper__handle cb-cropper__handle--' + corner" aria-hidden="true" (pointerdown)="startDrag($event, corner)"></span>
588
+ }
589
+ </div>
590
+ }
591
+ </div>
592
+
593
+ @if (preview() && natural()) {
594
+ <div class="cb-cropper__preview-wrap">
595
+ <span class="cb-cropper__preview-label">{{ t().cropperPreview }}</span>
596
+ <div
597
+ class="cb-cropper__preview"
598
+ [class.cb-cropper__preview--circle]="shape() === 'circle'"
599
+ role="img"
600
+ [attr.aria-label]="t().cropperPreview"
601
+ [style]="previewCss()"
602
+ ></div>
603
+ </div>
604
+ }
605
+ </div>
606
+
607
+ <p class="cb-cropper__help" [id]="helpId">{{ t().cropInstructions }}</p>
608
+
609
+ <div class="cb-cropper__bar">
610
+ @if (ratios().length && shape() !== 'circle') {
611
+ <div class="cb-cropper__ratios" role="group" [attr.aria-label]="t().cropperRatio">
612
+ @for (r of ratios(); track r.label) {
613
+ <button type="button" class="cb-cropper__btn" [attr.aria-pressed]="ratio() === r.value" (click)="setRatio(r.value)">
614
+ {{ r.value === null ? t().cropFree : r.label }}
615
+ </button>
616
+ }
617
+ </div>
618
+ }
619
+ @if (rotatable()) {
620
+ <button type="button" class="cb-cropper__icon-btn" [attr.aria-label]="t().cropperRotateLeft" [attr.title]="t().cropperRotateLeft" [disabled]="busy()" (click)="rotate(-1)">
621
+ <cb-icon name="rotate-ccw" [size]="16" aria-hidden="true" />
622
+ </button>
623
+ <button type="button" class="cb-cropper__icon-btn" [attr.aria-label]="t().cropperRotateRight" [attr.title]="t().cropperRotateRight" [disabled]="busy()" (click)="rotate(1)">
624
+ <cb-icon name="rotate-cw" [size]="16" aria-hidden="true" />
625
+ </button>
626
+ }
627
+ <span class="cb-cropper__spacer"></span>
628
+ @if (chooser()) {
629
+ <button type="button" class="cb-cropper__btn" (click)="choose()">{{ t().cropperChange }}</button>
630
+ }
631
+ @if (showApply()) {
632
+ <button type="button" class="cb-cropper__btn cb-cropper__btn--primary" [disabled]="busy() || !natural()" (click)="apply()">{{ t().cropImage }}</button>
633
+ }
634
+ </div>
635
+ } @else {
636
+ <div
637
+ class="cb-cropper__empty"
638
+ [class.cb-cropper__empty--over]="dragOver()"
639
+ (dragover)="onDragOver($event)"
640
+ (dragleave)="dragOver.set(false)"
641
+ (drop)="onDrop($event)"
642
+ >
643
+ <cb-icon name="cb-image" [size]="32" aria-hidden="true" />
644
+ <p>{{ emptyMessage() ?? t().cropperEmpty }}</p>
645
+ @if (chooser()) {
646
+ <button type="button" class="cb-cropper__btn cb-cropper__btn--primary" (click)="choose()">{{ t().cropperChoose }}</button>
647
+ }
648
+ </div>
649
+ }
650
+
651
+ @if (error(); as e) {
652
+ <p class="cb-cropper__error" role="alert">{{ e }}</p>
653
+ }
654
+ <span class="cb-cropper__sr" aria-live="polite">{{ announcement() }}</span>
655
+ <input #file type="file" class="cb-cropper__file" [accept]="accept()" tabindex="-1" aria-hidden="true" (change)="onFile($event)" />
656
+ `, styles: [":host{display:flex;flex-direction:column;gap:.75rem;color:var(--cb-color-text);font-family:var(--cb-font-sans);font-size:var(--cb-text-sm, .875rem)}.cb-cropper__main{display:flex;flex-wrap:wrap;align-items:flex-start;gap:1rem}.cb-cropper__stage{position:relative;max-width:100%;overflow:hidden;border-radius:var(--cb-radius-md, 6px);background:repeating-conic-gradient(#e2e8f0 0% 25%,#f8fafc 0% 50%) 0 0 / 16px 16px;-webkit-user-select:none;user-select:none;touch-action:none}.cb-cropper__img{display:block;max-width:100%}.cb-cropper__box{position:absolute;box-sizing:border-box;border:2px solid #fff;box-shadow:0 0 0 9999px #0000008c;cursor:move;outline:none}.cb-cropper__box--circle{border-radius:50%}.cb-cropper__box:focus-visible{border-color:var(--cb-color-focus-ring);box-shadow:0 0 0 2px #fff,0 0 0 9999px #0000008c}.cb-cropper__grid{position:absolute;inset:0;background:linear-gradient(to right,transparent calc(33.33% - .5px),rgb(255 255 255 / .5) calc(33.33% - .5px),rgb(255 255 255 / .5) calc(33.33% + .5px),transparent calc(33.33% + .5px),transparent calc(66.66% - .5px),rgb(255 255 255 / .5) calc(66.66% - .5px),rgb(255 255 255 / .5) calc(66.66% + .5px),transparent calc(66.66% + .5px)),linear-gradient(to bottom,transparent calc(33.33% - .5px),rgb(255 255 255 / .5) calc(33.33% - .5px),rgb(255 255 255 / .5) calc(33.33% + .5px),transparent calc(33.33% + .5px),transparent calc(66.66% - .5px),rgb(255 255 255 / .5) calc(66.66% - .5px),rgb(255 255 255 / .5) calc(66.66% + .5px),transparent calc(66.66% + .5px));border-radius:inherit;pointer-events:none}.cb-cropper__handle{position:absolute;width:14px;height:14px;box-sizing:border-box;border:2px solid var(--cb-color-primary);border-radius:2px;background:#fff}@media(pointer:coarse){.cb-cropper__handle{width:22px;height:22px}}.cb-cropper__handle--nw{left:-8px;top:-8px;cursor:nwse-resize}.cb-cropper__handle--ne{right:-8px;top:-8px;cursor:nesw-resize}.cb-cropper__handle--sw{left:-8px;bottom:-8px;cursor:nesw-resize}.cb-cropper__handle--se{right:-8px;bottom:-8px;cursor:nwse-resize}.cb-cropper__preview-wrap{display:flex;flex-direction:column;gap:.375rem}.cb-cropper__preview-label{color:var(--cb-color-text-muted);font-size:var(--cb-text-xs, .75rem)}.cb-cropper__preview{width:8rem;max-height:12rem;border:1px solid var(--cb-color-border);border-radius:var(--cb-radius-md, 6px)}.cb-cropper__preview--circle{border-radius:50%}.cb-cropper__help{margin:0;color:var(--cb-color-text-muted);font-size:var(--cb-text-xs, .75rem)}.cb-cropper__bar{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.cb-cropper__ratios{display:inline-flex;flex-wrap:wrap;gap:.25rem}.cb-cropper__spacer{flex:1}.cb-cropper__btn{min-height:2rem;padding:0 .75rem;border:1px solid var(--cb-color-border-strong, var(--cb-color-border));border-radius:var(--cb-radius-md, 6px);background:transparent;color:var(--cb-color-text);font:inherit;font-weight:var(--cb-font-weight-medium, 500);cursor:pointer}.cb-cropper__btn:hover:not(:disabled),.cb-cropper__icon-btn:hover:not(:disabled){background:var(--cb-color-surface-hover)}.cb-cropper__btn[aria-pressed=true]{border-color:var(--cb-color-primary);background:color-mix(in srgb,var(--cb-color-primary) 14%,transparent)}.cb-cropper__btn--primary{border-color:var(--cb-color-primary);background:var(--cb-color-primary);color:var(--cb-color-primary-contrast, #fff)}.cb-cropper__btn--primary:hover:not(:disabled){background:var(--cb-color-primary-700, var(--cb-color-primary))}.cb-cropper__btn:disabled,.cb-cropper__icon-btn:disabled{opacity:.5;cursor:not-allowed}.cb-cropper__icon-btn{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border:1px solid var(--cb-color-border-strong, var(--cb-color-border));border-radius:var(--cb-radius-md, 6px);background:transparent;color:var(--cb-color-text);cursor:pointer}.cb-cropper__btn:focus-visible,.cb-cropper__icon-btn:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:1px}.cb-cropper__empty{display:flex;flex-direction:column;align-items:center;gap:.75rem;padding:2.5rem 1rem;border:2px dashed var(--cb-color-border-strong, var(--cb-color-border));border-radius:var(--cb-radius-lg, 8px);color:var(--cb-color-text-muted);text-align:center}.cb-cropper__empty p{margin:0}.cb-cropper__empty--over{border-color:var(--cb-color-primary);background:color-mix(in srgb,var(--cb-color-primary) 6%,transparent)}.cb-cropper__error{margin:0;color:var(--cb-color-danger)}.cb-cropper__file{display:none}.cb-cropper__sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}\n"] }]
657
+ }], ctorParameters: () => [], propDecorators: { src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], rect: [{ type: i0.Input, args: [{ isSignal: true, alias: "rect", required: false }] }, { type: i0.Output, args: ["rectChange"] }], ratio: [{ type: i0.Input, args: [{ isSignal: true, alias: "ratio", required: false }] }, { type: i0.Output, args: ["ratioChange"] }], ratios: [{ type: i0.Input, args: [{ isSignal: true, alias: "ratios", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], output: [{ type: i0.Input, args: [{ isSignal: true, alias: "output", required: false }] }], rotatable: [{ type: i0.Input, args: [{ isSignal: true, alias: "rotatable", required: false }] }], preview: [{ type: i0.Input, args: [{ isSignal: true, alias: "preview", required: false }] }], chooser: [{ type: i0.Input, args: [{ isSignal: true, alias: "chooser", required: false }] }], showApply: [{ type: i0.Input, args: [{ isSignal: true, alias: "showApply", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], cropped: [{ type: i0.Output, args: ["cropped"] }], fileSelect: [{ type: i0.Output, args: ["fileSelect"] }], cropError: [{ type: i0.Output, args: ["cropError"] }], stage: [{ type: i0.ViewChild, args: ['stage', { isSignal: true }] }], box: [{ type: i0.ViewChild, args: ['box', { isSignal: true }] }], fileInput: [{ type: i0.ViewChild, args: ['file', { isSignal: true }] }] } });
658
+ /** Iconos propios (Lucide `image`). `rotate-ccw` y `rotate-cw` están en el set núcleo. */
659
+ const ICONS = {
660
+ 'cb-image': '<rect width="18" height="18" x="3" y="3" rx="2" ry="2" /><circle cx="9" cy="9" r="2" /><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />',
661
+ };
662
+
663
+ /*
664
+ * Public API Surface de @caribeng/ui/image-cropper
665
+ */
666
+
667
+ /**
668
+ * Generated bundle index. Do not edit.
669
+ */
670
+
671
+ export { CbImageCropper, clampRect, cropImage, fitRatio, initialRect, outputSize, previewStyle, readFileAsDataUrl, resizeRect, rotateImage };