@umodeapporg/ui 0.14.0 → 0.16.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 (61) hide show
  1. package/dist/components/Form/ControlledImage/index.d.ts +3 -0
  2. package/dist/components/Form/ControlledImage/index.js +2 -1
  3. package/dist/components/Form/ControlledImage/index.js.map +1 -1
  4. package/dist/components/UI/DataTable/DataTable.js +15 -4
  5. package/dist/components/UI/DataTable/DataTable.js.map +1 -1
  6. package/dist/components/UI/DataTable/DataTableBody.d.ts +5 -1
  7. package/dist/components/UI/DataTable/DataTableBody.js +15 -2
  8. package/dist/components/UI/DataTable/DataTableBody.js.map +1 -1
  9. package/dist/components/UI/DataTable/DataTableBodyCell.js +11 -0
  10. package/dist/components/UI/DataTable/DataTableBodyCell.js.map +1 -1
  11. package/dist/components/UI/DataTable/cells/DataTableEditableCell.js +7 -10
  12. package/dist/components/UI/DataTable/cells/DataTableEditableCell.js.map +1 -1
  13. package/dist/components/UI/DataTable/grid/GridProvider.d.ts +3 -0
  14. package/dist/components/UI/DataTable/grid/GridProvider.js +25 -3
  15. package/dist/components/UI/DataTable/grid/GridProvider.js.map +1 -1
  16. package/dist/components/UI/DataTable/grid/gridStore.d.ts +10 -2
  17. package/dist/components/UI/DataTable/grid/gridStore.js +30 -3
  18. package/dist/components/UI/DataTable/grid/gridStore.js.map +1 -1
  19. package/dist/components/UI/DataTable/grid/gridTypes.d.ts +17 -5
  20. package/dist/components/UI/DataTable/grid/gridTypes.js.map +1 -1
  21. package/dist/components/UI/DataTable/grid/useGridKeyboard.d.ts +4 -2
  22. package/dist/components/UI/DataTable/grid/useGridKeyboard.js +25 -11
  23. package/dist/components/UI/DataTable/grid/useGridKeyboard.js.map +1 -1
  24. package/dist/components/UI/ImageAiAdjust/ImageAiAdjustDialog.d.ts +6 -0
  25. package/dist/components/UI/ImageAiAdjust/ImageAiAdjustDialog.js +164 -74
  26. package/dist/components/UI/ImageAiAdjust/ImageAiAdjustDialog.js.map +1 -1
  27. package/dist/components/UI/ImageAiAdjust/ImageAiDiscardReasonDialog.d.ts +8 -0
  28. package/dist/components/UI/ImageAiAdjust/ImageAiDiscardReasonDialog.js +67 -0
  29. package/dist/components/UI/ImageAiAdjust/ImageAiDiscardReasonDialog.js.map +1 -0
  30. package/dist/components/UI/ImageAiAdjust/ImageAiVerificationNotice.d.ts +8 -0
  31. package/dist/components/UI/ImageAiAdjust/ImageAiVerificationNotice.js +39 -0
  32. package/dist/components/UI/ImageAiAdjust/ImageAiVerificationNotice.js.map +1 -0
  33. package/dist/components/UI/ImageAiAdjust/imageAiReview.d.ts +22 -0
  34. package/dist/components/UI/ImageAiAdjust/imageAiReview.js +37 -0
  35. package/dist/components/UI/ImageAiAdjust/imageAiReview.js.map +1 -0
  36. package/dist/components/UI/ImageAiAdjust/imageAiVerification.d.ts +19 -0
  37. package/dist/components/UI/ImageAiAdjust/imageAiVerification.js +25 -0
  38. package/dist/components/UI/ImageAiAdjust/imageAiVerification.js.map +1 -0
  39. package/dist/components/UI/ImageAiAdjust/useImageAiAdjust.d.ts +25 -2
  40. package/dist/components/UI/ImageAiAdjust/useImageAiAdjust.js +44 -19
  41. package/dist/components/UI/ImageAiAdjust/useImageAiAdjust.js.map +1 -1
  42. package/dist/components/UI/ImageMaskPainter/index.d.ts +19 -0
  43. package/dist/components/UI/ImageMaskPainter/index.js +267 -0
  44. package/dist/components/UI/ImageMaskPainter/index.js.map +1 -0
  45. package/dist/components/UI/ImageMaskPainter/maskStrokes.d.ts +19 -0
  46. package/dist/components/UI/ImageMaskPainter/maskStrokes.js +71 -0
  47. package/dist/components/UI/ImageMaskPainter/maskStrokes.js.map +1 -0
  48. package/dist/helpers/imageFile.d.ts +16 -0
  49. package/dist/helpers/imageFile.js +59 -10
  50. package/dist/helpers/imageFile.js.map +1 -1
  51. package/dist/index.d.ts +2 -0
  52. package/dist/index.js +12 -10
  53. package/dist/index.js.map +1 -1
  54. package/dist/locales/en/ui.json.d.ts +36 -1
  55. package/dist/locales/en/ui.json.js +7 -1
  56. package/dist/locales/en/ui.json.js.map +1 -1
  57. package/dist/locales/index.d.ts +70 -0
  58. package/dist/locales/pt/ui.json.d.ts +36 -1
  59. package/dist/locales/pt/ui.json.js +7 -1
  60. package/dist/locales/pt/ui.json.js.map +1 -1
  61. package/package.json +5 -1
@@ -14,13 +14,12 @@ const isTextEntryTarget = (target) => {
14
14
  if (target.isContentEditable) return true;
15
15
  return target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT";
16
16
  };
