@rojaostudio/ds 2.0.0-next.3 → 2.0.0-next.30

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 (109) hide show
  1. package/README.md +42 -2
  2. package/THIRD_PARTY_NOTICES.md +66 -0
  3. package/dist/components/avatar.d.ts +20 -12
  4. package/dist/components/avatar.js +8 -6
  5. package/dist/components/badge.d.ts +7 -6
  6. package/dist/components/badge.js +2 -1
  7. package/dist/components/breadcrumb.d.ts +4 -4
  8. package/dist/components/breadcrumb.js +1 -1
  9. package/dist/components/bubble.d.ts +26 -11
  10. package/dist/components/bubble.js +3 -3
  11. package/dist/components/button.d.ts +1 -1
  12. package/dist/components/card.d.ts +28 -12
  13. package/dist/components/card.js +4 -2
  14. package/dist/components/chart.d.ts +18 -8
  15. package/dist/components/chart.js +89 -25
  16. package/dist/components/choice-carousel.d.ts +1 -1
  17. package/dist/components/choice-carousel.js +2 -4
  18. package/dist/components/choice-list.d.ts +40 -0
  19. package/dist/components/choice-list.js +122 -0
  20. package/dist/components/choice-preview-card.d.ts +1 -7
  21. package/dist/components/color-input.d.ts +1 -1
  22. package/dist/components/context-menu.d.ts +5 -2
  23. package/dist/components/currency-input.d.ts +0 -2
  24. package/dist/components/data-table-header.d.ts +55 -18
  25. package/dist/components/data-table-header.js +148 -101
  26. package/dist/components/date-picker.d.ts +1 -1
  27. package/dist/components/dialog.d.ts +6 -3
  28. package/dist/components/dialog.js +2 -2
  29. package/dist/components/dropdown-menu.d.ts +5 -2
  30. package/dist/components/dropzone.d.ts +2 -2
  31. package/dist/components/dropzone.js +1 -1
  32. package/dist/components/file-input.d.ts +13 -8
  33. package/dist/components/file-input.js +24 -21
  34. package/dist/components/filter-chip.d.ts +11 -5
  35. package/dist/components/filter-chip.js +15 -6
  36. package/dist/components/floating-stepper.d.ts +11 -28
  37. package/dist/components/floating-stepper.js +2 -30
  38. package/dist/components/form-actions.d.ts +37 -15
  39. package/dist/components/form-actions.js +5 -14
  40. package/dist/components/heading.d.ts +42 -0
  41. package/dist/components/heading.js +25 -0
  42. package/dist/components/icon-button.d.ts +1 -1
  43. package/dist/components/image-crop-dialog.d.ts +60 -0
  44. package/dist/components/image-crop-dialog.js +240 -0
  45. package/dist/components/image-crop-modal.d.ts +12 -55
  46. package/dist/components/image-crop-modal.js +2 -249
  47. package/dist/components/image-upload.d.ts +8 -17
  48. package/dist/components/image-upload.js +3 -3
  49. package/dist/components/index.d.ts +15 -10
  50. package/dist/components/index.js +10 -0
  51. package/dist/components/internal/bottom-bar.d.ts +30 -0
  52. package/dist/components/internal/bottom-bar.js +25 -0
  53. package/dist/components/internal/button.d.ts +1 -1
  54. package/dist/components/internal/icons.d.ts +9 -1
  55. package/dist/components/internal/icons.js +16 -0
  56. package/dist/components/internal/menu.d.ts +3 -2
  57. package/dist/components/internal/menu.js +1 -1
  58. package/dist/components/internal/modal.js +9 -0
  59. package/dist/components/internal/toggle.d.ts +1 -1
  60. package/dist/components/internal/use-on-screen-keyboard.d.ts +12 -0
  61. package/dist/components/internal/use-on-screen-keyboard.js +49 -0
  62. package/dist/components/item.d.ts +14 -7
  63. package/dist/components/item.js +5 -2
  64. package/dist/components/loading-overlay.d.ts +43 -0
  65. package/dist/components/loading-overlay.js +81 -0
  66. package/dist/components/marker.d.ts +11 -3
  67. package/dist/components/marker.js +4 -3
  68. package/dist/components/number-input.d.ts +1 -1
  69. package/dist/components/progress.d.ts +3 -3
  70. package/dist/components/progress.js +2 -2
  71. package/dist/components/row-actions.d.ts +3 -0
  72. package/dist/components/row-actions.js +8 -4
  73. package/dist/components/saving-bar.d.ts +36 -10
  74. package/dist/components/saving-bar.js +14 -6
  75. package/dist/components/section-header.d.ts +4 -3
  76. package/dist/components/selectable-card.d.ts +2 -10
  77. package/dist/components/sidebar.d.ts +8 -11
  78. package/dist/components/sidebar.js +4 -5
  79. package/dist/components/spinner.d.ts +9 -6
  80. package/dist/components/spinner.js +3 -3
  81. package/dist/components/star-rating.d.ts +3 -3
  82. package/dist/components/star-rating.js +2 -2
  83. package/dist/components/stat.d.ts +18 -6
  84. package/dist/components/stat.js +9 -3
  85. package/dist/components/status.d.ts +4 -4
  86. package/dist/components/status.js +2 -2
  87. package/dist/components/stepper.d.ts +33 -0
  88. package/dist/components/stepper.js +36 -0
  89. package/dist/components/summary-bar.d.ts +3 -2
  90. package/dist/components/summary-bar.js +10 -1
  91. package/dist/components/tile.d.ts +5 -5
  92. package/dist/components/tile.js +2 -2
  93. package/dist/components/time-picker.d.ts +1 -1
  94. package/dist/components/toast.d.ts +17 -6
  95. package/dist/components/toast.js +5 -3
  96. package/dist/components/toggle-card-compact.d.ts +6 -25
  97. package/dist/components/toggle-card-compact.js +8 -35
  98. package/dist/components/toggle-card.d.ts +23 -9
  99. package/dist/components/toggle-card.js +38 -2
  100. package/dist/components/toggle.d.ts +1 -1
  101. package/dist/components/typing-indicator.d.ts +2 -2
  102. package/dist/components/typing-indicator.js +1 -1
  103. package/package.json +4 -4
  104. package/styles/base.css +13 -1
  105. package/styles/rds/components-styles.css +767 -281
  106. package/styles/rds/components.css +56 -31
  107. package/styles/rds/foundation.css +1 -1
  108. package/styles/rds/theme.css +40 -29
  109. package/styles/rds.css +1 -1
