@rojaostudio/ds 1.0.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 (291) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +89 -0
  3. package/brand/rojao-black-on-white.png +0 -0
  4. package/brand/rojao-black.png +0 -0
  5. package/brand/rojao-white-on-black.png +0 -0
  6. package/brand/rojao-white.png +0 -0
  7. package/dist/chunk-ORMEWXMH.js +37 -0
  8. package/dist/compat/generate.d.ts +6 -0
  9. package/dist/compat/generate.js +1 -0
  10. package/dist/compat/recipes.d.ts +1 -0
  11. package/dist/compat/recipes.js +1 -0
  12. package/dist/compat/themes.d.ts +1 -0
  13. package/dist/compat/themes.js +1 -0
  14. package/dist/compat/tokens.d.ts +1 -0
  15. package/dist/compat/tokens.js +1 -0
  16. package/dist/components/accordion.d.ts +35 -0
  17. package/dist/components/accordion.js +95 -0
  18. package/dist/components/alert.d.ts +24 -0
  19. package/dist/components/alert.js +72 -0
  20. package/dist/components/avatar.d.ts +45 -0
  21. package/dist/components/avatar.js +97 -0
  22. package/dist/components/badge.d.ts +45 -0
  23. package/dist/components/badge.js +114 -0
  24. package/dist/components/blocker-card.d.ts +42 -0
  25. package/dist/components/blocker-card.js +45 -0
  26. package/dist/components/bottom-sheet.d.ts +31 -0
  27. package/dist/components/bottom-sheet.js +120 -0
  28. package/dist/components/breadcrumb.d.ts +28 -0
  29. package/dist/components/breadcrumb.js +52 -0
  30. package/dist/components/button.d.ts +27 -0
  31. package/dist/components/button.js +124 -0
  32. package/dist/components/card.d.ts +27 -0
  33. package/dist/components/card.js +52 -0
  34. package/dist/components/chat-bubble.d.ts +16 -0
  35. package/dist/components/chat-bubble.js +55 -0
  36. package/dist/components/checkbox.d.ts +14 -0
  37. package/dist/components/checkbox.js +78 -0
  38. package/dist/components/chip-input.d.ts +26 -0
  39. package/dist/components/chip-input.js +182 -0
  40. package/dist/components/chips.d.ts +22 -0
  41. package/dist/components/chips.js +94 -0
  42. package/dist/components/choice-card.d.ts +16 -0
  43. package/dist/components/choice-card.js +60 -0
  44. package/dist/components/choice-carousel.d.ts +16 -0
  45. package/dist/components/choice-carousel.js +154 -0
  46. package/dist/components/choice-preview-card.d.ts +37 -0
  47. package/dist/components/choice-preview-card.js +39 -0
  48. package/dist/components/color-input.d.ts +20 -0
  49. package/dist/components/color-input.js +79 -0
  50. package/dist/components/combobox.d.ts +70 -0
  51. package/dist/components/combobox.js +213 -0
  52. package/dist/components/context-menu.d.ts +33 -0
  53. package/dist/components/context-menu.js +70 -0
  54. package/dist/components/copilot-hint.d.ts +18 -0
  55. package/dist/components/copilot-hint.js +63 -0
  56. package/dist/components/copy-field.d.ts +16 -0
  57. package/dist/components/copy-field.js +41 -0
  58. package/dist/components/currency-input.d.ts +32 -0
  59. package/dist/components/currency-input.js +138 -0
  60. package/dist/components/danger-zone.d.ts +20 -0
  61. package/dist/components/danger-zone.js +21 -0
  62. package/dist/components/data-table-header.d.ts +43 -0
  63. package/dist/components/data-table-header.js +205 -0
  64. package/dist/components/date-picker.d.ts +40 -0
  65. package/dist/components/date-picker.js +252 -0
  66. package/dist/components/divider.d.ts +20 -0
  67. package/dist/components/divider.js +47 -0
  68. package/dist/components/drawer.d.ts +26 -0
  69. package/dist/components/drawer.js +82 -0
  70. package/dist/components/dropzone.d.ts +19 -0
  71. package/dist/components/dropzone.js +65 -0
  72. package/dist/components/empty-state.d.ts +48 -0
  73. package/dist/components/empty-state.js +91 -0
  74. package/dist/components/fab.d.ts +38 -0
  75. package/dist/components/fab.js +100 -0
  76. package/dist/components/filter-chip.d.ts +54 -0
  77. package/dist/components/filter-chip.js +66 -0
  78. package/dist/components/floating-stepper.d.ts +44 -0
  79. package/dist/components/floating-stepper.js +53 -0
  80. package/dist/components/form-card-header.d.ts +13 -0
  81. package/dist/components/form-card-header.js +8 -0
  82. package/dist/components/icon-button.d.ts +17 -0
  83. package/dist/components/icon-button.js +80 -0
  84. package/dist/components/image-crop-modal.d.ts +59 -0
  85. package/dist/components/image-crop-modal.js +240 -0
  86. package/dist/components/image-upload.d.ts +72 -0
  87. package/dist/components/image-upload.js +271 -0
  88. package/dist/components/index.d.ts +85 -0
  89. package/dist/components/index.js +200 -0
  90. package/dist/components/input.d.ts +25 -0
  91. package/dist/components/input.js +136 -0
  92. package/dist/components/insight-card.d.ts +19 -0
  93. package/dist/components/insight-card.js +27 -0
  94. package/dist/components/label.d.ts +16 -0
  95. package/dist/components/label.js +43 -0
  96. package/dist/components/media-tile.d.ts +24 -0
  97. package/dist/components/media-tile.js +28 -0
  98. package/dist/components/menu.d.ts +50 -0
  99. package/dist/components/menu.js +80 -0
  100. package/dist/components/modal.d.ts +25 -0
  101. package/dist/components/modal.js +71 -0
  102. package/dist/components/notice.d.ts +50 -0
  103. package/dist/components/notice.js +103 -0
  104. package/dist/components/option-tile.d.ts +60 -0
  105. package/dist/components/option-tile.js +144 -0
  106. package/dist/components/page-shell.d.ts +29 -0
  107. package/dist/components/page-shell.js +36 -0
  108. package/dist/components/page-skeleton.d.ts +35 -0
  109. package/dist/components/page-skeleton.js +12 -0
  110. package/dist/components/page-tabs.d.ts +35 -0
  111. package/dist/components/page-tabs.js +55 -0
  112. package/dist/components/pagination.d.ts +31 -0
  113. package/dist/components/pagination.js +80 -0
  114. package/dist/components/paywall-banner.d.ts +37 -0
  115. package/dist/components/paywall-banner.js +42 -0
  116. package/dist/components/paywall.d.ts +44 -0
  117. package/dist/components/paywall.js +55 -0
  118. package/dist/components/phone-frame.d.ts +9 -0
  119. package/dist/components/phone-frame.js +18 -0
  120. package/dist/components/phone-input.d.ts +24 -0
  121. package/dist/components/phone-input.js +92 -0
  122. package/dist/components/pix-icon.d.ts +8 -0
  123. package/dist/components/pix-icon.js +24 -0
  124. package/dist/components/popover.d.ts +44 -0
  125. package/dist/components/popover.js +140 -0
  126. package/dist/components/pricing-card.d.ts +42 -0
  127. package/dist/components/pricing-card.js +29 -0
  128. package/dist/components/qr-display.d.ts +19 -0
  129. package/dist/components/qr-display.js +24 -0
  130. package/dist/components/radio.d.ts +33 -0
  131. package/dist/components/radio.js +97 -0
  132. package/dist/components/row-actions.d.ts +24 -0
  133. package/dist/components/row-actions.js +76 -0
  134. package/dist/components/saving-bar-context.d.ts +40 -0
  135. package/dist/components/saving-bar-context.js +125 -0
  136. package/dist/components/saving-bar.d.ts +18 -0
  137. package/dist/components/saving-bar.js +72 -0
  138. package/dist/components/search.d.ts +30 -0
  139. package/dist/components/search.js +81 -0
  140. package/dist/components/section-card.d.ts +14 -0
  141. package/dist/components/section-card.js +49 -0
  142. package/dist/components/section-header.d.ts +29 -0
  143. package/dist/components/section-header.js +7 -0
  144. package/dist/components/select.d.ts +21 -0
  145. package/dist/components/select.js +140 -0
  146. package/dist/components/selectable-card.d.ts +35 -0
  147. package/dist/components/selectable-card.js +27 -0
  148. package/dist/components/setting-row.d.ts +33 -0
  149. package/dist/components/setting-row.js +19 -0
  150. package/dist/components/settings-list.d.ts +25 -0
  151. package/dist/components/settings-list.js +22 -0
  152. package/dist/components/skeleton.d.ts +24 -0
  153. package/dist/components/skeleton.js +49 -0
  154. package/dist/components/slider.d.ts +32 -0
  155. package/dist/components/slider.js +79 -0
  156. package/dist/components/spinner.d.ts +14 -0
  157. package/dist/components/spinner.js +36 -0
  158. package/dist/components/step-progress.d.ts +12 -0
  159. package/dist/components/step-progress.js +24 -0
  160. package/dist/components/summary-bar.d.ts +31 -0
  161. package/dist/components/summary-bar.js +54 -0
  162. package/dist/components/table.d.ts +23 -0
  163. package/dist/components/table.js +80 -0
  164. package/dist/components/textarea.d.ts +21 -0
  165. package/dist/components/textarea.js +95 -0
  166. package/dist/components/themed.d.ts +36 -0
  167. package/dist/components/themed.js +22 -0
  168. package/dist/components/time-picker.d.ts +34 -0
  169. package/dist/components/time-picker.js +213 -0
  170. package/dist/components/toaster.d.ts +84 -0
  171. package/dist/components/toaster.js +284 -0
  172. package/dist/components/toggle-card-compact.d.ts +21 -0
  173. package/dist/components/toggle-card-compact.js +99 -0
  174. package/dist/components/toggle-card.d.ts +15 -0
  175. package/dist/components/toggle-card.js +76 -0
  176. package/dist/components/toggle-group.d.ts +67 -0
  177. package/dist/components/toggle-group.js +80 -0
  178. package/dist/components/toggle.d.ts +13 -0
  179. package/dist/components/toggle.js +64 -0
  180. package/dist/components/tooltip.d.ts +17 -0
  181. package/dist/components/tooltip.js +89 -0
  182. package/dist/components/typing-indicator.d.ts +8 -0
  183. package/dist/components/typing-indicator.js +27 -0
  184. package/dist/components/use-dismiss.d.ts +9 -0
  185. package/dist/components/use-dismiss.js +28 -0
  186. package/dist/components/use-focus-trap.d.ts +5 -0
  187. package/dist/components/use-focus-trap.js +54 -0
  188. package/dist/icons/index.d.ts +1 -0
  189. package/dist/icons/index.js +89 -0
  190. package/dist/targets/native/components/accordion.d.ts +18 -0
  191. package/dist/targets/native/components/accordion.js +122 -0
  192. package/dist/targets/native/components/alert.d.ts +16 -0
  193. package/dist/targets/native/components/alert.js +58 -0
  194. package/dist/targets/native/components/avatar.d.ts +25 -0
  195. package/dist/targets/native/components/avatar.js +88 -0
  196. package/dist/targets/native/components/badge.d.ts +27 -0
  197. package/dist/targets/native/components/badge.js +59 -0
  198. package/dist/targets/native/components/button.d.ts +18 -0
  199. package/dist/targets/native/components/button.js +84 -0
  200. package/dist/targets/native/components/card.d.ts +6 -0
  201. package/dist/targets/native/components/card.js +36 -0
  202. package/dist/targets/native/components/checkbox.d.ts +12 -0
  203. package/dist/targets/native/components/checkbox.js +59 -0
  204. package/dist/targets/native/components/chips.d.ts +17 -0
  205. package/dist/targets/native/components/chips.js +77 -0
  206. package/dist/targets/native/components/choice-card.d.ts +11 -0
  207. package/dist/targets/native/components/choice-card.js +52 -0
  208. package/dist/targets/native/components/context-menu.d.ts +11 -0
  209. package/dist/targets/native/components/context-menu.js +19 -0
  210. package/dist/targets/native/components/currency-input.d.ts +22 -0
  211. package/dist/targets/native/components/currency-input.js +105 -0
  212. package/dist/targets/native/components/danger-zone.d.ts +13 -0
  213. package/dist/targets/native/components/danger-zone.js +27 -0
  214. package/dist/targets/native/components/date-picker.d.ts +13 -0
  215. package/dist/targets/native/components/date-picker.js +185 -0
  216. package/dist/targets/native/components/divider.d.ts +9 -0
  217. package/dist/targets/native/components/divider.js +36 -0
  218. package/dist/targets/native/components/empty-state.d.ts +13 -0
  219. package/dist/targets/native/components/empty-state.js +10 -0
  220. package/dist/targets/native/components/fab.d.ts +25 -0
  221. package/dist/targets/native/components/fab.js +112 -0
  222. package/dist/targets/native/components/filter-chip.d.ts +22 -0
  223. package/dist/targets/native/components/filter-chip.js +70 -0
  224. package/dist/targets/native/components/form-field.d.ts +13 -0
  225. package/dist/targets/native/components/form-field.js +29 -0
  226. package/dist/targets/native/components/form-screen.d.ts +19 -0
  227. package/dist/targets/native/components/form-screen.js +88 -0
  228. package/dist/targets/native/components/google-icon.d.ts +8 -0
  229. package/dist/targets/native/components/google-icon.js +32 -0
  230. package/dist/targets/native/components/icon-button.d.ts +18 -0
  231. package/dist/targets/native/components/icon-button.js +80 -0
  232. package/dist/targets/native/components/icon.d.ts +11 -0
  233. package/dist/targets/native/components/icon.js +9 -0
  234. package/dist/targets/native/components/image-upload.d.ts +12 -0
  235. package/dist/targets/native/components/image-upload.js +95 -0
  236. package/dist/targets/native/components/index.d.ts +45 -0
  237. package/dist/targets/native/components/index.js +100 -0
  238. package/dist/targets/native/components/input.d.ts +17 -0
  239. package/dist/targets/native/components/input.js +103 -0
  240. package/dist/targets/native/components/list-item.d.ts +20 -0
  241. package/dist/targets/native/components/list-item.js +71 -0
  242. package/dist/targets/native/components/menu.d.ts +24 -0
  243. package/dist/targets/native/components/menu.js +71 -0
  244. package/dist/targets/native/components/modal.d.ts +15 -0
  245. package/dist/targets/native/components/modal.js +110 -0
  246. package/dist/targets/native/components/paywall-banner.d.ts +11 -0
  247. package/dist/targets/native/components/paywall-banner.js +46 -0
  248. package/dist/targets/native/components/paywall.d.ts +18 -0
  249. package/dist/targets/native/components/paywall.js +61 -0
  250. package/dist/targets/native/components/radio.d.ts +20 -0
  251. package/dist/targets/native/components/radio.js +71 -0
  252. package/dist/targets/native/components/search.d.ts +22 -0
  253. package/dist/targets/native/components/search.js +92 -0
  254. package/dist/targets/native/components/select.d.ts +17 -0
  255. package/dist/targets/native/components/select.js +86 -0
  256. package/dist/targets/native/components/sheet.d.ts +11 -0
  257. package/dist/targets/native/components/sheet.js +51 -0
  258. package/dist/targets/native/components/skeleton.d.ts +18 -0
  259. package/dist/targets/native/components/skeleton.js +52 -0
  260. package/dist/targets/native/components/slider.d.ts +18 -0
  261. package/dist/targets/native/components/slider.js +153 -0
  262. package/dist/targets/native/components/spinner.d.ts +7 -0
  263. package/dist/targets/native/components/spinner.js +10 -0
  264. package/dist/targets/native/components/stepper.d.ts +10 -0
  265. package/dist/targets/native/components/stepper.js +33 -0
  266. package/dist/targets/native/components/switch.d.ts +20 -0
  267. package/dist/targets/native/components/switch.js +68 -0
  268. package/dist/targets/native/components/text.d.ts +12 -0
  269. package/dist/targets/native/components/text.js +31 -0
  270. package/dist/targets/native/components/textarea.d.ts +13 -0
  271. package/dist/targets/native/components/textarea.js +92 -0
  272. package/dist/targets/native/components/theme.d.ts +18 -0
  273. package/dist/targets/native/components/theme.js +20 -0
  274. package/dist/targets/native/components/toaster.d.ts +35 -0
  275. package/dist/targets/native/components/toaster.js +159 -0
  276. package/dist/targets/native/fonts.d.ts +9 -0
  277. package/dist/targets/native/fonts.js +10 -0
  278. package/dist/targets/native/icons.d.ts +1 -0
  279. package/dist/targets/native/icons.js +1 -0
  280. package/dist/targets/native/index.d.ts +3 -0
  281. package/dist/targets/native/index.js +11 -0
  282. package/dist/targets/native/preset.cjs +69 -0
  283. package/dist/targets/native/resolve.d.ts +22 -0
  284. package/dist/targets/native/resolve.js +85 -0
  285. package/dist/targets/native/theme.css +118 -0
  286. package/dist/targets/native/theme.d.ts +117 -0
  287. package/dist/targets/native/theme.js +118 -0
  288. package/package.json +194 -0
  289. package/styles/_primitives.generated.css +284 -0
  290. package/styles/base.css +950 -0
  291. package/styles/themes/rojao.css +107 -0