17
- const moveKeepingTheEditorOpen = (store, columnDelta, canEditCell) => {
18
- if (!store.getState().moveActiveCell(0, columnDelta, { wrap: true })) return false;
19
- const { activeCell, startEditing } = store.getState();
20
- if (activeCell && canEditCell?.(activeCell)) startEditing(activeCell);
21
- return true;
17
+ const isPrintableKey = (event) => event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
18
+ const moveWithTab = (store, direction, canEditCell) => {
19
+ if (canEditCell) return store.getState().moveToEditableCell(direction, canEditCell);
20
+ return store.getState().moveActiveCell(0, direction, { wrap: true });
22
21
  };
23
- const useGridKeyboard = ({ store, canEditCell, onSave }) => useCallback(
22
+ const useGridKeyboard = ({ store, canEditCell, acceptsTypedValue, onSave }) => useCallback(
24
23
  (event) => {
25
24
  if (!store) return;
26
25
  const state = store.getState();
@@ -41,12 +40,12 @@ const useGridKeyboard = ({ store, canEditCell, onSave }) => useCallback(
41
40
  if (key === "Enter") {
42
41
  event.preventDefault();
43
42
  state.stopEditing();
44
- state.moveActiveCell(1, 0);
43
+ state.moveActiveCell(event.shiftKey ? -1 : 1, 0);
45
44
  return;
46
45
  }
47
46
  if (key === "Tab") {
48
47
  state.stopEditing();
49
- if (moveKeepingTheEditorOpen(store, event.shiftKey ? -1 : 1, canEditCell)) event.preventDefault();
48
+ if (moveWithTab(store, event.shiftKey ? -1 : 1, canEditCell)) event.preventDefault();
50
49
  }
51
50
  return;
52
51
  }
@@ -77,7 +76,7 @@ const useGridKeyboard = ({ store, canEditCell, onSave }) => useCallback(
77
76
  state.moveActiveCell(0, 1, { extend });
78
77
  return;
79
78
  case "Tab":
80
- if (moveKeepingTheEditorOpen(store, event.shiftKey ? -1 : 1, canEditCell)) event.preventDefault();
79
+ if (moveWithTab(store, event.shiftKey ? -1 : 1, canEditCell)) event.preventDefault();
81
80
  return;
82
81
  case "Home":
83
82
  event.preventDefault();
@@ -93,7 +92,7 @@ const useGridKeyboard = ({ store, canEditCell, onSave }) => useCallback(
93
92
  if (!active) return;
94
93
  if (canEditCell?.(active)) {
95
94
  event.preventDefault();
96
- state.startEditing(active);
95
+ state.startEditing(active, { entry: "keyboard" });
97
96
  return;
98
97
  }
99
98
  const focusableInsideCell = cellElement?.querySelector(CELL_CONTENT_FOCUS_SELECTOR);
@@ -110,9 +109,24 @@ const useGridKeyboard = ({ store, canEditCell, onSave }) => useCallback(
110
109
  case "Escape":
111
110
  state.clearSelection();
112
111
  return;
112
+ case "Backspace":
113
+ case "Delete": {
114
+ const active = state.activeCell;
115
+ if (!active || !canEditCell?.(active) || !acceptsTypedValue?.(active)) return;
116
+ event.preventDefault();
117
+ state.startEditing(active, { entry: "typing", seed: "" });
118
+ return;
119
+ }
120
+ default: {
121
+ const active = state.activeCell;
122
+ if (!active || !isPrintableKey(event) || !canEditCell?.(active)) return;
123
+ event.preventDefault();
124
+ const seed = acceptsTypedValue?.(active) ? key : void 0;
125
+ state.startEditing(active, { entry: "typing", seed });
126
+ }
113
127
  }
114
128
  },
115
- [store, canEditCell, onSave]
129
+ [store, canEditCell, acceptsTypedValue, onSave]
116
130
  );
117
131
  export {
118
132
  isTextEntryTarget,
@@ -1 +1 @@
1
- {"version":3,"file":"useGridKeyboard.js","sources":["../../../../../src/components/UI/DataTable/grid/useGridKeyboard.ts"],"sourcesContent":["import React, { useCallback } from 'react';\nimport { type StoreApi } from 'zustand/vanilla';\n\nimport { type IGridState } from './gridStore';\nimport { GRID_CELL_DATA_ATTR, type IGridCellCoord } from './gridTypes';\n\ninterface IUseGridKeyboardParams {\n store: StoreApi<IGridState> | null;\n /** Whether the cell can open an editor, so Enter/F2 know what to do. */\n canEditCell?: (coord: IGridCellCoord) => boolean;\n onSave?: () => void;\n}\n\nconst CELL_CONTENT_FOCUS_SELECTOR = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]',\n]\n .map((selector) => `${selector}:not([tabindex=\"-1\"])`)\n .join(', ');\n\n/** The `<td>` the event came from, so the handler never queries the whole document. */\nconst resolveCellElement = (target: EventTarget | null): HTMLElement | null =>\n target instanceof HTMLElement ? target.closest<HTMLElement>(`[${GRID_CELL_DATA_ATTR}]`) : null;\n\n/**\n * A control that owns the caret (input, textarea, select, contenteditable) keeps\n * every key the grid would otherwise claim: the horizontal arrows walk the text,\n * not the columns.\n */\nexport const isTextEntryTarget = (target: EventTarget | null): boolean => {\n if (!(target instanceof HTMLElement)) return false;\n if (target.isContentEditable) return true;\n\n return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT';\n};\n\n/**\n * Tab walks to the neighbouring cell and OPENS its editor when the cell accepts one, so a\n * row is filled by typing and tabbing instead of reopening the editor at every stop.\n * Returns whether the active cell moved, which is what decides if the grid keeps the event.\n */\nconst moveKeepingTheEditorOpen = (\n store: StoreApi<IGridState>,\n columnDelta: 1 | -1,\n canEditCell?: (coord: IGridCellCoord) => boolean,\n): boolean => {\n // Past the last cell the grid gives the focus back to the page: trapping Tab inside a\n // table is a keyboard trap, not a spreadsheet.\n if (!store.getState().moveActiveCell(0, columnDelta, { wrap: true })) return false;\n\n const { activeCell, startEditing } = store.getState();\n if (activeCell && canEditCell?.(activeCell)) startEditing(activeCell);\n\n return true;\n};\n\n/**\n * The grid's ONLY keyboard listener: one React handler on the scroll container,\n * reached by bubbling from the focused cell. No document listeners, no per-cell\n * handlers, so two tables on the same screen never fight over the same keys.\n */\nexport const useGridKeyboard = ({ store, canEditCell, onSave }: IUseGridKeyboardParams) =>\n useCallback(\n (event: React.KeyboardEvent<HTMLElement>) => {\n if (!store) return;\n\n const state = store.getState();\n const { key } = event;\n\n if ((event.metaKey || event.ctrlKey) && (key === 's' || key === 'S')) {\n if (!onSave) return;\n event.preventDefault();\n onSave();\n return;\n }\n\n const isEditing = !!state.editingCell || isTextEntryTarget(event.target);\n\n if (isEditing) {\n if (key === 'Escape') {\n // The table may live inside a dialog or drawer: leaving the editor must\n // not also close the surface around it.\n event.stopPropagation();\n state.stopEditing('cancel');\n return;\n }\n\n if (key === 'Enter') {\n event.preventDefault();\n state.stopEditing();\n state.moveActiveCell(1, 0);\n return;\n }\n\n if (key === 'Tab') {\n state.stopEditing();\n if (moveKeepingTheEditorOpen(store, event.shiftKey ? -1 : 1, canEditCell)) event.preventDefault();\n }\n\n return;\n }\n\n const cellElement = resolveCellElement(event.target);\n const isInsideCellContent = !!cellElement && event.target !== cellElement;\n\n // Focus went into a control inside the cell (an action button, a link).\n // While it is there the cell content owns the keyboard, and Escape brings\n // the focus back to the cell.\n if (isInsideCellContent) {\n if (key !== 'Escape') return;\n event.preventDefault();\n cellElement.focus();\n return;\n }\n\n const extend = event.shiftKey;\n\n switch (key) {\n case 'ArrowUp':\n event.preventDefault();\n state.moveActiveCell(-1, 0, { extend });\n return;\n case 'ArrowDown':\n event.preventDefault();\n state.moveActiveCell(1, 0, { extend });\n return;\n case 'ArrowLeft':\n event.preventDefault();\n state.moveActiveCell(0, -1, { extend });\n return;\n case 'ArrowRight':\n event.preventDefault();\n state.moveActiveCell(0, 1, { extend });\n return;\n case 'Tab':\n if (moveKeepingTheEditorOpen(store, event.shiftKey ? -1 : 1, canEditCell)) event.preventDefault();\n return;\n case 'Home':\n event.preventDefault();\n state.moveActiveToEdge(event.ctrlKey || event.metaKey ? -1 : 0, -1, { extend });\n return;\n case 'End':\n event.preventDefault();\n state.moveActiveToEdge(event.ctrlKey || event.metaKey ? 1 : 0, 1, { extend });\n return;\n case 'Enter':\n case 'F2': {\n const active = state.activeCell;\n if (!active) return;\n\n if (canEditCell?.(active)) {\n event.preventDefault();\n state.startEditing(active);\n return;\n }\n\n // Read-only cell: Enter steps INTO whatever control it holds, which is\n // the only way a keyboard user reaches the per-row actions now that Tab\n // walks the cells.\n const focusableInsideCell = cellElement?.querySelector<HTMLElement>(CELL_CONTENT_FOCUS_SELECTOR);\n if (focusableInsideCell) {\n event.preventDefault();\n focusableInsideCell.focus();\n return;\n }\n\n if (key !== 'Enter') return;\n event.preventDefault();\n state.moveActiveCell(1, 0);\n\n return;\n }\n case 'Escape':\n state.clearSelection();\n return;\n default:\n }\n },\n [store, canEditCell, onSave],\n );\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useGridKeyboard.js","sources":["../../../../../src/components/UI/DataTable/grid/useGridKeyboard.ts"],"sourcesContent":["import React, { useCallback } from 'react';\nimport { type StoreApi } from 'zustand/vanilla';\n\nimport { type IGridState } from './gridStore';\nimport { GRID_CELL_DATA_ATTR, type IGridCellCoord } from './gridTypes';\n\ninterface IUseGridKeyboardParams {\n store: StoreApi<IGridState> | null;\n /** Whether the cell can open an editor, so Enter/F2/typing know what to do and Tab knows where to stop. */\n canEditCell?: (coord: IGridCellCoord) => boolean;\n /** Whether the cell's editor is free text, so a typed key can become its value. */\n acceptsTypedValue?: (coord: IGridCellCoord) => boolean;\n onSave?: () => void;\n}\n\nconst CELL_CONTENT_FOCUS_SELECTOR = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]',\n]\n .map((selector) => `${selector}:not([tabindex=\"-1\"])`)\n .join(', ');\n\n/** The `<td>` the event came from, so the handler never queries the whole document. */\nconst resolveCellElement = (target: EventTarget | null): HTMLElement | null =>\n target instanceof HTMLElement ? target.closest<HTMLElement>(`[${GRID_CELL_DATA_ATTR}]`) : null;\n\n/**\n * A control that owns the caret (input, textarea, select, contenteditable) keeps\n * every key the grid would otherwise claim: the horizontal arrows walk the text,\n * not the columns.\n */\nexport const isTextEntryTarget = (target: EventTarget | null): boolean => {\n if (!(target instanceof HTMLElement)) return false;\n if (target.isContentEditable) return true;\n\n return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT';\n};\n\nconst isPrintableKey = (event: React.KeyboardEvent): boolean =>\n event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;\n\n/**\n * Tab jumps to the next cell that accepts an editor and opens it, so a row is filled by\n * typing and tabbing, never stopping on a read-only cell. A table with no editable cell\n * keeps the plain cell-by-cell walk. Returns whether the active cell moved, which is what\n * decides if the grid keeps the event: past the last cell the focus goes back to the page,\n * because trapping Tab inside a table is a keyboard trap, not a spreadsheet.\n */\nconst moveWithTab = (\n store: StoreApi<IGridState>,\n direction: 1 | -1,\n canEditCell?: (coord: IGridCellCoord) => boolean,\n): boolean => {\n if (canEditCell) return store.getState().moveToEditableCell(direction, canEditCell);\n return store.getState().moveActiveCell(0, direction, { wrap: true });\n};\n\n/**\n * The grid's ONLY keyboard listener: one React handler on the scroll container,\n * reached by bubbling from the focused cell. No document listeners, no per-cell\n * handlers, so two tables on the same screen never fight over the same keys.\n */\nexport const useGridKeyboard = ({ store, canEditCell, acceptsTypedValue, onSave }: IUseGridKeyboardParams) =>\n useCallback(\n (event: React.KeyboardEvent<HTMLElement>) => {\n if (!store) return;\n\n const state = store.getState();\n const { key } = event;\n\n if ((event.metaKey || event.ctrlKey) && (key === 's' || key === 'S')) {\n if (!onSave) return;\n event.preventDefault();\n onSave();\n return;\n }\n\n const isEditing = !!state.editingCell || isTextEntryTarget(event.target);\n\n if (isEditing) {\n if (key === 'Escape') {\n // The table may live inside a dialog or drawer: leaving the editor must\n // not also close the surface around it.\n event.stopPropagation();\n state.stopEditing('cancel');\n return;\n }\n\n if (key === 'Enter') {\n event.preventDefault();\n state.stopEditing();\n state.moveActiveCell(event.shiftKey ? -1 : 1, 0);\n return;\n }\n\n if (key === 'Tab') {\n state.stopEditing();\n if (moveWithTab(store, event.shiftKey ? -1 : 1, canEditCell)) event.preventDefault();\n }\n\n return;\n }\n\n const cellElement = resolveCellElement(event.target);\n const isInsideCellContent = !!cellElement && event.target !== cellElement;\n\n // Focus went into a control inside the cell (an action button, a link).\n // While it is there the cell content owns the keyboard, and Escape brings\n // the focus back to the cell.\n if (isInsideCellContent) {\n if (key !== 'Escape') return;\n event.preventDefault();\n cellElement.focus();\n return;\n }\n\n const extend = event.shiftKey;\n\n switch (key) {\n case 'ArrowUp':\n event.preventDefault();\n state.moveActiveCell(-1, 0, { extend });\n return;\n case 'ArrowDown':\n event.preventDefault();\n state.moveActiveCell(1, 0, { extend });\n return;\n case 'ArrowLeft':\n event.preventDefault();\n state.moveActiveCell(0, -1, { extend });\n return;\n case 'ArrowRight':\n event.preventDefault();\n state.moveActiveCell(0, 1, { extend });\n return;\n case 'Tab':\n if (moveWithTab(store, event.shiftKey ? -1 : 1, canEditCell)) event.preventDefault();\n return;\n case 'Home':\n event.preventDefault();\n state.moveActiveToEdge(event.ctrlKey || event.metaKey ? -1 : 0, -1, { extend });\n return;\n case 'End':\n event.preventDefault();\n state.moveActiveToEdge(event.ctrlKey || event.metaKey ? 1 : 0, 1, { extend });\n return;\n case 'Enter':\n case 'F2': {\n const active = state.activeCell;\n if (!active) return;\n\n if (canEditCell?.(active)) {\n event.preventDefault();\n state.startEditing(active, { entry: 'keyboard' });\n return;\n }\n\n // Read-only cell: Enter steps INTO whatever control it holds, which is\n // the only way a keyboard user reaches the per-row actions now that Tab\n // walks the cells.\n const focusableInsideCell = cellElement?.querySelector<HTMLElement>(CELL_CONTENT_FOCUS_SELECTOR);\n if (focusableInsideCell) {\n event.preventDefault();\n focusableInsideCell.focus();\n return;\n }\n\n if (key !== 'Enter') return;\n event.preventDefault();\n state.moveActiveCell(1, 0);\n\n return;\n }\n case 'Escape':\n state.clearSelection();\n return;\n case 'Backspace':\n case 'Delete': {\n const active = state.activeCell;\n if (!active || !canEditCell?.(active) || !acceptsTypedValue?.(active)) return;\n event.preventDefault();\n state.startEditing(active, { entry: 'typing', seed: '' });\n return;\n }\n default: {\n // Typing over the active cell edits it, like a spreadsheet: free text takes the\n // key as its new value, a picker opens so the next keys search its options.\n const active = state.activeCell;\n if (!active || !isPrintableKey(event) || !canEditCell?.(active)) return;\n event.preventDefault();\n const seed = acceptsTypedValue?.(active) ? key : undefined;\n state.startEditing(active, { entry: 'typing', seed });\n }\n }\n },\n [store, canEditCell, acceptsTypedValue, onSave],\n );\n"],"names":[],"mappings":";;AAeA,MAAM,8BAA8B;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,EACG,IAAI,CAAC,aAAa,GAAG,QAAQ,uBAAuB,EACpD,KAAK,IAAI;AAGZ,MAAM,qBAAqB,CAAC,WAC1B,kBAAkB,cAAc,OAAO,QAAqB,IAAI,mBAAmB,GAAG,IAAI;AAOrF,MAAM,oBAAoB,CAAC,WAAwC;AACxE,MAAI,EAAE,kBAAkB,aAAc,QAAO;AAC7C,MAAI,OAAO,kBAAmB,QAAO;AAErC,SAAO,OAAO,YAAY,WAAW,OAAO,YAAY,cAAc,OAAO,YAAY;AAC3F;AAEA,MAAM,iBAAiB,CAAC,UACtB,MAAM,IAAI,WAAW,KAAK,CAAC,MAAM,WAAW,CAAC,MAAM,WAAW,CAAC,MAAM;AASvE,MAAM,cAAc,CAClB,OACA,WACA,gBACY;AACZ,MAAI,YAAa,QAAO,MAAM,WAAW,mBAAmB,WAAW,WAAW;AAClF,SAAO,MAAM,WAAW,eAAe,GAAG,WAAW,EAAE,MAAM,MAAM;AACrE;AAOO,MAAM,kBAAkB,CAAC,EAAE,OAAO,aAAa,mBAAmB,aACvE;AAAA,EACE,CAAC,UAA4C;AAC3C,QAAI,CAAC,MAAO;AAEZ,UAAM,QAAQ,MAAM,SAAA;AACpB,UAAM,EAAE,QAAQ;AAEhB,SAAK,MAAM,WAAW,MAAM,aAAa,QAAQ,OAAO,QAAQ,MAAM;AACpE,UAAI,CAAC,OAAQ;AACb,YAAM,eAAA;AACN,aAAA;AACA;AAAA,IACF;AAEA,UAAM,YAAY,CAAC,CAAC,MAAM,eAAe,kBAAkB,MAAM,MAAM;AAEvE,QAAI,WAAW;AACb,UAAI,QAAQ,UAAU;AAGpB,cAAM,gBAAA;AACN,cAAM,YAAY,QAAQ;AAC1B;AAAA,MACF;AAEA,UAAI,QAAQ,SAAS;AACnB,cAAM,eAAA;AACN,cAAM,YAAA;AACN,cAAM,eAAe,MAAM,WAAW,KAAK,GAAG,CAAC;AAC/C;AAAA,MACF;AAEA,UAAI,QAAQ,OAAO;AACjB,cAAM,YAAA;AACN,YAAI,YAAY,OAAO,MAAM,WAAW,KAAK,GAAG,WAAW,EAAG,OAAM,eAAA;AAAA,MACtE;AAEA;AAAA,IACF;AAEA,UAAM,cAAc,mBAAmB,MAAM,MAAM;AACnD,UAAM,sBAAsB,CAAC,CAAC,eAAe,MAAM,WAAW;AAK9D,QAAI,qBAAqB;AACvB,UAAI,QAAQ,SAAU;AACtB,YAAM,eAAA;AACN,kBAAY,MAAA;AACZ;AAAA,IACF;AAEA,UAAM,SAAS,MAAM;AAErB,YAAQ,KAAA;AAAA,MACN,KAAK;AACH,cAAM,eAAA;AACN,cAAM,eAAe,IAAI,GAAG,EAAE,QAAQ;AACtC;AAAA,MACF,KAAK;AACH,cAAM,eAAA;AACN,cAAM,eAAe,GAAG,GAAG,EAAE,QAAQ;AACrC;AAAA,MACF,KAAK;AACH,cAAM,eAAA;AACN,cAAM,eAAe,GAAG,IAAI,EAAE,QAAQ;AACtC;AAAA,MACF,KAAK;AACH,cAAM,eAAA;AACN,cAAM,eAAe,GAAG,GAAG,EAAE,QAAQ;AACrC;AAAA,MACF,KAAK;AACH,YAAI,YAAY,OAAO,MAAM,WAAW,KAAK,GAAG,WAAW,EAAG,OAAM,eAAA;AACpE;AAAA,MACF,KAAK;AACH,cAAM,eAAA;AACN,cAAM,iBAAiB,MAAM,WAAW,MAAM,UAAU,KAAK,GAAG,IAAI,EAAE,OAAA,CAAQ;AAC9E;AAAA,MACF,KAAK;AACH,cAAM,eAAA;AACN,cAAM,iBAAiB,MAAM,WAAW,MAAM,UAAU,IAAI,GAAG,GAAG,EAAE,OAAA,CAAQ;AAC5E;AAAA,MACF,KAAK;AAAA,MACL,KAAK,MAAM;AACT,cAAM,SAAS,MAAM;AACrB,YAAI,CAAC,OAAQ;AAEb,YAAI,cAAc,MAAM,GAAG;AACzB,gBAAM,eAAA;AACN,gBAAM,aAAa,QAAQ,EAAE,OAAO,YAAY;AAChD;AAAA,QACF;AAKA,cAAM,sBAAsB,aAAa,cAA2B,2BAA2B;AAC/F,YAAI,qBAAqB;AACvB,gBAAM,eAAA;AACN,8BAAoB,MAAA;AACpB;AAAA,QACF;AAEA,YAAI,QAAQ,QAAS;AACrB,cAAM,eAAA;AACN,cAAM,eAAe,GAAG,CAAC;AAEzB;AAAA,MACF;AAAA,MACA,KAAK;AACH,cAAM,eAAA;AACN;AAAA,MACF,KAAK;AAAA,MACL,KAAK,UAAU;AACb,cAAM,SAAS,MAAM;AACrB,YAAI,CAAC,UAAU,CAAC,cAAc,MAAM,KAAK,CAAC,oBAAoB,MAAM,EAAG;AACvE,cAAM,eAAA;AACN,cAAM,aAAa,QAAQ,EAAE,OAAO,UAAU,MAAM,IAAI;AACxD;AAAA,MACF;AAAA,MACA,SAAS;AAGP,cAAM,SAAS,MAAM;AACrB,YAAI,CAAC,UAAU,CAAC,eAAe,KAAK,KAAK,CAAC,cAAc,MAAM,EAAG;AACjE,cAAM,eAAA;AACN,cAAM,OAAO,oBAAoB,MAAM,IAAI,MAAM;AACjD,cAAM,aAAa,QAAQ,EAAE,OAAO,UAAU,MAAM;AAAA,MACtD;AAAA,IAAA;AAAA,EAEJ;AAAA,EACA,CAAC,OAAO,aAAa,mBAAmB,MAAM;AAChD;"}
@@ -1,3 +1,4 @@
1
+ import { TImageAiReviewHandler } from './imageAiReview';
1
2
  import { TImageAiAdjuster } from './useImageAiAdjust';
2
3
  interface IImageAiAdjustDialogProps {
3
4
  open: boolean;
@@ -10,6 +11,11 @@ interface IImageAiAdjustDialogProps {
10
11
  onClose: () => void;
11
12
  /** Recebe a imagem ajustada; resolver fecha o diálogo, lançar mantém aberto. */
12
13
  onSave: (file: File) => Promise<void> | void;
14
+ /**
15
+ * Optional feedback sink. Only used when the adjuster resolved a `generationId`: using the
16
+ * result approves it, discarding asks for a reason first. Failures never block the flow.
17
+ */
18
+ onReviewResult?: TImageAiReviewHandler;
13
19
  }
14
20
  declare const ImageAiAdjustDialog: React.FC<IImageAiAdjustDialogProps>;
15
21
  export default ImageAiAdjustDialog;
@@ -1,4 +1,4 @@
1
- import { jsx, jsxs, Fragment } from "react/jsx-runtime";
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
3
  import classNames from "classnames";
4
4
  import { useTranslation } from "react-i18next";
@@ -6,12 +6,16 @@ import ControlledButton from "../../Form/ControlledButton.js";
6
6
  import ControlledDialog from "../../Form/ControlledDialog.js";
7
7
  import ControlledImage from "../../Form/ControlledImage/index.js";
8
8
  import ControlledTextarea from "../../Form/ControlledTextarea.js";
9
+ import ImageMaskPainter from "../ImageMaskPainter/index.js";
9
10
  import LoadingSpinner from "../LoadingSpinner.js";
10
11
  import { MaterialSymbol } from "../MaterialSymbol/index.js";
11
12
  import { uiLogger } from "../../../helpers/uiLogger.js";
12
13
  import { useBreakpoint } from "../../../hooks/useBreakpoint.js";
13
14
  import { useNotification } from "../../../hooks/useNotification.js";
14
- import { useImageAiAdjust } from "./useImageAiAdjust.js";
15
+ import ImageAiDiscardReasonDialog from "./ImageAiDiscardReasonDialog.js";
16
+ import { submitImageAiReview } from "./imageAiReview.js";
17
+ import ImageAiVerificationNotice from "./ImageAiVerificationNotice.js";
18
+ import { useImageAiAdjust, IMAGE_AI_ADJUST_MAX_INSTRUCTIONS } from "./useImageAiAdjust.js";
15
19
  const ImageAiAdjustDialog = ({
16
20
  open,
17
21
  imageUrl,
@@ -19,28 +23,42 @@ const ImageAiAdjustDialog = ({
19
23
  productId,
20
24
  adjustImage,
21
25
  onClose,
22
- onSave
26
+ onSave,
27
+ onReviewResult
23
28
  }) => {
24
29
  const { t } = useTranslation("ui");
25
30
  const { isTabletDown } = useBreakpoint();
26
31
  const { notify } = useNotification();
27
32
  const [saving, setSaving] = useState(false);
33
+ const [pendingDiscard, setPendingDiscard] = useState(null);
34
+ const [markingArea, setMarkingArea] = useState(false);
35
+ const [mask, setMask] = useState(null);
28
36
  const {
29
37
  instructions,
30
38
  setInstructions,
31
39
  resultUrl,
40
+ resultGenerationId,
41
+ resultVerification,
32
42
  isGenerating,
33
43
  canGenerate,
34
44
  generate,
35
45
  cancel,
36
46
  discardResult,
37
47
  buildResultFile
38
- } = useImageAiAdjust({ imageUrl, imageName, productId, adjustImage });
48
+ } = useImageAiAdjust({ imageUrl, imageName, productId, adjustImage, mask: markingArea ? mask : null });
49
+ const toggleMarkingArea = () => {
50
+ if (markingArea) setMask(null);
51
+ setMarkingArea(!markingArea);
52
+ };
53
+ const reviewEnabled = Boolean(onReviewResult && resultGenerationId);
39
54
  const handleSave = async () => {
40
55
  if (!resultUrl || saving) return;
41
56
  setSaving(true);
42
57
  try {
43
58
  await onSave(await buildResultFile());
59
+ if (reviewEnabled && resultGenerationId) {
60
+ void submitImageAiReview(onReviewResult, { generationId: resultGenerationId, decision: "approved" });
61
+ }
44
62
  onClose();
45
63
  } catch (error) {
46
64
  uiLogger.error("Failed to save the AI adjusted image", error);
@@ -49,8 +67,39 @@ const ImageAiAdjustDialog = ({
49
67
  setSaving(false);
50
68
  }
51
69
  };
70
+ const handleTryAgain = () => {
71
+ if (reviewEnabled) {
72
+ setPendingDiscard("tryAgain");
73
+ return;
74
+ }
75
+ discardResult();
76
+ };
77
+ const handleGenerate = () => {
78
+ if (reviewEnabled && resultUrl) {
79
+ setPendingDiscard("regenerate");
80
+ return;
81
+ }
82
+ void generate();
83
+ };
84
+ const handleDiscardConfirmed = ({ reasonCode, reason }) => {
85
+ const action = pendingDiscard;
86
+ setPendingDiscard(null);
87
+ if (resultGenerationId) {
88
+ void submitImageAiReview(onReviewResult, {
89
+ generationId: resultGenerationId,
90
+ decision: "discarded",
91
+ reasonCode,
92
+ reason
93
+ });
94
+ }
95
+ if (action === "regenerate") {
96
+ void generate();
97
+ return;
98
+ }
99
+ discardResult();
100
+ };
52
101
  const previewClass = "max-h-[38vh] w-auto max-w-full rounded-lg object-contain";
53
- return /* @__PURE__ */ jsx(
102
+ return /* @__PURE__ */ jsxs(
54
103
  ControlledDialog,
55
104
  {
56
105
  open,
@@ -60,81 +109,122 @@ const ImageAiAdjustDialog = ({
60
109
  onCancel: onClose,
61
110
  useFooter: false,
62
111
  fitBody: true,
63
- children: /* @__PURE__ */ jsxs("div", { className: "flex max-h-[76vh] flex-col gap-y-3 overflow-y-auto", children: [
64
- /* @__PURE__ */ jsxs("div", { className: classNames("flex gap-3", isTabletDown ? "flex-col" : "flex-row"), children: [
65
- /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-y-1.5", children: [
66
- /* @__PURE__ */ jsx("span", { className: "text-xxs font-semibold uppercase tracking-wide text-foreground-subtle", children: t("imageAiAdjust.original") }),
67
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center rounded-xl bg-ink/95 p-2", children: /* @__PURE__ */ jsx(ControlledImage, { src: imageUrl, className: previewClass }) })
68
- ] }),
69
- /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-y-1.5", children: [
70
- /* @__PURE__ */ jsx("span", { className: "text-xxs font-semibold uppercase tracking-wide text-foreground-subtle", children: t("imageAiAdjust.result") }),
71
- /* @__PURE__ */ jsxs("div", { className: "flex min-h-40 items-center justify-center rounded-xl bg-ink/95 p-2", children: [
72
- isGenerating && /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-y-2 p-4", children: [
73
- /* @__PURE__ */ jsx(LoadingSpinner, { size: 8, color: "white" }),
74
- /* @__PURE__ */ jsx("span", { className: "text-center text-xs text-white", children: t("imageAiAdjust.generating") })
112
+ children: [
113
+ /* @__PURE__ */ jsxs("div", { className: "flex max-h-[76vh] flex-col gap-y-3 overflow-y-auto", children: [
114
+ /* @__PURE__ */ jsxs("div", { className: classNames("flex gap-3", isTabletDown ? "flex-col" : "flex-row"), children: [
115
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-y-1.5", children: [
116
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
117
+ /* @__PURE__ */ jsx("span", { className: "text-xxs font-semibold uppercase tracking-wide text-foreground-subtle", children: t("imageAiAdjust.original") }),
118
+ /* @__PURE__ */ jsxs(
119
+ ControlledButton,
120
+ {
121
+ size: "sm",
122
+ variant: "text",
123
+ tone: markingArea ? "brand" : "neutral",
124
+ "aria-pressed": markingArea,
125
+ disabled: isGenerating || saving,
126
+ onClick: toggleMarkingArea,
127
+ className: "flex items-center gap-x-1.5 px-2 py-1 normal-case",
128
+ children: [
129
+ /* @__PURE__ */ jsx(MaterialSymbol, { "aria-hidden": true, icon: markingArea ? "close" : "brush", size: 14 }),
130
+ markingArea ? t("imageAiAdjust.mask.stop") : t("imageAiAdjust.mask.start")
131
+ ]
132
+ }
133
+ )
75
134
  ] }),
76
- !isGenerating && resultUrl && /* @__PURE__ */ jsx(ControlledImage, { src: resultUrl, className: previewClass }),
77
- !isGenerating && !resultUrl && /* @__PURE__ */ jsx("span", { className: "max-w-64 p-4 text-center text-xs text-white/70", children: t("imageAiAdjust.resultHint") })
135
+ markingArea ? /* @__PURE__ */ jsxs(Fragment, { children: [
136
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-foreground-muted", children: t("imageAiAdjust.mask.hint") }),
137
+ /* @__PURE__ */ jsx(
138
+ ImageMaskPainter,
139
+ {
140
+ imageUrl,
141
+ disabled: isGenerating || saving,
142
+ onMaskChange: setMask,
143
+ imageClassName: previewClass
144
+ }
145
+ )
146
+ ] }) : /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center rounded-xl bg-ink/95 p-2", children: /* @__PURE__ */ jsx(ControlledImage, { src: imageUrl, className: previewClass }) })
147
+ ] }),
148
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-y-1.5", children: [
149
+ /* @__PURE__ */ jsx("span", { className: "text-xxs font-semibold uppercase tracking-wide text-foreground-subtle", children: t("imageAiAdjust.result") }),
150
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-40 items-center justify-center rounded-xl bg-ink/95 p-2", children: [
151
+ isGenerating && /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-y-2 p-4", children: [
152
+ /* @__PURE__ */ jsx(LoadingSpinner, { size: 8, color: "white" }),
153
+ /* @__PURE__ */ jsx("span", { className: "text-center text-xs text-white", children: t("imageAiAdjust.generating") })
154
+ ] }),
155
+ !isGenerating && resultUrl && /* @__PURE__ */ jsx(ControlledImage, { src: resultUrl, className: previewClass }),
156
+ !isGenerating && !resultUrl && /* @__PURE__ */ jsx("span", { className: "max-w-64 p-4 text-center text-xs text-white/70", children: t("imageAiAdjust.resultHint") })
157
+ ] }),
158
+ !isGenerating && resultUrl && /* @__PURE__ */ jsx(ImageAiVerificationNotice, { verification: resultVerification })
78
159
  ] })
79
- ] })
160
+ ] }),
161
+ /* @__PURE__ */ jsx(
162
+ ControlledTextarea,
163
+ {
164
+ resize: "none",
165
+ value: instructions,
166
+ rows: 3,
167
+ maxLength: IMAGE_AI_ADJUST_MAX_INSTRUCTIONS,
168
+ disabled: isGenerating || saving,
169
+ "aria-label": t("imageAiAdjust.promptLabel"),
170
+ placeholder: t("imageAiAdjust.promptPlaceholder"),
171
+ onChange: (event) => setInstructions(event.target.value),
172
+ className: "rounded-xl"
173
+ }
174
+ ),
175
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center justify-end gap-2", children: isGenerating ? /* @__PURE__ */ jsx(ControlledButton, { size: "sm", variant: "outlined", onClick: cancel, className: "normal-case", children: t("actions.cancel") }) : /* @__PURE__ */ jsxs(Fragment, { children: [
176
+ resultUrl && /* @__PURE__ */ jsxs(
177
+ ControlledButton,
178
+ {
179
+ size: "sm",
180
+ variant: "outlined",
181
+ disabled: saving,
182
+ onClick: handleTryAgain,
183
+ className: "flex items-center gap-x-1.5 normal-case",
184
+ children: [
185
+ /* @__PURE__ */ jsx(MaterialSymbol, { icon: "refresh", size: 14 }),
186
+ t("imageAiAdjust.tryAgain")
187
+ ]
188
+ }
189
+ ),
190
+ /* @__PURE__ */ jsxs(
191
+ ControlledButton,
192
+ {
193
+ size: "sm",
194
+ variant: resultUrl ? "outlined" : "filled",
195
+ disabled: !canGenerate || saving,
196
+ onClick: handleGenerate,
197
+ className: "flex items-center gap-x-1.5 normal-case",
198
+ children: [
199
+ /* @__PURE__ */ jsx(MaterialSymbol, { icon: "auto_fix_high", size: 14 }),
200
+ t("imageAiAdjust.generate")
201
+ ]
202
+ }
203
+ ),
204
+ resultUrl && /* @__PURE__ */ jsxs(
205
+ ControlledButton,
206
+ {
207
+ size: "sm",
208
+ disabled: saving,
209
+ onClick: handleSave,
210
+ className: "flex items-center gap-x-1.5 normal-case",
211
+ children: [
212
+ saving ? /* @__PURE__ */ jsx(LoadingSpinner, { size: 4, color: "white" }) : /* @__PURE__ */ jsx(MaterialSymbol, { icon: "check", size: 14, color: "white" }),
213
+ t("imageAiAdjust.useResult")
214
+ ]
215
+ }
216
+ )
217
+ ] }) })
80
218
  ] }),