@@ -13,8 +13,11 @@ function FilterChip(_a) {
13
13
  children,
14
14
  count,
15
15
  icon,
16
+ pressed: pressedProp,
17
+ defaultPressed,
18
+ onPressedChange,
16
19
  active,
17
- defaultActive = false,
20
+ defaultActive,
18
21
  onActiveChange,
19
22
  disabled,
20
23
  asChild,
@@ -25,6 +28,9 @@ function FilterChip(_a) {
25
28
  "children",
26
29
  "count",
27
30
  "icon",
31
+ "pressed",
32
+ "defaultPressed",
33
+ "onPressedChange",
28
34
  "active",
29
35
  "defaultActive",
30
36
  "onActiveChange",
@@ -34,20 +40,23 @@ function FilterChip(_a) {
34
40
  "className",
35
41
  "onClick"
36
42
  ]);
37
- const [own, setOwn] = useState(defaultActive);
38
- const on = active != null ? active : own;
43
+ var _a2;
44
+ const pressed = pressedProp != null ? pressedProp : active;
45
+ const [own, setOwn] = useState((_a2 = defaultPressed != null ? defaultPressed : defaultActive) != null ? _a2 : false);
46
+ const on = pressed != null ? pressed : own;
39
47
  const Root = asChild ? Slot : "button";
40
48
  function click(event) {
49
+ var _a3;
41
50
  onClick == null ? void 0 : onClick(event);
42
51
  if (event.defaultPrevented || asChild) return;
43
- if (active === void 0) setOwn(!on);
44
- onActiveChange == null ? void 0 : onActiveChange(!on);
52
+ if (pressed === void 0) setOwn(!on);
53
+ (_a3 = onPressedChange != null ? onPressedChange : onActiveChange) == null ? void 0 : _a3(!on);
45
54
  }
46
55
  return /* @__PURE__ */ jsxs(
47
56
  Root,
48
57
  __spreadProps(__spreadValues({}, rest), {
49
58
  type: asChild ? void 0 : type,
50
- className: ["rds-filter-chip", on && "rds-filter-chip--active", className].filter(Boolean).join(" "),
59
+ className: ["rds-filter-chip", on && "rds-filter-chip--pressed", className].filter(Boolean).join(" "),
51
60
  "aria-pressed": asChild ? void 0 : on,
52
61
  "aria-current": asChild && on ? "true" : void 0,
53
62
  "aria-disabled": disabled || void 0,
@@ -1,33 +1,16 @@
1
- import * as react from 'react';
2
- import { ReactNode } from 'react';
1
+ import { Stepper, StepperProps, StepperStep } from './stepper.js';
2
+ import 'react';
3
3
 
4
4
  /**
5
- * FloatingStepper — a composition of the Card (as a pill) and Buttons (Figma [RDS] Actions/Button, neutral): the
6
- * floating steps of a flow (point of sale, checkout). The current step is the filled Button (aria-current="step");
7
- * earlier steps go back, later ones are disabled; `canNavigate` changes the rule. `action` is a last Button after
8
- * a line (Reset). Where it floats is the caller's (className or a wrapper). Styles: floating-stepper.css.
5
+ * floating-stepper — the 1.x and 2.0-next name of the Stepper, kept so `@rojaostudio/ds/components/floating-stepper`
6
+ * and `FloatingStepper` still work in 2.0. The codemod (`npx @rojaostudio/ds-codemod`) renames both.
9
7
  */
10
- type FloatingStepperStep = {
11
- key: string;
12
- label: string;
13
- };
14
- interface FloatingStepperProps {
15
- steps: FloatingStepperStep[];
16
- /** key da etapa atual */
17
- current: string;
18
- onNavigate?: (key: string) => void;
19
- /** Regra de navegação por etapa. Default: só etapas anteriores à atual. */
20
- canNavigate?: (key: string, index: number, currentIndex: number) => boolean;
21
- /** Ação à direita, separada por divisor (ex.: Reset). */
22
- action?: {
23
- label: string;
24
- icon?: ReactNode;
25
- onClick: () => void;
26
- };
27
- className?: string;
28
- /** The navigation's name. Default "Etapas". */
29
- 'aria-label'?: string;
30
- }
31
- declare function FloatingStepper({ steps, current, onNavigate, canNavigate, action, className, 'aria-label': ariaLabel }: FloatingStepperProps): react.JSX.Element;
8
+
9
+ /** @deprecated Renamed to `Stepper` (`@rojaostudio/ds/components/stepper`) in 2.0. Same props. */
10
+ declare const FloatingStepper: typeof Stepper;
11
+ /** @deprecated Renamed to `StepperProps` in 2.0. */
12
+ type FloatingStepperProps = StepperProps;
13
+ /** @deprecated Renamed to `StepperStep` in 2.0. */
14
+ type FloatingStepperStep = StepperStep;
32
15
 
33
16
  export { FloatingStepper, type FloatingStepperProps, type FloatingStepperStep };
@@ -1,35 +1,7 @@
1
1
  "use client";
2
2
  import "../chunk-ORMEWXMH.js";
3
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
- import { Button } from "./button.js";
5
- import { Card } from "./card.js";
6
- function FloatingStepper({ steps, current, onNavigate, canNavigate, action, className, "aria-label": ariaLabel = "Etapas" }) {
7
- const currentIdx = steps.findIndex((s) => s.key === current);
8
- return /* @__PURE__ */ jsxs(Card, { as: "div", size: "sm", role: "navigation", "aria-label": ariaLabel, className: ["rds-floating-stepper", className].filter(Boolean).join(" "), children: [
9
- steps.map((s, idx) => {
10
- const isCurrent = idx === currentIdx;
11
- const canGo = canNavigate ? canNavigate(s.key, idx, currentIdx) : idx < currentIdx;
12
- return /* @__PURE__ */ jsx(
13
- Button,
14
- {
15
- tone: "neutral",
16
- variant: isCurrent ? "fill" : "ghost",
17
- disabled: !canGo && !isCurrent,
18
- "aria-current": isCurrent ? "step" : void 0,
19
- onClick: () => {
20
- if (canGo && !isCurrent) onNavigate == null ? void 0 : onNavigate(s.key);
21
- },
22
- children: s.label
23
- },
24
- s.key
25
- );
26
- }),
27
- action && /* @__PURE__ */ jsxs(Fragment, { children: [
28
- /* @__PURE__ */ jsx("span", { className: "rds-floating-stepper__divider", "aria-hidden": "true" }),
29
- /* @__PURE__ */ jsx(Button, { tone: "neutral", variant: "ghost", icon: action.icon, onClick: action.onClick, children: action.label })
30
- ] })
31
- ] });
32
- }
3
+ import { Stepper } from "./stepper.js";
4
+ const FloatingStepper = Stepper;
33
5
  export {
34
6
  FloatingStepper
35
7
  };
@@ -1,28 +1,50 @@
1
1
  import * as react from 'react';
2
2
  import { HTMLAttributes, ReactNode } from 'react';
3
+ import { BottomBarPlacement } from './internal/bottom-bar.js';
3
4
 
4
- type FormActionsLayout = 'inline' | 'stacked';
5
+ type FormActionsPlacement = BottomBarPlacement;
5
6
  interface FormActionsProps extends HTMLAttributes<HTMLDivElement> {
6
7
  /**
7
- * The buttons (Figma: slot `actions`, up to 3), always in reading order: Cancelar first, the primary
8
- * last. In `stacked` the primary goes on top, and it is moved first in the page too, so the focus
9
- * order follows what is seen.
8
+ * The actions (Figma: the exposed `cancel` and `primary` Buttons), in reading order: Cancelar (tone neutral, ghost)
9
+ * first, the primary (tone action, fill) last. Expanded (from 1024) they all show, on the right; compact (below
10
+ * 1024) only the last one, the primary: the others are `display: none`, and the way out is the topbar's X.
10
11
  */
11
12
  children: ReactNode;
12
- /** A short supporting sentence beside the actions (Figma: `showHelper` + `helper`). */
13
- helper?: ReactNode;
14
13
  /**
15
- * inline (desktop: floats and hugs its content, sentence left, actions right) or stacked (phone: edge
16
- * to edge at the foot, primary full width on top) (Figma: `layout`).
14
+ * docked (default: the container's width, against the foot) or floating (from 1024 up: off the foot, rounded, the
15
+ * overlay shadow, 24 of margin, at most 768, centred). Below 1024 floating is docked (Figma: `placement`).
17
16
  */
18
- layout?: FormActionsLayout;
17
+ placement?: FormActionsPlacement;
18
+ /**
19
+ * One ~44 control on the left, never running text (Figma: slot `leading`, `showLeading`). Present, even `null`
20
+ * while its control is hidden, it reserves 44 so the actions do not jump. Whoever fills it says its own changes
21
+ * (its own aria-live); the bar says nothing.
22
+ */
23
+ leading?: ReactNode;
19
24
  }
20
25
  /**
21
- * FormActions — Figma [RDS] Actions/FormActions. The actions at the end of a create form or a task,
22
- * there from the first second. Where it sticks is the page's job: `position: sticky` at the bottom of
23
- * the scrolling container, with `scroll-padding-bottom` equal to its height, so a focused field is never
24
- * hidden behind it (WCAG 2.4.11). Styles: form-actions.css.
26
+ * FormActions — Figma [RDS] Actions/FormActions. The actions at the end of a create form or of a long task (Cancelar
27
+ * and the primary), at the foot of the screen from the first second. Only actions: what is missing is the project's
28
+ * (if it needs a control, it goes in `leading`). Built on the private BottomBar (.rds-bottom-bar), the SavingBar's
29
+ * shell: a band in the brand's primary colour (bottom-bar/background), 64 plus the safe area compact, 68 expanded,
30
+ * 24 at the sides, sticky at the bottom of the scrolling container. Styles: form-actions.css, internal/bottom-bar.css.
31
+ *
32
+ * `<FormActions placement="docked" leading={…}><Button tone="neutral" variant="ghost">Cancelar</Button><Button
33
+ * tone="action">Criar produto</Button></FormActions>`: the last child is the primary; compact, the others leave.
34
+ *
35
+ * The app's rules around it:
36
+ * - The primary is never disabled: validate on click, show the error on the field and move the focus to the first
37
+ * pending one.
38
+ * - The scrolling container takes `scroll-padding-bottom` (and the content `padding-bottom`) equal to the bar's height
39
+ * (plus the margin when floating) plus `env(safe-area-inset-bottom)`, so a focused field is never hidden behind it
40
+ * (WCAG 2.4.11).
41
+ * - On the phone (below 1024) the bar leaves while the on-screen keyboard is open (a field is focused and the visible
42
+ * area shrinks): it slides down (no motion with prefers-reduced-motion) and comes back on blur; it never takes the
43
+ * focus.
44
+ * - Compact has no Cancelar: the way out is the X in the topbar, with an AlertDialog "Sair sem criar?" ([Continuar
45
+ * editando] [Sair sem criar]) when something was typed.
46
+ * - Buttons at the end of a card or a dialog are loose Buttons or a ButtonGroup, not this.
25
47
  */
26
- declare function FormActions({ children, helper, layout, className, ...rest }: FormActionsProps): react.JSX.Element;
48
+ declare function FormActions({ children, placement, leading, className, ...rest }: FormActionsProps): react.JSX.Element;
27
49
 
28
- export { FormActions, type FormActionsLayout, type FormActionsProps };
50
+ export { FormActions, type FormActionsPlacement, type FormActionsProps };
@@ -1,23 +1,14 @@
1
+ "use client";
1
2
  import {
2
3
  __objRest,
3
4
  __spreadProps,
4
5
  __spreadValues
5
6
  } from "../chunk-ORMEWXMH.js";
6
- import { jsx, jsxs } from "react/jsx-runtime";
7
- import { Children } from "react";
7
+ import { jsx } from "react/jsx-runtime";
8
+ import { BottomBar } from "./internal/bottom-bar.js";
8
9
  function FormActions(_a) {
9
- var _b = _a, { children, helper, layout = "inline", className } = _b, rest = __objRest(_b, ["children", "helper", "layout", "className"]);
10
- const actions = Children.toArray(children);
11
- return /* @__PURE__ */ jsxs(
12
- "div",
13
- __spreadProps(__spreadValues({}, rest), {
14
- className: ["rds-form-actions", `rds-form-actions--${layout}`, className].filter(Boolean).join(" "),
15
- children: [
16
- helper && /* @__PURE__ */ jsx("p", { className: "rds-form-actions__helper", children: helper }),
17
- /* @__PURE__ */ jsx("div", { className: "rds-form-actions__actions", children: layout === "stacked" ? actions.reverse() : actions })
18
- ]
19
- })
20
- );
10
+ var _b = _a, { children, placement = "docked", leading, className } = _b, rest = __objRest(_b, ["children", "placement", "leading", "className"]);
11
+ return /* @__PURE__ */ jsx(BottomBar, __spreadProps(__spreadValues({}, rest), { placement, leading, className: ["rds-form-actions", className].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx("div", { className: "rds-form-actions__actions", children }) }));
21
12
  }
22
13
  export {
23
14
  FormActions
@@ -0,0 +1,42 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes, ReactNode } from 'react';
3
+
4
+ /** The [RDS] type scale for titles: band 36/48, heading 24/30, value 20/30. */
5
+ type HeadingLevel = 'band' | 'heading' | 'value';
6
+ /** neutral: text/heading (navy on light in Rojão, white on dark and on the brand plate). accent: the logo orange. */
7
+ type HeadingTone = 'neutral' | 'accent';
8
+ /** The element in the page outline. The visual level never decides it: the order of the page does. */
9
+ type HeadingElement = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
10
+ interface HeadingProps extends HTMLAttributes<HTMLHeadingElement> {
11
+ /** The size, from the type scale (Figma `level`). */
12
+ level?: HeadingLevel;
13
+ /**
14
+ * The colour of the brand pair (Figma `tone`): neutral (default) or accent. The Mark takes the other one.
15
+ * `'default'` is deprecated (2.0.0-next): it is `'neutral'`.
16
+ */
17
+ tone?: HeadingTone | 'default';
18
+ /** The heading element. Default h2. */
19
+ as?: HeadingElement;
20
+ /** The text (Figma `text`), with an optional <Heading.Mark> stretch (Figma `showMark` + `mark`). */
21
+ children: ReactNode;
22
+ }
23
+ interface HeadingMarkProps extends HTMLAttributes<HTMLSpanElement> {
24
+ children: ReactNode;
25
+ }
26
+ /**
27
+ * Heading.Mark — Figma [RDS] .heading/mark. A stretch of the title in the other colour of the pair: accent inside a
28
+ * neutral Heading, neutral inside an accent one. A plain span: screen readers read the whole title, unannounced.
29
+ */
30
+ declare function HeadingMark({ className, children, ...rest }: HeadingMarkProps): react.JSX.Element;
31
+ /**
32
+ * Heading — Figma [RDS] Content/Heading. A title in the brand pair: one colour, or both in the same title with a
33
+ * Heading.Mark. The colour is chosen per screen; tone=neutral is the rule. Orange on white fails contrast even for
34
+ * a large title (2.9:1 in Rojão): tone=accent belongs on dark or on the brand plate. Styles: heading.css.
35
+ */
36
+ declare function HeadingRoot({ level, tone, as: Tag, className, children, ...rest }: HeadingProps): react.JSX.Element;
37
+ /** `<Heading>` with `<Heading.Mark>` for the stretch in the other colour. */
38
+ declare const Heading: typeof HeadingRoot & {
39
+ Mark: typeof HeadingMark;
40
+ };
41
+
42
+ export { Heading, type HeadingElement, type HeadingLevel, HeadingMark, type HeadingMarkProps, type HeadingProps, type HeadingTone };
@@ -0,0 +1,25 @@
1
+ import {
2
+ __objRest,
3
+ __spreadProps,
4
+ __spreadValues
5
+ } from "../chunk-ORMEWXMH.js";
6
+ import { jsx } from "react/jsx-runtime";
7
+ function HeadingMark(_a) {
8
+ var _b = _a, { className, children } = _b, rest = __objRest(_b, ["className", "children"]);
9
+ return /* @__PURE__ */ jsx("span", __spreadProps(__spreadValues({}, rest), { className: ["rds-heading__mark", className].filter(Boolean).join(" "), children }));
10
+ }
11
+ function HeadingRoot(_c) {
12
+ var _d = _c, { level = "heading", tone = "neutral", as: Tag = "h2", className, children } = _d, rest = __objRest(_d, ["level", "tone", "as", "className", "children"]);
13
+ return /* @__PURE__ */ jsx(
14
+ Tag,
15
+ __spreadProps(__spreadValues({}, rest), {
16
+ className: ["rds-heading", `rds-heading--${level}`, tone === "accent" && "rds-heading--accent", className].filter(Boolean).join(" "),
17
+ children
18
+ })
19
+ );
20
+ }
21
+ const Heading = Object.assign(HeadingRoot, { Mark: HeadingMark });
22
+ export {
23
+ Heading,
24
+ HeadingMark
25
+ };
@@ -9,7 +9,7 @@ interface IconButtonProps extends Omit<ComponentPropsWithRef<'button'>, 'childre
9
9
  label: string;
10
10
  /** Same tones as the Button. */
11
11
  tone?: ButtonTone;
12
- /** Figma calls this property `style`. */
12
+ /** The emphasis (Figma: `variant`). */
13
13
  variant?: ButtonVariant;
14
14
  /**
15
15
  * The size of the square (Figma: `size`): sm 36 with a 16 icon, md 44 with a 20 icon (default), lg 52 with a 24
@@ -0,0 +1,60 @@
1
+ import * as react from 'react';
2
+
3
+ /**
4
+ * ImageCropDialog — Figma [RDS] Overlays/ImageCropDialog: a composition of the Dialog, a ToggleGroup (the aspect
5
+ * presets), the Alert (an aspect out of bounds) and the Spinner: crops an image before upload. Needs
6
+ * `react-image-crop` (an optional peer, needed only here). The crop's shade, line and handles and the stage's
7
+ * background are its own tokens (Overlays, image-crop-dialog/*). Styles: image-crop-dialog.css.
8
+ *
9
+ * Uso:
10
+ * <ImageCropDialog
11
+ * open={open}
12
+ * file={selectedFile}
13
+ * onCancel={() => setOpen(false)}
14
+ * onConfirm={(blob) => uploadCroppedImage(blob)}
15
+ * />
16
+ *
17
+ * Presets default: Livre (1:2 a 2:1), 1:1, 4:5, 16:9.
18
+ * Customize via prop `presets`.
19
+ */
20
+ interface CropPreset {
21
+ id: string;
22
+ label: string;
23
+ /** Aspect ratio fixo. undefined = livre (com bounds MIN/MAX_ASPECT). */
24
+ aspect?: number;
25
+ /** Ícone opcional exibido antes do rótulo (ajuda quem não decodifica "16:9"). */
26
+ icon?: React.ReactNode;
27
+ }
28
+ interface ImageCropDialogProps {
29
+ open: boolean;
30
+ file: File | null;
31
+ /** Label de contexto pro título (ex: "Foto 2 de 4"). */
32
+ position?: string;
33
+ onCancel: () => void;
34
+ /**
35
+ * Recebe o blob do recorte. Pode ser assíncrono (ex.: upload) — o modal AGUARDA a promise
36
+ * e mantém o botão travado/carregando até resolver, evitando duplo-clique que duplicaria a
37
+ * imagem (o upload roda fora do `busy` se o modal não esperar).
38
+ */
39
+ onConfirm: (blob: Blob) => void | Promise<void>;
40
+ /** Presets disponíveis. default = Livre + 1:1 + 4:5 + 16:9. */
41
+ presets?: CropPreset[];
42
+ /** Limite mínimo de aspect ratio (alto-baixo). default = 0.5 (1:2). */
43
+ minAspect?: number;
44
+ /** Limite máximo de aspect ratio (largo-baixo). default = 2.0 (2:1). */
45
+ maxAspect?: number;
46
+ /** Título do modal. default = "Ajustar foto". */
47
+ title?: string;
48
+ /** Label do botão confirmar. default = "Aplicar e enviar". */
49
+ confirmLabel?: string;
50
+ /**
51
+ * Teto de tamanho do blob de saída (bytes). Quando definido, a saída do crop
52
+ * é forçada a JPEG e comprimida (qualidade + downscale) até caber no limite —
53
+ * evita que foto de celular grande estoure o limite de upload. Sem ele, a
54
+ * saída mantém o tipo original e qualidade fixa (comportamento inalterado).
55
+ */
56
+ maxSizeBytes?: number;
57
+ }
58
+ declare function ImageCropDialog({ open, file, position, onCancel, onConfirm, presets, minAspect, maxAspect, title, confirmLabel, maxSizeBytes, }: ImageCropDialogProps): react.JSX.Element;
59
+
60
+ export { type CropPreset, ImageCropDialog, type ImageCropDialogProps };
@@ -0,0 +1,240 @@
1
+ "use client";
2
+ import "../chunk-ORMEWXMH.js";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ import { useEffect, useRef, useState } from "react";
5
+ import ReactCrop, { centerCrop } from "react-image-crop";
6
+ import "react-image-crop/dist/ReactCrop.css";
7
+ import { Alert } from "./alert.js";
8
+ import { Button } from "./button.js";
9
+ import { Dialog } from "./dialog.js";
10
+ import { RectangleHorizontalIcon, RectangleVerticalIcon, SquareDashedIcon, SquareIcon } from "./internal/icons.js";
11
+ import { Spinner } from "./spinner.js";
12
+ import { ToggleGroup, ToggleGroupItem } from "./toggle-group.js";
13
+ const DEFAULT_PRESETS = [
14
+ { id: "square", label: "1:1", aspect: 1, icon: /* @__PURE__ */ jsx(SquareIcon, {}) },
15
+ { id: "portrait", label: "4:5", aspect: 4 / 5, icon: /* @__PURE__ */ jsx(RectangleVerticalIcon, {}) },
16
+ { id: "landscape", label: "16:9", aspect: 16 / 9, icon: /* @__PURE__ */ jsx(RectangleHorizontalIcon, {}) },
17
+ { id: "free", label: "Livre", icon: /* @__PURE__ */ jsx(SquareDashedIcon, {}) }
18
+ ];
19
+ const DEFAULT_MIN_ASPECT = 0.5;
20
+ const DEFAULT_MAX_ASPECT = 2;
21
+ function buildInitialCrop(width, height, aspect) {
22
+ if (!aspect) {
23
+ return { unit: "%", x: 0, y: 0, width: 100, height: 100 };
24
+ }
25
+ const imageAspect = width / height;
26
+ let cropW;
27
+ let cropH;
28
+ if (aspect >= imageAspect) {
29
+ cropW = 100;
30
+ cropH = imageAspect / aspect * 100;
31
+ } else {
32
+ cropH = 100;
33
+ cropW = aspect / imageAspect * 100;
34
+ }
35
+ return centerCrop({ unit: "%", x: 0, y: 0, width: cropW, height: cropH }, width, height);
36
+ }
37
+ const QUALITY_STEPS = [0.85, 0.75, 0.65, 0.55];
38
+ const MAX_SIDE_STEPS = [1600, 1200];
39
+ function canvasToBlob(canvas, mimeType, quality) {
40
+ return new Promise((resolve, reject) => {
41
+ canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error("toBlob failed")), mimeType, quality);
42
+ });
43
+ }
44
+ function drawCrop(image, crop, maxSide) {
45
+ const canvas = document.createElement("canvas");
46
+ const scaleX = image.naturalWidth / image.width;
47
+ const scaleY = image.naturalHeight / image.height;
48
+ let outW = Math.round(crop.width * scaleX);
49
+ let outH = Math.round(crop.height * scaleY);
50
+ if (maxSide && Math.max(outW, outH) > maxSide) {
51
+ const ratio = maxSide / Math.max(outW, outH);
52
+ outW = Math.round(outW * ratio);
53
+ outH = Math.round(outH * ratio);
54
+ }
55
+ canvas.width = outW;
56
+ canvas.height = outH;
57
+ const ctx = canvas.getContext("2d");
58
+ if (!ctx) throw new Error("Canvas not supported");
59
+ ctx.drawImage(
60
+ image,
61
+ crop.x * scaleX,
62
+ crop.y * scaleY,
63
+ crop.width * scaleX,
64
+ crop.height * scaleY,
65
+ 0,
66
+ 0,
67
+ outW,
68
+ outH
69
+ );
70
+ return canvas;
71
+ }
72
+ async function cropToBlob(image, crop, mimeType, maxSizeBytes) {
73
+ const baseCanvas = drawCrop(image, crop);
74
+ if (!maxSizeBytes) {
75
+ return canvasToBlob(baseCanvas, mimeType, 0.92);
76
+ }
77
+ const out = await canvasToBlob(baseCanvas, "image/jpeg", 0.92);
78
+ if (out.size <= maxSizeBytes) return out;
79
+ let best = out;
80
+ for (const q of QUALITY_STEPS) {
81
+ const candidate = await canvasToBlob(baseCanvas, "image/jpeg", q);
82
+ best = candidate;
83
+ if (candidate.size <= maxSizeBytes) return candidate;
84
+ }
85
+ for (const side of MAX_SIDE_STEPS) {
86
+ const scaledCanvas = drawCrop(image, crop, side);
87
+ for (const q of QUALITY_STEPS) {
88
+ const candidate = await canvasToBlob(scaledCanvas, "image/jpeg", q);
89
+ best = candidate;
90
+ if (candidate.size <= maxSizeBytes) return candidate;
91
+ }
92
+ }
93
+ return best;
94
+ }
95
+ function ImageCropDialog({
96
+ open,
97
+ file,
98
+ position,
99
+ onCancel,
100
+ onConfirm,
101
+ presets = DEFAULT_PRESETS,
102
+ minAspect = DEFAULT_MIN_ASPECT,
103
+ maxAspect = DEFAULT_MAX_ASPECT,
104
+ title = "Ajustar foto",
105
+ confirmLabel = "Aplicar e enviar",
106
+ maxSizeBytes
107
+ }) {
108
+ const [src, setSrc] = useState(null);
109
+ const [preset, setPreset] = useState(presets[0]);
110
+ const [crop, setCrop] = useState();
111
+ const [completedCrop, setCompletedCrop] = useState(null);
112
+ const [aspectError, setAspectError] = useState(null);
113
+ const [busy, setBusy] = useState(false);
114
+ const [imgLoaded, setImgLoaded] = useState(false);
115
+ const imgRef = useRef(null);
116
+ useEffect(() => {
117
+ if (!file) {
118
+ setSrc(null);
119
+ setImgLoaded(false);
120
+ return;
121
+ }
122
+ const url = URL.createObjectURL(file);
123
+ setSrc(url);
124
+ setPreset(presets[0]);
125
+ setCrop(void 0);
126
+ setCompletedCrop(null);
127
+ setAspectError(null);
128
+ setImgLoaded(false);
129
+ return () => URL.revokeObjectURL(url);
130
+ }, [file, presets]);
131
+ function handleImageLoad(e) {
132
+ var _a, _b, _c, _d;
133
+ const { width, height } = e.currentTarget;
134
+ const initial = buildInitialCrop(width, height, preset.aspect);
135
+ setCrop(initial);
136
+ const pixelInitial = {
137
+ unit: "px",
138
+ x: ((_a = initial.x) != null ? _a : 0) / 100 * width,
139
+ y: ((_b = initial.y) != null ? _b : 0) / 100 * height,
140
+ width: ((_c = initial.width) != null ? _c : 0) / 100 * width,
141
+ height: ((_d = initial.height) != null ? _d : 0) / 100 * height
142
+ };
143
+ setCompletedCrop(pixelInitial);
144
+ setImgLoaded(true);
145
+ }
146
+ function applyPreset(p) {
147
+ setPreset(p);
148
+ setAspectError(null);
149
+ if (imgRef.current) {
150
+ const { width, height } = imgRef.current;
151
+ setCrop(buildInitialCrop(width, height, p.aspect));
152
+ }
153
+ }
154
+ function validateFreeAspect(c) {
155
+ if (preset.aspect) return true;
156
+ if (!c.width || !c.height) return false;
157
+ const ratio = c.width / c.height;
158
+ if (ratio < minAspect) {
159
+ setAspectError(`Recorte muito alto. M\xEDnimo permitido: ${minAspect.toFixed(1)}.`);
160
+ return false;
161
+ }
162
+ if (ratio > maxAspect) {
163
+ setAspectError(`Recorte muito largo. M\xE1ximo permitido: ${maxAspect.toFixed(1)}.`);
164
+ return false;
165
+ }
166
+ setAspectError(null);
167
+ return true;
168
+ }
169
+ async function handleConfirm() {
170
+ if (busy) return;
171
+ if (!completedCrop || !imgRef.current || !file) return;
172
+ if (!validateFreeAspect(completedCrop)) return;
173
+ setBusy(true);
174
+ try {
175
+ const blob = await cropToBlob(imgRef.current, completedCrop, file.type || "image/jpeg", maxSizeBytes);
176
+ await onConfirm(blob);
177
+ } catch (err) {
178
+ console.error("cropToBlob error", err);
179
+ } finally {
180
+ setBusy(false);
181
+ }
182
+ }
183
+ return /* @__PURE__ */ jsx(
184
+ Dialog,
185
+ {
186
+ open,
187
+ onOpenChange: (next) => {
188
+ if (!next) onCancel();
189
+ },
190
+ size: "lg",
191
+ title: position ? `${title} \xB7 ${position}` : title,
192
+ footer: /* @__PURE__ */ jsxs(Fragment, { children: [
193
+ /* @__PURE__ */ jsx(Button, { tone: "neutral", variant: "ghost", onClick: onCancel, children: "Cancelar" }),
194
+ /* @__PURE__ */ jsx(Button, { onClick: handleConfirm, disabled: busy || !completedCrop || !!aspectError, loading: busy, children: busy ? "Aplicando\u2026" : confirmLabel })
195
+ ] }),
196
+ children: /* @__PURE__ */ jsxs("div", { className: "rds-image-crop", children: [
197
+ presets.length > 1 && /* @__PURE__ */ jsx(
198
+ ToggleGroup,
199
+ {
200
+ type: "single",
201
+ "aria-label": "Propor\xE7\xE3o",
202
+ value: preset.id,
203
+ onValueChange: (id) => {
204
+ const next = presets.find((p) => p.id === id);
205
+ if (next) applyPreset(next);
206
+ },
207
+ className: "rds-image-crop__presets",
208
+ children: presets.map((p) => /* @__PURE__ */ jsx(ToggleGroupItem, { value: p.id, icon: p.icon, children: p.label }, p.id))
209
+ }
210
+ ),
211
+ /* @__PURE__ */ jsxs("div", { className: "rds-image-crop__stage", children: [
212
+ src && /* @__PURE__ */ jsx(
213
+ ReactCrop,
214
+ {
215
+ crop,
216
+ onChange: (_, percentCrop) => setCrop(percentCrop),
217
+ onComplete: (c) => {
218
+ setCompletedCrop(c);
219
+ validateFreeAspect(c);
220
+ },
221
+ aspect: preset.aspect,
222
+ keepSelection: true,
223
+ minWidth: 50,
224
+ minHeight: 50,
225
+ className: "rds-image-crop__crop",
226
+ children: /* @__PURE__ */ jsx("img", { ref: imgRef, src, alt: "", onLoad: handleImageLoad, className: "rds-image-crop__image" })
227
+ }
228
+ ),
229
+ !imgLoaded && /* @__PURE__ */ jsx("div", { className: "rds-image-crop__layer", children: /* @__PURE__ */ jsx(Spinner, { size: "lg", label: "Carregando imagem\u2026" }) }),
230
+ busy && /* @__PURE__ */ jsx("div", { className: "rds-image-crop__layer rds-image-crop__layer--busy", children: /* @__PURE__ */ jsx(Spinner, { size: "lg", tone: "inverse", label: "Enviando\u2026" }) })
231
+ ] }),
232
+ aspectError && /* @__PURE__ */ jsx(Alert, { tone: "danger", title: aspectError, announce: "alert" }),
233
+ !aspectError && imgLoaded && !busy && /* @__PURE__ */ jsx("p", { className: "rds-image-crop__hint", children: preset.aspect ? "Arraste as al\xE7as para ajustar o recorte." : `Livre: propor\xE7\xE3o entre ${minAspect.toFixed(1)} (vertical) e ${maxAspect.toFixed(1)} (horizontal).` })
234
+ ] })
235
+ }
236
+ );
237
+ }
238
+ export {
239
+ ImageCropDialog
240
+ };