@@ -0,0 +1,80 @@
1
+ import {
2
+ __objRest,
3
+ __spreadValues
4
+ } from "../chunk-ORMEWXMH.js";
5
+ import { forwardRef } from "react";
6
+ const SIZE_CLASSES = {
7
+ sm: "size-control-sm [&_svg]:size-4",
8
+ md: "size-control-md [&_svg]:size-5",
9
+ lg: "size-control-lg [&_svg]:size-6"
10
+ };
11
+ const COLOR_CLASSES = {
12
+ filled: {
13
+ primary: "bg-brand-primary text-brand-on-primary hover:bg-brand-hover",
14
+ secondary: "bg-surface-raised text-fg-primary hover:opacity-90",
15
+ danger: "bg-danger text-fg-inverse hover:bg-danger-text",
16
+ neutral: "bg-surface-raised text-fg-primary hover:bg-stroke-default"
17
+ },
18
+ outline: {
19
+ primary: "border border-brand-primary text-brand-primary hover:bg-surface-raised",
20
+ secondary: "border border-stroke-default text-fg-secondary hover:bg-surface-raised",
21
+ danger: "border border-danger text-danger-text hover:bg-danger-soft",
22
+ neutral: "border border-stroke-default text-fg-secondary hover:bg-surface-raised"
23
+ },
24
+ ghost: {
25
+ primary: "text-brand-primary hover:bg-surface-raised",
26
+ secondary: "text-fg-secondary hover:bg-surface-raised",
27
+ danger: "text-danger-text hover:bg-danger-soft",
28
+ neutral: "text-fg-muted hover:bg-surface-raised hover:text-fg-primary"
29
+ }
30
+ };
31
+ const IconButton = forwardRef(function IconButton2(_a, ref) {
32
+ var _b = _a, {
33
+ icon,
34
+ variant = "ghost",
35
+ color = "neutral",
36
+ size = "md",
37
+ loading = false,
38
+ disabled,
39
+ className = ""
40
+ } = _b, rest = __objRest(_b, [
41
+ "icon",
42
+ "variant",
43
+ "color",
44
+ "size",
45
+ "loading",
46
+ "disabled",
47
+ "className"
48
+ ]);
49
+ return /* @__PURE__ */ React.createElement(
50
+ "button",
51
+ __spreadValues({
52
+ ref,
53
+ type: "button",
54
+ disabled: disabled || loading,
55
+ className: [
56
+ "inline-flex items-center justify-center rounded-(--radius-control) transition-colors",
57
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-stroke-focus",
58
+ "disabled:cursor-not-allowed disabled:opacity-50",
59
+ SIZE_CLASSES[size],
60
+ COLOR_CLASSES[variant][color],
61
+ className
62
+ ].join(" ")
63
+ }, rest),
64
+ loading ? /* @__PURE__ */ React.createElement(Spinner, null) : icon
65
+ );
66
+ });
67
+ function Spinner() {
68
+ return /* @__PURE__ */ React.createElement("svg", { className: "animate-spin", viewBox: "0 0 24 24", fill: "none" }, /* @__PURE__ */ React.createElement("circle", { cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "2.5", opacity: "0.25" }), /* @__PURE__ */ React.createElement(
69
+ "path",
70
+ {
71
+ d: "M22 12a10 10 0 0 1-10 10",
72
+ stroke: "currentColor",
73
+ strokeWidth: "2.5",
74
+ strokeLinecap: "round"
75
+ }
76
+ ));
77
+ }
78
+ export {
79
+ IconButton
80
+ };
@@ -0,0 +1,59 @@
1
+ import * as react from 'react';
2
+
3
+ /**
4
+ * ImageCropModal — modal de crop de imagem com presets de aspect ratio.
5
+ *
6
+ * Requer `react-image-crop` instalado no consumer (peerDep).
7
+ *
8
+ * Uso:
9
+ * <ImageCropModal
10
+ * open={open}
11
+ * file={selectedFile}
12
+ * onCancel={() => setOpen(false)}
13
+ * onConfirm={(blob) => uploadCroppedImage(blob)}
14
+ * />
15
+ *
16
+ * Presets default: Livre (1:2 a 2:1), 1:1, 4:5, 16:9.
17
+ * Customize via prop `presets`.
18
+ */
19
+ interface CropPreset {
20
+ id: string;
21
+ label: string;
22
+ /** Aspect ratio fixo. undefined = livre (com bounds MIN/MAX_ASPECT). */
23
+ aspect?: number;
24
+ /** Ícone opcional exibido antes do rótulo (ajuda quem não decodifica "16:9"). */
25
+ icon?: React.ReactNode;
26
+ }
27
+ interface ImageCropModalProps {
28
+ open: boolean;
29
+ file: File | null;
30
+ /** Label de contexto pro título (ex: "Foto 2 de 4"). */
31
+ position?: string;
32
+ onCancel: () => void;
33
+ /**
34
+ * Recebe o blob do recorte. Pode ser assíncrono (ex.: upload) — o modal AGUARDA a promise
35
+ * e mantém o botão travado/carregando até resolver, evitando duplo-clique que duplicaria a
36
+ * imagem (o upload roda fora do `busy` se o modal não esperar).
37
+ */
38
+ onConfirm: (blob: Blob) => void | Promise<void>;
39
+ /** Presets disponíveis. default = Livre + 1:1 + 4:5 + 16:9. */
40
+ presets?: CropPreset[];
41
+ /** Limite mínimo de aspect ratio (alto-baixo). default = 0.5 (1:2). */
42
+ minAspect?: number;
43
+ /** Limite máximo de aspect ratio (largo-baixo). default = 2.0 (2:1). */
44
+ maxAspect?: number;
45
+ /** Título do modal. default = "Ajustar foto". */
46
+ title?: string;
47
+ /** Label do botão confirmar. default = "Aplicar e enviar". */
48
+ confirmLabel?: string;
49
+ /**
50
+ * Teto de tamanho do blob de saída (bytes). Quando definido, a saída do crop
51
+ * é forçada a JPEG e comprimida (qualidade + downscale) até caber no limite —
52
+ * evita que foto de celular grande estoure o limite de upload. Sem ele, a
53
+ * saída mantém o tipo original e qualidade fixa (comportamento inalterado).
54
+ */
55
+ maxSizeBytes?: number;
56
+ }
57
+ declare function ImageCropModal({ open, file, position, onCancel, onConfirm, presets, minAspect, maxAspect, title, confirmLabel, maxSizeBytes, }: ImageCropModalProps): react.JSX.Element;
58
+
59
+ export { type CropPreset, ImageCropModal, type ImageCropModalProps };
@@ -0,0 +1,240 @@
1
+ "use client";
2
+ import "../chunk-ORMEWXMH.js";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import ReactCrop, { centerCrop } from "react-image-crop";
5
+ import "react-image-crop/dist/ReactCrop.css";
6
+ import { Modal } from "./modal.js";
7
+ import { Spinner } from "./spinner.js";
8
+ import { Button } from "./button.js";
9
+ function RatioIcon({ w, h }) {
10
+ const box = 16;
11
+ const rw = w >= h ? box : w / h * box;
12
+ const rh = h >= w ? box : h / w * box;
13
+ return /* @__PURE__ */ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 18 18", fill: "none", "aria-hidden": true, className: "shrink-0" }, /* @__PURE__ */ React.createElement("rect", { x: (18 - rw) / 2, y: (18 - rh) / 2, width: rw, height: rh, rx: "1.5", stroke: "currentColor", strokeWidth: "1.6" }));
14
+ }
15
+ function FreeIcon() {
16
+ return /* @__PURE__ */ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 18 18", fill: "none", "aria-hidden": true, className: "shrink-0" }, /* @__PURE__ */ React.createElement("rect", { x: "2.5", y: "2.5", width: "13", height: "13", rx: "1.5", stroke: "currentColor", strokeWidth: "1.6", strokeDasharray: "2.5 2.2" }));
17
+ }
18
+ const DEFAULT_PRESETS = [
19
+ { id: "square", label: "1:1", aspect: 1, icon: /* @__PURE__ */ React.createElement(RatioIcon, { w: 1, h: 1 }) },
20
+ { id: "portrait", label: "4:5", aspect: 4 / 5, icon: /* @__PURE__ */ React.createElement(RatioIcon, { w: 4, h: 5 }) },
21
+ { id: "landscape", label: "16:9", aspect: 16 / 9, icon: /* @__PURE__ */ React.createElement(RatioIcon, { w: 16, h: 9 }) },
22
+ { id: "free", label: "Livre", icon: /* @__PURE__ */ React.createElement(FreeIcon, null) }
23
+ ];
24
+ const DEFAULT_MIN_ASPECT = 0.5;
25
+ const DEFAULT_MAX_ASPECT = 2;
26
+ function buildInitialCrop(width, height, aspect) {
27
+ if (!aspect) {
28
+ return { unit: "%", x: 0, y: 0, width: 100, height: 100 };
29
+ }
30
+ const imageAspect = width / height;
31
+ let cropW;
32
+ let cropH;
33
+ if (aspect >= imageAspect) {
34
+ cropW = 100;
35
+ cropH = imageAspect / aspect * 100;
36
+ } else {
37
+ cropH = 100;
38
+ cropW = aspect / imageAspect * 100;
39
+ }
40
+ return centerCrop({ unit: "%", x: 0, y: 0, width: cropW, height: cropH }, width, height);
41
+ }
42
+ const QUALITY_STEPS = [0.85, 0.75, 0.65, 0.55];
43
+ const MAX_SIDE_STEPS = [1600, 1200];
44
+ function canvasToBlob(canvas, mimeType, quality) {
45
+ return new Promise((resolve, reject) => {
46
+ canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error("toBlob failed")), mimeType, quality);
47
+ });
48
+ }
49
+ function drawCrop(image, crop, maxSide) {
50
+ const canvas = document.createElement("canvas");
51
+ const scaleX = image.naturalWidth / image.width;
52
+ const scaleY = image.naturalHeight / image.height;
53
+ let outW = Math.round(crop.width * scaleX);
54
+ let outH = Math.round(crop.height * scaleY);
55
+ if (maxSide && Math.max(outW, outH) > maxSide) {
56
+ const ratio = maxSide / Math.max(outW, outH);
57
+ outW = Math.round(outW * ratio);
58
+ outH = Math.round(outH * ratio);
59
+ }
60
+ canvas.width = outW;
61
+ canvas.height = outH;
62
+ const ctx = canvas.getContext("2d");
63
+ if (!ctx) throw new Error("Canvas not supported");
64
+ ctx.drawImage(
65
+ image,
66
+ crop.x * scaleX,
67
+ crop.y * scaleY,
68
+ crop.width * scaleX,
69
+ crop.height * scaleY,
70
+ 0,
71
+ 0,
72
+ outW,
73
+ outH
74
+ );
75
+ return canvas;
76
+ }
77
+ async function cropToBlob(image, crop, mimeType, maxSizeBytes) {
78
+ const baseCanvas = drawCrop(image, crop);
79
+ if (!maxSizeBytes) {
80
+ return canvasToBlob(baseCanvas, mimeType, 0.92);
81
+ }
82
+ const out = await canvasToBlob(baseCanvas, "image/jpeg", 0.92);
83
+ if (out.size <= maxSizeBytes) return out;
84
+ let best = out;
85
+ for (const q of QUALITY_STEPS) {
86
+ const candidate = await canvasToBlob(baseCanvas, "image/jpeg", q);
87
+ best = candidate;
88
+ if (candidate.size <= maxSizeBytes) return candidate;
89
+ }
90
+ for (const side of MAX_SIDE_STEPS) {
91
+ const scaledCanvas = drawCrop(image, crop, side);
92
+ for (const q of QUALITY_STEPS) {
93
+ const candidate = await canvasToBlob(scaledCanvas, "image/jpeg", q);
94
+ best = candidate;
95
+ if (candidate.size <= maxSizeBytes) return candidate;
96
+ }
97
+ }
98
+ return best;
99
+ }
100
+ function ImageCropModal({
101
+ open,
102
+ file,
103
+ position,
104
+ onCancel,
105
+ onConfirm,
106
+ presets = DEFAULT_PRESETS,
107
+ minAspect = DEFAULT_MIN_ASPECT,
108
+ maxAspect = DEFAULT_MAX_ASPECT,
109
+ title = "Ajustar foto",
110
+ confirmLabel = "Aplicar e enviar",
111
+ maxSizeBytes
112
+ }) {
113
+ const [src, setSrc] = useState(null);
114
+ const [preset, setPreset] = useState(presets[0]);
115
+ const [crop, setCrop] = useState();
116
+ const [completedCrop, setCompletedCrop] = useState(null);
117
+ const [aspectError, setAspectError] = useState(null);
118
+ const [busy, setBusy] = useState(false);
119
+ const [imgLoaded, setImgLoaded] = useState(false);
120
+ const imgRef = useRef(null);
121
+ useEffect(() => {
122
+ if (!file) {
123
+ setSrc(null);
124
+ setImgLoaded(false);
125
+ return;
126
+ }
127
+ const url = URL.createObjectURL(file);
128
+ setSrc(url);
129
+ setPreset(presets[0]);
130
+ setCrop(void 0);
131
+ setCompletedCrop(null);
132
+ setAspectError(null);
133
+ setImgLoaded(false);
134
+ return () => URL.revokeObjectURL(url);
135
+ }, [file, presets]);
136
+ function handleImageLoad(e) {
137
+ var _a, _b, _c, _d;
138
+ const { width, height } = e.currentTarget;
139
+ const initial = buildInitialCrop(width, height, preset.aspect);
140
+ setCrop(initial);
141
+ const pixelInitial = {
142
+ unit: "px",
143
+ x: ((_a = initial.x) != null ? _a : 0) / 100 * width,
144
+ y: ((_b = initial.y) != null ? _b : 0) / 100 * height,
145
+ width: ((_c = initial.width) != null ? _c : 0) / 100 * width,
146
+ height: ((_d = initial.height) != null ? _d : 0) / 100 * height
147
+ };
148
+ setCompletedCrop(pixelInitial);
149
+ setImgLoaded(true);
150
+ }
151
+ function applyPreset(p) {
152
+ setPreset(p);
153
+ setAspectError(null);
154
+ if (imgRef.current) {
155
+ const { width, height } = imgRef.current;
156
+ setCrop(buildInitialCrop(width, height, p.aspect));
157
+ }
158
+ }
159
+ function validateFreeAspect(c) {
160
+ if (preset.aspect) return true;
161
+ if (!c.width || !c.height) return false;
162
+ const ratio = c.width / c.height;
163
+ if (ratio < minAspect) {
164
+ setAspectError(`Recorte muito alto. M\xEDnimo permitido: ${minAspect.toFixed(1)}.`);
165
+ return false;
166
+ }
167
+ if (ratio > maxAspect) {
168
+ setAspectError(`Recorte muito largo. M\xE1ximo permitido: ${maxAspect.toFixed(1)}.`);
169
+ return false;
170
+ }
171
+ setAspectError(null);
172
+ return true;
173
+ }
174
+ async function handleConfirm() {
175
+ if (busy) return;
176
+ if (!completedCrop || !imgRef.current || !file) return;
177
+ if (!validateFreeAspect(completedCrop)) return;
178
+ setBusy(true);
179
+ try {
180
+ const blob = await cropToBlob(imgRef.current, completedCrop, file.type || "image/jpeg", maxSizeBytes);
181
+ await onConfirm(blob);
182
+ } catch (err) {
183
+ console.error("cropToBlob error", err);
184
+ } finally {
185
+ setBusy(false);
186
+ }
187
+ }
188
+ return /* @__PURE__ */ React.createElement(
189
+ Modal,
190
+ {
191
+ open,
192
+ onClose: onCancel,
193
+ size: "lg",
194
+ title: position ? `${title} \xB7 ${position}` : title,
195
+ footer: /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Button, { variant: "ghost", onClick: onCancel }, "Cancelar"), /* @__PURE__ */ React.createElement(Button, { onClick: handleConfirm, disabled: busy || !completedCrop || !!aspectError, loading: busy }, busy ? "Aplicando\u2026" : confirmLabel))
196
+ },
197
+ /* @__PURE__ */ React.createElement("div", { className: "space-y-3" }, presets.length > 1 && /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap gap-1 rounded-(--radius-control) bg-surface-raised p-1" }, presets.map((p) => /* @__PURE__ */ React.createElement(
198
+ "button",
199
+ {
200
+ key: p.id,
201
+ type: "button",
202
+ onClick: () => applyPreset(p),
203
+ className: [
204
+ "flex flex-1 min-w-[80px] items-center justify-center gap-1.5 rounded-(--radius-control) px-3 py-1.5 text-caption font-medium transition-colors",
205
+ preset.id === p.id ? "bg-surface-default text-fg-primary shadow-sm" : "text-fg-muted hover:text-fg-primary"
206
+ ].join(" ")
207
+ },
208
+ p.icon,
209
+ p.label
210
+ ))), /* @__PURE__ */ React.createElement("div", { className: "relative rounded-(--radius-control) bg-surface-overlay overflow-hidden flex items-center justify-center min-h-[300px] max-h-[60vh] p-4" }, src && /* @__PURE__ */ React.createElement(
211
+ ReactCrop,
212
+ {
213
+ crop,
214
+ onChange: (_, percentCrop) => setCrop(percentCrop),
215
+ onComplete: (c) => {
216
+ setCompletedCrop(c);
217
+ validateFreeAspect(c);
218
+ },
219
+ aspect: preset.aspect,
220
+ keepSelection: true,
221
+ minWidth: 50,
222
+ minHeight: 50,
223
+ className: "max-h-[60vh] max-w-full"
224
+ },
225
+ /* @__PURE__ */ React.createElement(
226
+ "img",
227
+ {
228
+ ref: imgRef,
229
+ src,
230
+ alt: "",
231
+ onLoad: handleImageLoad,
232
+ className: "max-h-full max-w-full"
233
+ }
234
+ )
235
+ ), !imgLoaded && /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 flex items-center justify-center pointer-events-none" }, /* @__PURE__ */ React.createElement(Spinner, { size: "lg", color: "inverse" })), busy && /* @__PURE__ */ React.createElement("div", { className: "absolute inset-0 z-10 flex items-center justify-center bg-black/40" }, /* @__PURE__ */ React.createElement(Spinner, { size: "lg", color: "inverse" }))), aspectError && /* @__PURE__ */ React.createElement("p", { className: "rounded-(--radius-control) bg-danger-soft px-3 py-2 text-caption text-danger-text" }, aspectError), !aspectError && preset.id === "free" && /* @__PURE__ */ React.createElement("p", { className: "text-caption text-fg-muted" }, "Livre: propor\xE7\xE3o entre ", minAspect.toFixed(1), " (vertical) e ", maxAspect.toFixed(1), " (horizontal)."))
236
+ );
237
+ }
238
+ export {
239
+ ImageCropModal
240
+ };
@@ -0,0 +1,72 @@
1
+ import * as react from 'react';
2
+ import { CropPreset } from './image-crop-modal.js';
3
+
4
+ /**
5
+ * ImageUpload — upload com preview e remoção.
6
+ *
7
+ * variant="button" → botão + preview simples (inline, form denso)
8
+ * variant="dropzone" → zona de drop grande com drag & drop (upload é o herói: banner, galeria)
9
+ * variant="tile" → tile COMPACTO auto-contido (ativo único e pequeno: logo, avatar). O
10
+ * próprio tile é o alvo de clique/drop; quando preenchido, vira o preview
11
+ * com botões-ícone de trocar/remover no canto (sem fileira de controles
12
+ * separada, sem largura full). Todos os estados no mesmo footprint.
13
+ *
14
+ * Textos são i18n-áveis via `labels` (defaults pt-BR preservam o comportamento atual).
15
+ *
16
+ * Uso:
17
+ * <ImageUpload name="logo" value={url} onChange={setUrl}
18
+ * onUpload={async (file) => { ... return url; }} variant="tile" aspect="logo" />
19
+ */
20
+ type ImageUploadAspect = 'logo' | 'banner' | 'square';
21
+ type ImageUploadVariant = 'button' | 'dropzone' | 'tile';
22
+ interface ImageUploadLabels {
23
+ /** Botão/prompt do estado vazio (button/dropzone/tile). */
24
+ select: string;
25
+ /** Ação de trocar a imagem (botão + aria-label do tile). */
26
+ change: string;
27
+ /** Ação de remover. */
28
+ remove: string;
29
+ /** Texto durante o upload. */
30
+ uploading: string;
31
+ /** Título da dropzone vazia. */
32
+ dropTitle: string;
33
+ /** Subtítulo da dropzone vazia. */
34
+ dropHint: string;
35
+ /** aria-label da área de upload. */
36
+ aria: string;
37
+ /** Mensagem de erro genérica de upload. */
38
+ error: string;
39
+ }
40
+ /** Config de crop: quando presente, a imagem selecionada passa pelo ImageCropModal antes do upload. */
41
+ interface ImageUploadCrop {
42
+ /** Presets de proporção. 1 só preset (ex.: 'free') = recorte livre, sem barra de presets. */
43
+ presets?: CropPreset[];
44
+ minAspect?: number;
45
+ maxAspect?: number;
46
+ maxSizeBytes?: number;
47
+ title?: string;
48
+ confirmLabel?: string;
49
+ }
50
+ interface ImageUploadProps {
51
+ /** Sobrescreve o tamanho/estilo do preview (dropzone/tile). */
52
+ previewClassName?: string;
53
+ /** Classe do wrapper do preview (ex.: simular o fundo claro/escuro da vitrine). */
54
+ previewWrapperClassName?: string;
55
+ name: string;
56
+ value: string;
57
+ onChange: (url: string) => void;
58
+ onUpload: (file: File) => Promise<string>;
59
+ label?: string;
60
+ hint?: string;
61
+ aspect?: ImageUploadAspect;
62
+ accept?: string;
63
+ className?: string;
64
+ variant?: ImageUploadVariant;
65
+ /** Quando definido, abre o crop (ImageCropModal) entre selecionar o arquivo e subir. */
66
+ crop?: ImageUploadCrop;
67
+ /** Textos exibidos — mescla com os defaults pt-BR (passe só o que quiser sobrescrever). */
68
+ labels?: Partial<ImageUploadLabels>;
69
+ }
70
+ declare function ImageUpload({ name, value, onChange, onUpload, label, hint, aspect, accept, className, variant, previewClassName, previewWrapperClassName, crop, labels, }: ImageUploadProps): react.JSX.Element;
71
+
72
+ export { ImageUpload, type ImageUploadAspect, type ImageUploadCrop, type ImageUploadLabels, type ImageUploadProps, type ImageUploadVariant };