81
219
  /* @__PURE__ */ jsx(
82
- ControlledTextarea,
220
+ ImageAiDiscardReasonDialog,
83
221
  {
84
- resize: "none",
85
- value: instructions,
86
- rows: 3,
87
- disabled: isGenerating || saving,
88
- "aria-label": t("imageAiAdjust.promptLabel"),
89
- placeholder: t("imageAiAdjust.promptPlaceholder"),
90
- onChange: (event) => setInstructions(event.target.value),
91
- className: "rounded-xl"
222
+ open: pendingDiscard !== null,
223
+ onCancel: () => setPendingDiscard(null),
224
+ onConfirm: handleDiscardConfirmed
92
225
  }
93
- ),
94
- /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center justify-end gap-2", children: isGenerating ? /* @__PURE__ */ jsx(ControlledButton, { size: "sm", variant: "outlined", onClick: cancel, className: "normal-case", children: t("actions.cancel") }) : /* @__PURE__ */ jsxs(Fragment, { children: [
95
- resultUrl && /* @__PURE__ */ jsxs(
96
- ControlledButton,
97
- {
98
- size: "sm",
99
- variant: "outlined",
100
- disabled: saving,
101
- onClick: discardResult,
102
- className: "flex items-center gap-x-1.5 normal-case",
103
- children: [
104
- /* @__PURE__ */ jsx(MaterialSymbol, { icon: "refresh", size: 14 }),
105
- t("imageAiAdjust.tryAgain")
106
- ]
107
- }
108
- ),
109
- /* @__PURE__ */ jsxs(
110
- ControlledButton,
111
- {
112
- size: "sm",
113
- variant: resultUrl ? "outlined" : "filled",
114
- disabled: !canGenerate || saving,
115
- onClick: generate,
116
- className: "flex items-center gap-x-1.5 normal-case",
117
- children: [
118
- /* @__PURE__ */ jsx(MaterialSymbol, { icon: "auto_fix_high", size: 14 }),
119
- t("imageAiAdjust.generate")
120
- ]
121
- }
122
- ),
123
- resultUrl && /* @__PURE__ */ jsxs(
124
- ControlledButton,
125
- {
126
- size: "sm",
127
- disabled: saving,
128
- onClick: handleSave,
129
- className: "flex items-center gap-x-1.5 normal-case",
130
- children: [
131
- saving ? /* @__PURE__ */ jsx(LoadingSpinner, { size: 4, color: "white" }) : /* @__PURE__ */ jsx(MaterialSymbol, { icon: "check", size: 14, color: "white" }),
132
- t("imageAiAdjust.useResult")
133
- ]
134
- }
135
- )
136
- ] }) })
137
- ] })
226
+ )
227
+ ]
138
228
  }
139
229
  );
140
230
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ImageAiAdjustDialog.js","sources":["../../../../src/components/UI/ImageAiAdjust/ImageAiAdjustDialog.tsx"],"sourcesContent":["import { useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledDialog from '@/components/Form/ControlledDialog';\nimport ControlledImage from '@/components/Form/ControlledImage';\nimport ControlledTextarea from '@/components/Form/ControlledTextarea';\nimport LoadingSpinner from '@/components/UI/LoadingSpinner';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport { uiLogger } from '@/helpers/uiLogger';\nimport { useBreakpoint } from '@/hooks/useBreakpoint';\nimport { useNotification } from '@/hooks/useNotification';\n\nimport { TImageAiAdjuster, useImageAiAdjust } from './useImageAiAdjust';\n\ninterface IImageAiAdjustDialogProps {\n open: boolean;\n imageUrl: string;\n imageName?: string;\n /** Opcional: quando existe, a geração fica associada ao produto no criAI. */\n productId?: string;\n /** Runs the generation; the host decides which AI backend serves it. */\n adjustImage: TImageAiAdjuster;\n onClose: () => void;\n /** Recebe a imagem ajustada; resolver fecha o diálogo, lançar mantém aberto. */\n onSave: (file: File) => Promise<void> | void;\n}\n\nconst ImageAiAdjustDialog: React.FC<IImageAiAdjustDialogProps> = ({\n open,\n imageUrl,\n imageName,\n productId,\n adjustImage,\n onClose,\n onSave,\n}) => {\n const { t } = useTranslation('ui');\n const { isTabletDown } = useBreakpoint();\n const { notify } = useNotification();\n\n const [saving, setSaving] = useState(false);\n const {\n instructions,\n setInstructions,\n resultUrl,\n isGenerating,\n canGenerate,\n generate,\n cancel,\n discardResult,\n buildResultFile,\n } = useImageAiAdjust({ imageUrl, imageName, productId, adjustImage });\n\n const handleSave = async () => {\n if (!resultUrl || saving) return;\n\n setSaving(true);\n try {\n await onSave(await buildResultFile());\n onClose();\n } catch (error) {\n uiLogger.error('Failed to save the AI adjusted image', error);\n notify({ message: t('imageAiAdjust.errorSave'), type: 'error' });\n } finally {\n setSaving(false);\n }\n };\n\n const previewClass = 'max-h-[38vh] w-auto max-w-full rounded-lg object-contain';\n\n return (\n <ControlledDialog\n open={open}\n size={isTabletDown ? 'xxl' : 'lg'}\n title={t('imageAiAdjust.title')}\n subtitle={t('imageAiAdjust.subtitle')}\n onCancel={onClose}\n useFooter={false}\n fitBody\n >\n <div className=\"flex max-h-[76vh] flex-col gap-y-3 overflow-y-auto\">\n <div className={classNames('flex gap-3', isTabletDown ? 'flex-col' : 'flex-row')}>\n <div className=\"flex min-w-0 flex-1 flex-col gap-y-1.5\">\n <span className=\"text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {t('imageAiAdjust.original')}\n </span>\n <div className=\"flex items-center justify-center rounded-xl bg-ink/95 p-2\">\n <ControlledImage src={imageUrl} className={previewClass} />\n </div>\n </div>\n\n <div className=\"flex min-w-0 flex-1 flex-col gap-y-1.5\">\n <span className=\"text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {t('imageAiAdjust.result')}\n </span>\n <div className=\"flex min-h-40 items-center justify-center rounded-xl bg-ink/95 p-2\">\n {isGenerating && (\n <div className=\"flex flex-col items-center gap-y-2 p-4\">\n <LoadingSpinner size={8} color=\"white\" />\n <span className=\"text-center text-xs text-white\">{t('imageAiAdjust.generating')}</span>\n </div>\n )}\n\n {!isGenerating && resultUrl && <ControlledImage src={resultUrl} className={previewClass} />}\n\n {!isGenerating && !resultUrl && (\n <span className=\"max-w-64 p-4 text-center text-xs text-white/70\">{t('imageAiAdjust.resultHint')}</span>\n )}\n </div>\n </div>\n </div>\n\n <ControlledTextarea\n resize=\"none\"\n value={instructions}\n rows={3}\n disabled={isGenerating || saving}\n aria-label={t('imageAiAdjust.promptLabel')}\n placeholder={t('imageAiAdjust.promptPlaceholder')}\n onChange={(event) => setInstructions(event.target.value)}\n className=\"rounded-xl\"\n />\n\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n {isGenerating ? (\n <ControlledButton size=\"sm\" variant=\"outlined\" onClick={cancel} className=\"normal-case\">\n {t('actions.cancel')}\n </ControlledButton>\n ) : (\n <>\n {resultUrl && (\n <ControlledButton\n size=\"sm\"\n variant=\"outlined\"\n disabled={saving}\n onClick={discardResult}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n <MaterialSymbol icon=\"refresh\" size={14} />\n {t('imageAiAdjust.tryAgain')}\n </ControlledButton>\n )}\n\n <ControlledButton\n size=\"sm\"\n variant={resultUrl ? 'outlined' : 'filled'}\n disabled={!canGenerate || saving}\n onClick={generate}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n <MaterialSymbol icon=\"auto_fix_high\" size={14} />\n {t('imageAiAdjust.generate')}\n </ControlledButton>\n\n {resultUrl && (\n <ControlledButton\n size=\"sm\"\n disabled={saving}\n onClick={handleSave}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n {saving ? (\n <LoadingSpinner size={4} color=\"white\" />\n ) : (\n <MaterialSymbol icon=\"check\" size={14} color=\"white\" />\n )}\n {t('imageAiAdjust.useResult')}\n </ControlledButton>\n )}\n </>\n )}\n </div>\n </div>\n </ControlledDialog>\n );\n};\n\nexport default ImageAiAdjustDialog;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ImageAiAdjustDialog.js","sources":["../../../../src/components/UI/ImageAiAdjust/ImageAiAdjustDialog.tsx"],"sourcesContent":["import { useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledDialog from '@/components/Form/ControlledDialog';\nimport ControlledImage from '@/components/Form/ControlledImage';\nimport ControlledTextarea from '@/components/Form/ControlledTextarea';\nimport ImageMaskPainter from '@/components/UI/ImageMaskPainter';\nimport LoadingSpinner from '@/components/UI/LoadingSpinner';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport { uiLogger } from '@/helpers/uiLogger';\nimport { useBreakpoint } from '@/hooks/useBreakpoint';\nimport { useNotification } from '@/hooks/useNotification';\n\nimport ImageAiDiscardReasonDialog from './ImageAiDiscardReasonDialog';\nimport { IImageAiDiscardReason, submitImageAiReview, TImageAiReviewHandler } from './imageAiReview';\nimport ImageAiVerificationNotice from './ImageAiVerificationNotice';\nimport { IMAGE_AI_ADJUST_MAX_INSTRUCTIONS, TImageAiAdjuster, useImageAiAdjust } from './useImageAiAdjust';\n\ntype TPendingDiscard = 'tryAgain' | 'regenerate';\n\ninterface IImageAiAdjustDialogProps {\n open: boolean;\n imageUrl: string;\n imageName?: string;\n /** Opcional: quando existe, a geração fica associada ao produto no criAI. */\n productId?: string;\n /** Runs the generation; the host decides which AI backend serves it. */\n adjustImage: TImageAiAdjuster;\n onClose: () => void;\n /** Recebe a imagem ajustada; resolver fecha o diálogo, lançar mantém aberto. */\n onSave: (file: File) => Promise<void> | void;\n /**\n * Optional feedback sink. Only used when the adjuster resolved a `generationId`: using the\n * result approves it, discarding asks for a reason first. Failures never block the flow.\n */\n onReviewResult?: TImageAiReviewHandler;\n}\n\nconst ImageAiAdjustDialog: React.FC<IImageAiAdjustDialogProps> = ({\n open,\n imageUrl,\n imageName,\n productId,\n adjustImage,\n onClose,\n onSave,\n onReviewResult,\n}) => {\n const { t } = useTranslation('ui');\n const { isTabletDown } = useBreakpoint();\n const { notify } = useNotification();\n\n const [saving, setSaving] = useState(false);\n const [pendingDiscard, setPendingDiscard] = useState<TPendingDiscard | null>(null);\n const [markingArea, setMarkingArea] = useState(false);\n const [mask, setMask] = useState<File | null>(null);\n const {\n instructions,\n setInstructions,\n resultUrl,\n resultGenerationId,\n resultVerification,\n isGenerating,\n canGenerate,\n generate,\n cancel,\n discardResult,\n buildResultFile,\n } = useImageAiAdjust({ imageUrl, imageName, productId, adjustImage, mask: markingArea ? mask : null });\n\n const toggleMarkingArea = () => {\n if (markingArea) setMask(null);\n setMarkingArea(!markingArea);\n };\n\n const reviewEnabled = Boolean(onReviewResult && resultGenerationId);\n\n const handleSave = async () => {\n if (!resultUrl || saving) return;\n\n setSaving(true);\n try {\n await onSave(await buildResultFile());\n if (reviewEnabled && resultGenerationId) {\n void submitImageAiReview(onReviewResult, { generationId: resultGenerationId, decision: 'approved' });\n }\n onClose();\n } catch (error) {\n uiLogger.error('Failed to save the AI adjusted image', error);\n notify({ message: t('imageAiAdjust.errorSave'), type: 'error' });\n } finally {\n setSaving(false);\n }\n };\n\n const handleTryAgain = () => {\n if (reviewEnabled) {\n setPendingDiscard('tryAgain');\n return;\n }\n discardResult();\n };\n\n const handleGenerate = () => {\n if (reviewEnabled && resultUrl) {\n setPendingDiscard('regenerate');\n return;\n }\n void generate();\n };\n\n const handleDiscardConfirmed = ({ reasonCode, reason }: IImageAiDiscardReason) => {\n const action = pendingDiscard;\n setPendingDiscard(null);\n if (resultGenerationId) {\n void submitImageAiReview(onReviewResult, {\n generationId: resultGenerationId,\n decision: 'discarded',\n reasonCode,\n reason,\n });\n }\n if (action === 'regenerate') {\n void generate();\n return;\n }\n discardResult();\n };\n\n const previewClass = 'max-h-[38vh] w-auto max-w-full rounded-lg object-contain';\n\n return (\n <ControlledDialog\n open={open}\n size={isTabletDown ? 'xxl' : 'lg'}\n title={t('imageAiAdjust.title')}\n subtitle={t('imageAiAdjust.subtitle')}\n onCancel={onClose}\n useFooter={false}\n fitBody\n >\n <div className=\"flex max-h-[76vh] flex-col gap-y-3 overflow-y-auto\">\n <div className={classNames('flex gap-3', isTabletDown ? 'flex-col' : 'flex-row')}>\n <div className=\"flex min-w-0 flex-1 flex-col gap-y-1.5\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <span className=\"text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {t('imageAiAdjust.original')}\n </span>\n <ControlledButton\n size=\"sm\"\n variant=\"text\"\n tone={markingArea ? 'brand' : 'neutral'}\n aria-pressed={markingArea}\n disabled={isGenerating || saving}\n onClick={toggleMarkingArea}\n className=\"flex items-center gap-x-1.5 px-2 py-1 normal-case\"\n >\n <MaterialSymbol aria-hidden icon={markingArea ? 'close' : 'brush'} size={14} />\n {markingArea ? t('imageAiAdjust.mask.stop') : t('imageAiAdjust.mask.start')}\n </ControlledButton>\n </div>\n {markingArea ? (\n <>\n <span className=\"text-xs text-foreground-muted\">{t('imageAiAdjust.mask.hint')}</span>\n <ImageMaskPainter\n imageUrl={imageUrl}\n disabled={isGenerating || saving}\n onMaskChange={setMask}\n imageClassName={previewClass}\n />\n </>\n ) : (\n <div className=\"flex items-center justify-center rounded-xl bg-ink/95 p-2\">\n <ControlledImage src={imageUrl} className={previewClass} />\n </div>\n )}\n </div>\n\n <div className=\"flex min-w-0 flex-1 flex-col gap-y-1.5\">\n <span className=\"text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {t('imageAiAdjust.result')}\n </span>\n <div className=\"flex min-h-40 items-center justify-center rounded-xl bg-ink/95 p-2\">\n {isGenerating && (\n <div className=\"flex flex-col items-center gap-y-2 p-4\">\n <LoadingSpinner size={8} color=\"white\" />\n <span className=\"text-center text-xs text-white\">{t('imageAiAdjust.generating')}</span>\n </div>\n )}\n\n {!isGenerating && resultUrl && <ControlledImage src={resultUrl} className={previewClass} />}\n\n {!isGenerating && !resultUrl && (\n <span className=\"max-w-64 p-4 text-center text-xs text-white/70\">{t('imageAiAdjust.resultHint')}</span>\n )}\n </div>\n {!isGenerating && resultUrl && <ImageAiVerificationNotice verification={resultVerification} />}\n </div>\n </div>\n\n <ControlledTextarea\n resize=\"none\"\n value={instructions}\n rows={3}\n maxLength={IMAGE_AI_ADJUST_MAX_INSTRUCTIONS}\n disabled={isGenerating || saving}\n aria-label={t('imageAiAdjust.promptLabel')}\n placeholder={t('imageAiAdjust.promptPlaceholder')}\n onChange={(event) => setInstructions(event.target.value)}\n className=\"rounded-xl\"\n />\n\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n {isGenerating ? (\n <ControlledButton size=\"sm\" variant=\"outlined\" onClick={cancel} className=\"normal-case\">\n {t('actions.cancel')}\n </ControlledButton>\n ) : (\n <>\n {resultUrl && (\n <ControlledButton\n size=\"sm\"\n variant=\"outlined\"\n disabled={saving}\n onClick={handleTryAgain}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n <MaterialSymbol icon=\"refresh\" size={14} />\n {t('imageAiAdjust.tryAgain')}\n </ControlledButton>\n )}\n\n <ControlledButton\n size=\"sm\"\n variant={resultUrl ? 'outlined' : 'filled'}\n disabled={!canGenerate || saving}\n onClick={handleGenerate}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n <MaterialSymbol icon=\"auto_fix_high\" size={14} />\n {t('imageAiAdjust.generate')}\n </ControlledButton>\n\n {resultUrl && (\n <ControlledButton\n size=\"sm\"\n disabled={saving}\n onClick={handleSave}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n {saving ? (\n <LoadingSpinner size={4} color=\"white\" />\n ) : (\n <MaterialSymbol icon=\"check\" size={14} color=\"white\" />\n )}\n {t('imageAiAdjust.useResult')}\n </ControlledButton>\n )}\n </>\n )}\n </div>\n </div>\n\n <ImageAiDiscardReasonDialog\n open={pendingDiscard !== null}\n onCancel={() => setPendingDiscard(null)}\n onConfirm={handleDiscardConfirmed}\n />\n </ControlledDialog>\n );\n};\n\nexport default ImageAiAdjustDialog;\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import { IImageAiDiscardReason } from './imageAiReview';
2
+ interface IImageAiDiscardReasonDialogProps {
3
+ open: boolean;
4
+ onCancel: () => void;
5
+ onConfirm: (reason: IImageAiDiscardReason) => void;
6
+ }
7
+ declare const ImageAiDiscardReasonDialog: React.FC<IImageAiDiscardReasonDialogProps>;
8
+ export default ImageAiDiscardReasonDialog;
@@ -0,0 +1,67 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { useTranslation } from "react-i18next";
4
+ import ControlledDialog from "../../Form/ControlledDialog.js";
5
+ import ControlledRadio from "../../Form/ControlledRadio.js";
6
+ import ControlledTextarea from "../../Form/ControlledTextarea.js";
7
+ import { IMAGE_AI_REVIEW_REASON_CODES, IMAGE_AI_REVIEW_REASON_LABEL_KEYS, IMAGE_AI_REVIEW_REASON_MAX } from "./imageAiReview.js";
8
+ const ImageAiDiscardReasonDialog = ({ open, onCancel, onConfirm }) => {
9
+ const { t } = useTranslation("ui");
10
+ const [reasonCode, setReasonCode] = useState(null);
11
+ const [reason, setReason] = useState("");
12
+ useEffect(() => {
13
+ if (!open) return;
14
+ setReasonCode(null);
15
+ setReason("");
16
+ }, [open]);
17
+ const handleConfirm = () => {
18
+ if (!reasonCode) return;
19
+ const trimmed = reason.trim().slice(0, IMAGE_AI_REVIEW_REASON_MAX);
20
+ onConfirm({ reasonCode, reason: trimmed || void 0 });
21
+ };
22
+ return /* @__PURE__ */ jsx(
23
+ ControlledDialog,
24
+ {
25
+ open,
26
+ size: "sm",
27
+ title: t("imageAiReview.discardTitle"),
28
+ subtitle: t("imageAiReview.discardSubtitle"),
29
+ onCancel,
30
+ onConfirm: handleConfirm,
31
+ cancelLabel: t("actions.cancel"),
32
+ confirmLabel: t("imageAiReview.discardConfirm"),
33
+ disableConfirm: !reasonCode,
34
+ children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-3", children: [
35
+ /* @__PURE__ */ jsx("div", { role: "radiogroup", "aria-label": t("imageAiReview.reasonLabel"), className: "flex flex-col gap-y-1.5", children: IMAGE_AI_REVIEW_REASON_CODES.map((code) => /* @__PURE__ */ jsx(
36
+ ControlledRadio,
37
+ {
38
+ id: `image-ai-discard-reason-${code}`,
39
+ name: "image-ai-discard-reason",
40
+ size: "sm",
41
+ checked: reasonCode === code,
42
+ onChange: () => setReasonCode(code),
43
+ label: t(IMAGE_AI_REVIEW_REASON_LABEL_KEYS[code])
44
+ },
45
+ code
46
+ )) }),
47
+ /* @__PURE__ */ jsx(
48
+ ControlledTextarea,
49
+ {
50
+ resize: "none",
51
+ rows: 3,
52
+ value: reason,
53
+ maxLength: IMAGE_AI_REVIEW_REASON_MAX,
54
+ "aria-label": t("imageAiReview.detailsLabel"),
55
+ placeholder: t("imageAiReview.detailsPlaceholder"),
56
+ onChange: (event) => setReason(event.target.value),
57
+ className: "rounded-xl"
58
+ }
59
+ )
60
+ ] })
61
+ }
62
+ );
63
+ };
64
+ export {
65
+ ImageAiDiscardReasonDialog as default
66
+ };
67
+ //# sourceMappingURL=ImageAiDiscardReasonDialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImageAiDiscardReasonDialog.js","sources":["../../../../src/components/UI/ImageAiAdjust/ImageAiDiscardReasonDialog.tsx"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledDialog from '@/components/Form/ControlledDialog';\nimport ControlledRadio from '@/components/Form/ControlledRadio';\nimport ControlledTextarea from '@/components/Form/ControlledTextarea';\n\nimport {\n IImageAiDiscardReason,\n IMAGE_AI_REVIEW_REASON_CODES,\n IMAGE_AI_REVIEW_REASON_LABEL_KEYS,\n IMAGE_AI_REVIEW_REASON_MAX,\n TImageAiReviewReasonCode,\n} from './imageAiReview';\n\ninterface IImageAiDiscardReasonDialogProps {\n open: boolean;\n onCancel: () => void;\n onConfirm: (reason: IImageAiDiscardReason) => void;\n}\n\nconst ImageAiDiscardReasonDialog: React.FC<IImageAiDiscardReasonDialogProps> = ({ open, onCancel, onConfirm }) => {\n const { t } = useTranslation('ui');\n const [reasonCode, setReasonCode] = useState<TImageAiReviewReasonCode | null>(null);\n const [reason, setReason] = useState('');\n\n useEffect(() => {\n if (!open) return;\n setReasonCode(null);\n setReason('');\n }, [open]);\n\n const handleConfirm = () => {\n if (!reasonCode) return;\n const trimmed = reason.trim().slice(0, IMAGE_AI_REVIEW_REASON_MAX);\n onConfirm({ reasonCode, reason: trimmed || undefined });\n };\n\n return (\n <ControlledDialog\n open={open}\n size=\"sm\"\n title={t('imageAiReview.discardTitle')}\n subtitle={t('imageAiReview.discardSubtitle')}\n onCancel={onCancel}\n onConfirm={handleConfirm}\n cancelLabel={t('actions.cancel')}\n confirmLabel={t('imageAiReview.discardConfirm')}\n disableConfirm={!reasonCode}\n >\n <div className=\"flex flex-col gap-y-3\">\n <div role=\"radiogroup\" aria-label={t('imageAiReview.reasonLabel')} className=\"flex flex-col gap-y-1.5\">\n {IMAGE_AI_REVIEW_REASON_CODES.map((code) => (\n <ControlledRadio\n key={code}\n id={`image-ai-discard-reason-${code}`}\n name=\"image-ai-discard-reason\"\n size=\"sm\"\n checked={reasonCode === code}\n onChange={() => setReasonCode(code)}\n label={t(IMAGE_AI_REVIEW_REASON_LABEL_KEYS[code])}\n />\n ))}\n </div>\n\n <ControlledTextarea\n resize=\"none\"\n rows={3}\n value={reason}\n maxLength={IMAGE_AI_REVIEW_REASON_MAX}\n aria-label={t('imageAiReview.detailsLabel')}\n placeholder={t('imageAiReview.detailsPlaceholder')}\n onChange={(event) => setReason(event.target.value)}\n className=\"rounded-xl\"\n />\n </div>\n </ControlledDialog>\n );\n};\n\nexport default ImageAiDiscardReasonDialog;\n"],"names":[],"mappings":";;;;;;;AAqBA,MAAM,6BAAyE,CAAC,EAAE,MAAM,UAAU,gBAAgB;AAChH,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,CAAC,YAAY,aAAa,IAAI,SAA0C,IAAI;AAClF,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,EAAE;AAEvC,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,kBAAc,IAAI;AAClB,cAAU,EAAE;AAAA,EACd,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,gBAAgB,MAAM;AAC1B,QAAI,CAAC,WAAY;AACjB,UAAM,UAAU,OAAO,KAAA,EAAO,MAAM,GAAG,0BAA0B;AACjE,cAAU,EAAE,YAAY,QAAQ,WAAW,QAAW;AAAA,EACxD;AAEA,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,MAAK;AAAA,MACL,OAAO,EAAE,4BAA4B;AAAA,MACrC,UAAU,EAAE,+BAA+B;AAAA,MAC3C;AAAA,MACA,WAAW;AAAA,MACX,aAAa,EAAE,gBAAgB;AAAA,MAC/B,cAAc,EAAE,8BAA8B;AAAA,MAC9C,gBAAgB,CAAC;AAAA,MAEjB,UAAA,qBAAC,OAAA,EAAI,WAAU,yBACb,UAAA;AAAA,QAAA,oBAAC,OAAA,EAAI,MAAK,cAAa,cAAY,EAAE,2BAA2B,GAAG,WAAU,2BAC1E,UAAA,6BAA6B,IAAI,CAAC,SACjC;AAAA,UAAC;AAAA,UAAA;AAAA,YAEC,IAAI,2BAA2B,IAAI;AAAA,YACnC,MAAK;AAAA,YACL,MAAK;AAAA,YACL,SAAS,eAAe;AAAA,YACxB,UAAU,MAAM,cAAc,IAAI;AAAA,YAClC,OAAO,EAAE,kCAAkC,IAAI,CAAC;AAAA,UAAA;AAAA,UAN3C;AAAA,QAAA,CAQR,GACH;AAAA,QAEA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,QAAO;AAAA,YACP,MAAM;AAAA,YACN,OAAO;AAAA,YACP,WAAW;AAAA,YACX,cAAY,EAAE,4BAA4B;AAAA,YAC1C,aAAa,EAAE,kCAAkC;AAAA,YACjD,UAAU,CAAC,UAAU,UAAU,MAAM,OAAO,KAAK;AAAA,YACjD,WAAU;AAAA,UAAA;AAAA,QAAA;AAAA,MACZ,EAAA,CACF;AAAA,IAAA;AAAA,EAAA;AAGN;"}
@@ -0,0 +1,8 @@
1
+ import { IImageAiVerification } from './imageAiVerification';
2
+ interface IImageAiVerificationNoticeProps {
3
+ verification: IImageAiVerification | null | undefined;
4
+ className?: string;
5
+ }
6
+ /** Renders nothing unless the backend flagged the result; the flags are advisory only. */
7
+ declare const ImageAiVerificationNotice: React.FC<IImageAiVerificationNoticeProps>;
8
+ export default ImageAiVerificationNotice;