@agent-native/core 0.90.1 → 0.90.2

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 (295) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/AgentPanel.tsx +1 -1
  5. package/corpus/core/src/client/rich-markdown-editor/useCollabReconcile.ts +11 -6
  6. package/corpus/core/src/client/use-action.ts +39 -2
  7. package/corpus/core/src/client/use-db-sync.ts +44 -18
  8. package/corpus/core/src/db/create-get-db.ts +32 -2
  9. package/corpus/core/src/server/framework-request-handler.ts +52 -4
  10. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +5 -0
  11. package/corpus/templates/assets/.agents/skills/logo-composite/SKILL.md +34 -6
  12. package/corpus/templates/assets/AGENTS.md +23 -4
  13. package/corpus/templates/assets/actions/_generation-preset-settings.ts +48 -0
  14. package/corpus/templates/assets/actions/_helpers.ts +8 -1
  15. package/corpus/templates/assets/actions/_preset-skeleton-validation.ts +64 -0
  16. package/corpus/templates/assets/actions/create-generation-preset.ts +11 -3
  17. package/corpus/templates/assets/actions/generate-image-batch.ts +7 -2
  18. package/corpus/templates/assets/actions/generate-image.ts +342 -25
  19. package/corpus/templates/assets/actions/get-library.ts +4 -3
  20. package/corpus/templates/assets/actions/navigate.ts +2 -1
  21. package/corpus/templates/assets/actions/refresh-generation-run.ts +5 -1
  22. package/corpus/templates/assets/actions/update-asset.ts +1 -0
  23. package/corpus/templates/assets/actions/update-generation-preset.ts +8 -1
  24. package/corpus/templates/assets/actions/view-screen.ts +8 -1
  25. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +9 -1
  26. package/corpus/templates/assets/app/hooks/use-image-model-menu.ts +1 -0
  27. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +11 -0
  28. package/corpus/templates/assets/app/i18n/zh-TW.ts +64 -0
  29. package/corpus/templates/assets/app/i18n-data.ts +650 -0
  30. package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -1
  31. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +44 -33
  32. package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +1474 -0
  33. package/corpus/templates/assets/app/routes/library.tsx +43 -1
  34. package/corpus/templates/assets/server/lib/generation.ts +366 -44
  35. package/corpus/templates/assets/server/lib/image-processing.ts +252 -0
  36. package/corpus/templates/assets/server/lib/preset-skeleton.ts +137 -0
  37. package/corpus/templates/assets/shared/api.ts +47 -0
  38. package/corpus/templates/clips/app/components/library/library-layout.tsx +7 -2
  39. package/corpus/templates/clips/app/components/library/sort-menu.tsx +1 -1
  40. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +114 -33
  41. package/corpus/templates/clips/app/routes/_app.library._index.tsx +27 -1
  42. package/corpus/templates/clips/app/routes/record.tsx +157 -68
  43. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +5 -5
  44. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +20 -3
  45. package/corpus/templates/clips/shared/finalize-recovery.ts +23 -3
  46. package/corpus/templates/content/AGENTS.md +52 -52
  47. package/corpus/templates/content/actions/_builder-cms-read-client.ts +188 -8
  48. package/corpus/templates/content/actions/_builder-docs-client.ts +41 -3
  49. package/corpus/templates/content/actions/_database-source-utils.ts +998 -160
  50. package/corpus/templates/content/actions/_database-utils.ts +44 -4
  51. package/corpus/templates/content/actions/_notion-action-utils.ts +6 -2
  52. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -1
  53. package/corpus/templates/content/actions/attach-content-database-source.ts +85 -12
  54. package/corpus/templates/content/actions/change-content-database-source-role.ts +9 -1
  55. package/corpus/templates/content/actions/delete-content-database.ts +1 -0
  56. package/corpus/templates/content/actions/get-document.ts +11 -2
  57. package/corpus/templates/content/actions/list-content-databases.ts +57 -9
  58. package/corpus/templates/content/actions/prepare-builder-source-review.ts +23 -6
  59. package/corpus/templates/content/actions/refresh-content-database-source.ts +26 -3
  60. package/corpus/templates/content/actions/restore-content-database.ts +1 -0
  61. package/corpus/templates/content/actions/update-document.ts +43 -0
  62. package/corpus/templates/content/app/components/editor/BuilderBodySyncingNotice.tsx +21 -0
  63. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +0 -1
  64. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +70 -28
  65. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +69 -17
  66. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +4 -4
  67. package/corpus/templates/content/app/components/editor/body-hydration.ts +112 -0
  68. package/corpus/templates/content/app/components/editor/builder-body-hydration-pump.ts +52 -0
  69. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +9076 -2876
  70. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +41 -1
  71. package/corpus/templates/content/app/components/editor/extensions/ImageBlock.tsx +36 -11
  72. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +21 -1
  73. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +11 -1
  74. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +11 -8
  75. package/corpus/templates/content/app/global.css +41 -0
  76. package/corpus/templates/content/app/hooks/use-content-database.ts +346 -42
  77. package/corpus/templates/content/app/hooks/use-db-sync.ts +9 -0
  78. package/corpus/templates/content/app/hooks/use-documents.ts +47 -0
  79. package/corpus/templates/content/app/hooks/use-notion.ts +12 -10
  80. package/corpus/templates/content/app/i18n/zh-TW.ts +35 -0
  81. package/corpus/templates/content/app/i18n-data.ts +355 -0
  82. package/corpus/templates/content/app/root.tsx +22 -0
  83. package/corpus/templates/content/changelog/2026-07-01-builder-database-sources-keep-loading-rows-in-the-background.md +6 -0
  84. package/corpus/templates/content/changelog/2026-07-01-builder-source-attach-starts-with-usable-rows.md +6 -0
  85. package/corpus/templates/content/changelog/2026-07-01-builder-source-attachment-now-stays-connected-after-large.md +6 -0
  86. package/corpus/templates/content/changelog/2026-07-01-builder-source-refreshes-now-show-progress-for-large-docs-blog.md +6 -0
  87. package/corpus/templates/content/changelog/2026-07-01-database-pages-now-delete-to-trash-without-route-errors.md +6 -0
  88. package/corpus/templates/content/changelog/2026-07-02-builder-source-refreshes-recover-from-stalled-background-row.md +6 -0
  89. package/corpus/templates/content/changelog/2026-07-02-builder-sourced-rows-open-with-sync-protection.md +6 -0
  90. package/corpus/templates/content/changelog/2026-07-02-database-rows-and-pages-open-faster-from-recently-loaded-dat.md +6 -0
  91. package/corpus/templates/content/changelog/2026-07-02-preview-panels-close-reliably-narrow-sidebar-row-actions-sta.md +6 -0
  92. package/corpus/templates/content/changelog/2026-07-06-builder-sync-progress-no-longer-shows-continue.md +6 -0
  93. package/corpus/templates/content/shared/api.ts +13 -0
  94. package/corpus/templates/content/shared/builder-mdx.ts +16 -3
  95. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +23 -13
  96. package/corpus/templates/design/.agents/skills/responsive-breakpoints/SKILL.md +98 -0
  97. package/corpus/templates/design/.agents/skills/shader-fills/SKILL.md +162 -0
  98. package/corpus/templates/design/AGENTS.md +9 -2
  99. package/corpus/templates/design/actions/add-breakpoint.ts +6 -3
  100. package/corpus/templates/design/actions/apply-a11y-fix.ts +57 -37
  101. package/corpus/templates/design/actions/apply-component-prop-edit.ts +41 -20
  102. package/corpus/templates/design/actions/apply-motion-edit.ts +251 -38
  103. package/corpus/templates/design/actions/apply-shader-fill.ts +90 -66
  104. package/corpus/templates/design/actions/apply-visual-edit.ts +282 -55
  105. package/corpus/templates/design/actions/create-component.ts +29 -60
  106. package/corpus/templates/design/actions/create-file.ts +9 -2
  107. package/corpus/templates/design/actions/duplicate-design.ts +31 -2
  108. package/corpus/templates/design/actions/edit-design.ts +36 -38
  109. package/corpus/templates/design/actions/generate-design-title.ts +87 -0
  110. package/corpus/templates/design/actions/generate-design.ts +53 -18
  111. package/corpus/templates/design/actions/generate-screens.ts +108 -4
  112. package/corpus/templates/design/actions/get-motion-timeline.ts +35 -7
  113. package/corpus/templates/design/actions/insert-asset.ts +34 -29
  114. package/corpus/templates/design/actions/insert-design-native-asset.ts +129 -32
  115. package/corpus/templates/design/actions/insert-figma-library-asset.ts +35 -30
  116. package/corpus/templates/design/actions/present-design-variants.ts +7 -2
  117. package/corpus/templates/design/actions/remove-motion-timeline.ts +31 -36
  118. package/corpus/templates/design/actions/update-file.ts +190 -75
  119. package/corpus/templates/design/app/components/design/BreakpointBar.tsx +403 -0
  120. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +469 -222
  121. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +708 -53
  122. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +83 -3
  123. package/corpus/templates/design/app/components/design/EditPanel.tsx +1091 -6948
  124. package/corpus/templates/design/app/components/design/LayersPanel.tsx +419 -85
  125. package/corpus/templates/design/app/components/design/MotionDock.tsx +1124 -103
  126. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +2610 -1398
  127. package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +22 -0
  128. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +2182 -235
  129. package/corpus/templates/design/app/components/design/bridge/motion-preview.bridge.ts +187 -11
  130. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +376 -1
  131. package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +746 -0
  132. package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +17 -0
  133. package/corpus/templates/design/app/components/design/code-workbench/SideBar.tsx +1 -1
  134. package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +1 -3
  135. package/corpus/templates/design/app/components/design/code-workbench/quickinput/QuickInput.tsx +1 -1
  136. package/corpus/templates/design/app/components/design/edit-panel/appearance-properties.tsx +518 -0
  137. package/corpus/templates/design/app/components/design/edit-panel/code-inspect-helpers.tsx +562 -0
  138. package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +763 -0
  139. package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +94 -0
  140. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +780 -0
  141. package/corpus/templates/design/app/components/design/edit-panel/element-classification.ts +446 -0
  142. package/corpus/templates/design/app/components/design/edit-panel/element-identity.ts +79 -0
  143. package/corpus/templates/design/app/components/design/edit-panel/field-primitives.tsx +315 -0
  144. package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +344 -0
  145. package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +594 -0
  146. package/corpus/templates/design/app/components/design/edit-panel/frame-presets-panel.tsx +119 -0
  147. package/corpus/templates/design/app/components/design/edit-panel/inspector-controls.tsx +248 -0
  148. package/corpus/templates/design/app/components/design/edit-panel/interaction-state-helpers.ts +45 -0
  149. package/corpus/templates/design/app/components/design/edit-panel/layout-properties.tsx +693 -0
  150. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +724 -0
  151. package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +207 -0
  152. package/corpus/templates/design/app/components/design/edit-panel/position-layout-properties.tsx +628 -0
  153. package/corpus/templates/design/app/components/design/edit-panel/selection-helpers.ts +91 -0
  154. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +649 -0
  155. package/corpus/templates/design/app/components/design/edit-panel/style-change-types.ts +118 -0
  156. package/corpus/templates/design/app/components/design/edit-panel/style-options.ts +88 -0
  157. package/corpus/templates/design/app/components/design/edit-panel/transform-helpers.ts +100 -0
  158. package/corpus/templates/design/app/components/design/edit-panel/typography-helpers.ts +117 -0
  159. package/corpus/templates/design/app/components/design/edit-panel/typography-properties.tsx +501 -0
  160. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +91 -40
  161. package/corpus/templates/design/app/components/design/inspector/BreakpointOverrideIndicator.tsx +82 -0
  162. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +189 -87
  163. package/corpus/templates/design/app/components/design/inspector/GlslShaderPanel.tsx +1097 -0
  164. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +44 -0
  165. package/corpus/templates/design/app/components/design/inspector/InteractionStatePanel.tsx +258 -0
  166. package/corpus/templates/design/app/components/design/inspector/MotionKeyframeDiamond.tsx +144 -0
  167. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +99 -9
  168. package/corpus/templates/design/app/components/design/inspector/frame-size-presets.ts +116 -0
  169. package/corpus/templates/design/app/components/design/inspector/index.ts +26 -0
  170. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +29 -0
  171. package/corpus/templates/design/app/components/design/multi-screen/alt-hover-measurement.ts +171 -0
  172. package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +220 -0
  173. package/corpus/templates/design/app/components/design/multi-screen/chrome-transitions.ts +35 -0
  174. package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +134 -0
  175. package/corpus/templates/design/app/components/design/multi-screen/culling.ts +185 -0
  176. package/corpus/templates/design/app/components/design/multi-screen/gradient-overlay-geometry.ts +120 -0
  177. package/corpus/templates/design/app/components/design/multi-screen/types.ts +892 -0
  178. package/corpus/templates/design/app/components/design/multi-screen/vector-edit-geometry.ts +21 -0
  179. package/corpus/templates/design/app/components/design/types.ts +15 -0
  180. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +109 -5
  181. package/corpus/templates/design/app/global.css +8 -8
  182. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +259 -2
  183. package/corpus/templates/design/app/i18n/zh-TW.ts +163 -9
  184. package/corpus/templates/design/app/i18n-breakpoints.ts +205 -0
  185. package/corpus/templates/design/app/i18n-data.ts +1664 -152
  186. package/corpus/templates/design/app/i18n-motion.ts +563 -0
  187. package/corpus/templates/design/app/lib/design-import.ts +73 -0
  188. package/corpus/templates/design/app/pages/Index.tsx +41 -22
  189. package/corpus/templates/design/changelog/2026-06-29-design-inspector-controls-now-match-figmas-softer-dark-chrome.md +1 -1
  190. package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +1 -1
  191. package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +1 -1
  192. package/corpus/templates/design/changelog/2026-06-30-multi-select-outlines-now-stay-consistent-across-all-screens.md +1 -1
  193. package/corpus/templates/design/changelog/2026-07-03-canvas-interactions-now-match-figma-rotation-aware-resizing-.md +1 -1
  194. package/corpus/templates/design/changelog/2026-07-03-layers-panel-matches-figma-top-layer-on-top-drag-with-auto-s.md +1 -1
  195. package/corpus/templates/design/changelog/2026-07-05-designs-get-ai-generated-names-from-your-prompt.md +6 -0
  196. package/corpus/templates/design/changelog/2026-07-05-local-code-screens-keep-their-connection-when-duplicated-and.md +6 -0
  197. package/corpus/templates/design/changelog/2026-07-05-press-a-to-draw-screens-on-the-canvas-inside-a-screen-it-dr.md +6 -0
  198. package/corpus/templates/design/changelog/2026-07-06-3d-rotation-and-perspective-controls-on-every-element.md +6 -0
  199. package/corpus/templates/design/changelog/2026-07-06-ai-generated-and-duplicated-screens-now-get-their-editor-sel.md +6 -0
  200. package/corpus/templates/design/changelog/2026-07-06-applying-shaders-and-fills-in-quick-succession-can-no-longer.md +6 -0
  201. package/corpus/templates/design/changelog/2026-07-06-breakpoint-frames-in-the-overview-now-support-click-to-selec.md +6 -0
  202. package/corpus/templates/design/changelog/2026-07-06-breakpoint-segments-in-the-inspector-now-show-a-device-icon-.md +6 -0
  203. package/corpus/templates/design/changelog/2026-07-06-breakpoint-targeting-now-lives-in-one-compact-control-in-the.md +6 -0
  204. package/corpus/templates/design/changelog/2026-07-06-canvas-edits-can-no-longer-be-overwritten-by-stale-collabora.md +6 -0
  205. package/corpus/templates/design/changelog/2026-07-06-canvas-edits-inside-screens-save-reliably-again.md +6 -0
  206. package/corpus/templates/design/changelog/2026-07-06-cleaner-inspector-removed-the-ask-ai-box-from-the-design-tab.md +6 -0
  207. package/corpus/templates/design/changelog/2026-07-06-clicking-empty-canvas-space-now-deselects-an-element-inside-.md +6 -0
  208. package/corpus/templates/design/changelog/2026-07-06-dragging-an-element-onto-a-rectangle-now-nests-it-inside-wit.md +6 -0
  209. package/corpus/templates/design/changelog/2026-07-06-dragging-screens-and-objects-on-the-canvas-is-now-much-smoot.md +6 -0
  210. package/corpus/templates/design/changelog/2026-07-06-drawing-a-shape-or-text-on-the-canvas-no-longer-flashes-away.md +6 -0
  211. package/corpus/templates/design/changelog/2026-07-06-dropping-an-element-between-two-others-inside-a-screen-now-s.md +6 -0
  212. package/corpus/templates/design/changelog/2026-07-06-duplicating-a-screen-now-places-the-copy-exactly-where-you-d.md +6 -0
  213. package/corpus/templates/design/changelog/2026-07-06-edit-gradients-directly-on-elements-inside-screens-scale-str.md +6 -0
  214. package/corpus/templates/design/changelog/2026-07-06-edits-from-collaborators-and-the-agent-now-update-the-canvas.md +6 -0
  215. package/corpus/templates/design/changelog/2026-07-06-fixed-a-bug-where-most-style-position-and-text-edits-briefly.md +6 -0
  216. package/corpus/templates/design/changelog/2026-07-06-fixed-a-confusing-raw-error-message-when-a-file-upload-fails.md +6 -0
  217. package/corpus/templates/design/changelog/2026-07-06-fixed-a-rare-case-where-an-edit-could-be-silently-overwritte.md +6 -0
  218. package/corpus/templates/design/changelog/2026-07-06-fixed-inserted-assets-sometimes-disappearing-reappearing-or-.md +6 -0
  219. package/corpus/templates/design/changelog/2026-07-06-fixed-inspector-value-scrubbing-producing-erratic-numbers-an.md +6 -0
  220. package/corpus/templates/design/changelog/2026-07-06-fixed-padding-gap-and-min-max-size-scrubbing-not-updating-th.md +6 -0
  221. package/corpus/templates/design/changelog/2026-07-06-fixed-text-stroke-color-turning-black-and-glyphs-disappearin.md +6 -0
  222. package/corpus/templates/design/changelog/2026-07-06-fixed-the-collapsed-effects-and-stroke-inspector-sections-sh.md +6 -0
  223. package/corpus/templates/design/changelog/2026-07-06-fixed-the-new-design-dialog-closing-unexpectedly-when-using-.md +6 -0
  224. package/corpus/templates/design/changelog/2026-07-06-fixed-the-typography-section-missing-when-selecting-text-nes.md +6 -0
  225. package/corpus/templates/design/changelog/2026-07-06-full-view-no-longer-flashes-back-to-the-overview-on-entry-no.md +6 -0
  226. package/corpus/templates/design/changelog/2026-07-06-keyframe-animation-timeline-per-property-keyframes-easing-cu.md +6 -0
  227. package/corpus/templates/design/changelog/2026-07-06-moving-restyling-and-scrubbing-values-on-elements-in-ai-gene.md +6 -0
  228. package/corpus/templates/design/changelog/2026-07-06-padding-and-corner-radius-value-scrubbing-now-snaps-to-whole.md +6 -0
  229. package/corpus/templates/design/changelog/2026-07-06-padding-handles-now-only-resize-on-the-handle-line-itself-sh.md +6 -0
  230. package/corpus/templates/design/changelog/2026-07-06-preview-and-edit-hover-focus-and-pressed-states-with-real-cs.md +6 -0
  231. package/corpus/templates/design/changelog/2026-07-06-promoting-an-element-to-a-component-now-works-correctly-for-.md +6 -0
  232. package/corpus/templates/design/changelog/2026-07-06-reduced-url-history-churn-and-selection-overlay-flicker-whil.md +6 -0
  233. package/corpus/templates/design/changelog/2026-07-06-removed-the-decorative-dot-before-screen-names-in-the-overvi.md +6 -0
  234. package/corpus/templates/design/changelog/2026-07-06-responsive-breakpoints-edit-at-any-width-with-cascading-over.md +6 -0
  235. package/corpus/templates/design/changelog/2026-07-06-screen-labels-no-longer-turn-blue-when-you-hover-inside-the-.md +6 -0
  236. package/corpus/templates/design/changelog/2026-07-06-scrolling-a-screen-with-an-element-selected-stays-smooth-on-.md +6 -0
  237. package/corpus/templates/design/changelog/2026-07-06-selecting-multiple-elements-and-dragging-now-moves-the-whole.md +6 -0
  238. package/corpus/templates/design/changelog/2026-07-06-selection-borders-handles-and-value-readouts-now-stay-the-sa.md +6 -0
  239. package/corpus/templates/design/changelog/2026-07-06-selection-handles-and-editing-chrome-now-stay-a-constant-siz.md +6 -0
  240. package/corpus/templates/design/changelog/2026-07-06-shader-fills-and-effects-ai-authored-or-preset-glsl-shaders-.md +6 -0
  241. package/corpus/templates/design/changelog/2026-07-06-text-drawn-on-the-canvas-now-defaults-to-white-and-inter-in-.md +6 -0
  242. package/corpus/templates/design/changelog/2026-07-06-text-dropped-into-a-shape-now-stays-readable-instead-of-disa.md +6 -0
  243. package/corpus/templates/design/changelog/2026-07-06-the-add-tweak-controls-prompt-no-longer-jumps-to-the-corner-.md +6 -0
  244. package/corpus/templates/design/changelog/2026-07-06-the-padding-value-readout-now-stays-visible-while-you-hover-.md +6 -0
  245. package/corpus/templates/design/changelog/2026-07-06-the-selected-element-attachment-now-clears-from-the-chat-com.md +6 -0
  246. package/corpus/templates/design/changelog/2026-07-06-the-zoom-menu-is-more-compact-with-tight-rows-and-right-alig.md +6 -0
  247. package/corpus/templates/design/changelog/2026-07-06-undo-and-redo-no-longer-flash-the-screen-changes-now-apply-i.md +6 -0
  248. package/corpus/templates/design/changelog/2026-07-06-zoom-presets-like-zoom-to-50-now-stay-on-the-focused-screen-.md +6 -0
  249. package/corpus/templates/design/server/lib/import-design-files.ts +11 -2
  250. package/corpus/templates/design/server/source-workspace.ts +139 -59
  251. package/corpus/templates/design/shared/board-file.ts +11 -2
  252. package/corpus/templates/design/shared/breakpoint-media.ts +543 -0
  253. package/corpus/templates/design/shared/canvas-math.ts +354 -35
  254. package/corpus/templates/design/shared/code-layer.ts +277 -8
  255. package/corpus/templates/design/shared/design-state.ts +11 -4
  256. package/corpus/templates/design/shared/interaction-states.ts +548 -0
  257. package/corpus/templates/design/shared/motion-compiler.ts +186 -23
  258. package/corpus/templates/design/shared/motion-easing.ts +294 -0
  259. package/corpus/templates/design/shared/motion-timeline.ts +400 -34
  260. package/corpus/templates/design/shared/prompt-title.ts +95 -0
  261. package/corpus/templates/design/shared/responsive-classes.ts +448 -10
  262. package/corpus/templates/design/shared/screen-annotation.ts +46 -0
  263. package/corpus/templates/design/shared/shader-fills.ts +1009 -0
  264. package/corpus/templates/design/shared/shader-presets.ts +852 -4
  265. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +15 -2
  266. package/corpus/templates/slides/app/components/editor/ImportButton.tsx +10 -1
  267. package/corpus/templates/slides/app/lib/upload-response.ts +71 -0
  268. package/dist/client/AgentPanel.js +1 -1
  269. package/dist/client/AgentPanel.js.map +1 -1
  270. package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
  271. package/dist/client/rich-markdown-editor/useCollabReconcile.js +11 -6
  272. package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
  273. package/dist/client/use-action.d.ts +3 -0
  274. package/dist/client/use-action.d.ts.map +1 -1
  275. package/dist/client/use-action.js +34 -3
  276. package/dist/client/use-action.js.map +1 -1
  277. package/dist/client/use-db-sync.d.ts +4 -0
  278. package/dist/client/use-db-sync.d.ts.map +1 -1
  279. package/dist/client/use-db-sync.js +37 -18
  280. package/dist/client/use-db-sync.js.map +1 -1
  281. package/dist/collab/routes.d.ts +1 -1
  282. package/dist/collab/struct-routes.d.ts +1 -1
  283. package/dist/db/create-get-db.d.ts +25 -0
  284. package/dist/db/create-get-db.d.ts.map +1 -1
  285. package/dist/db/create-get-db.js +25 -2
  286. package/dist/db/create-get-db.js.map +1 -1
  287. package/dist/observability/routes.d.ts +3 -3
  288. package/dist/secrets/routes.d.ts +9 -9
  289. package/dist/server/framework-request-handler.d.ts.map +1 -1
  290. package/dist/server/framework-request-handler.js +37 -1
  291. package/dist/server/framework-request-handler.js.map +1 -1
  292. package/package.json +1 -1
  293. package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +0 -153
  294. package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +0 -133
  295. package/corpus/templates/design/app/pages/DesignEditor.tsx +0 -22924
@@ -0,0 +1,1474 @@
1
+ import {
2
+ appBasePath,
3
+ insertAgentComposerReference,
4
+ useActionMutation,
5
+ useActionQuery,
6
+ useT,
7
+ } from "@agent-native/core/client";
8
+ import {
9
+ Alert,
10
+ AlertDescription,
11
+ AlertTitle,
12
+ } from "@agent-native/toolkit/ui/alert";
13
+ import {
14
+ AlertDialog,
15
+ AlertDialogAction,
16
+ AlertDialogCancel,
17
+ AlertDialogContent,
18
+ AlertDialogDescription,
19
+ AlertDialogFooter,
20
+ AlertDialogHeader,
21
+ AlertDialogTitle,
22
+ } from "@agent-native/toolkit/ui/alert-dialog";
23
+ import { Badge } from "@agent-native/toolkit/ui/badge";
24
+ import { Button } from "@agent-native/toolkit/ui/button";
25
+ import { Checkbox } from "@agent-native/toolkit/ui/checkbox";
26
+ import { Input } from "@agent-native/toolkit/ui/input";
27
+ import { Label } from "@agent-native/toolkit/ui/label";
28
+ import {
29
+ Select,
30
+ SelectContent,
31
+ SelectItem,
32
+ SelectTrigger,
33
+ SelectValue,
34
+ } from "@agent-native/toolkit/ui/select";
35
+ import { Separator } from "@agent-native/toolkit/ui/separator";
36
+ import { Skeleton } from "@agent-native/toolkit/ui/skeleton";
37
+ import { Spinner } from "@agent-native/toolkit/ui/spinner";
38
+ import {
39
+ IconArrowLeft,
40
+ IconChevronDown,
41
+ IconDeviceFloppy,
42
+ IconLock,
43
+ IconPhotoPlus,
44
+ IconTrash,
45
+ IconUpload,
46
+ } from "@tabler/icons-react";
47
+ import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
48
+ import { Link, useNavigate, useParams } from "react-router";
49
+ import { toast } from "sonner";
50
+
51
+ import { Textarea } from "@/components/ui/textarea";
52
+ import { messagesByLocale } from "@/i18n-data";
53
+ import { assetMediaUrl } from "@/lib/asset-urls";
54
+ import type { AssetUploadResult } from "@/lib/upload-results";
55
+ import { cn } from "@/lib/utils";
56
+
57
+ import {
58
+ ASPECT_RATIOS,
59
+ GENERATION_PRESET_REFERENCE_POLICIES,
60
+ IMAGE_CATEGORIES,
61
+ IMAGE_MODELS,
62
+ IMAGE_SIZES,
63
+ supportedAspectRatiosForModel,
64
+ type AspectRatio,
65
+ type GenerationPresetReferencePolicy,
66
+ type ImageCategory,
67
+ type ImageModel,
68
+ type ImageSize,
69
+ type PresetSkeletonSpec,
70
+ } from "../../shared/api";
71
+
72
+ type SkeletonContentMode = PresetSkeletonSpec["contentMode"];
73
+ type SkeletonLogoPlacement =
74
+ | "upper-right"
75
+ | "upper-left"
76
+ | "lower-right"
77
+ | "lower-left";
78
+ type SkeletonForegroundLayer = NonNullable<
79
+ PresetSkeletonSpec["foreground"]
80
+ >[number];
81
+
82
+ type PresetFormState = {
83
+ title: string;
84
+ description: string;
85
+ category: ImageCategory;
86
+ model: ImageModel;
87
+ aspectRatio: AspectRatio;
88
+ imageSize: ImageSize;
89
+ referencePolicy: GenerationPresetReferencePolicy;
90
+ collectionId: string | null;
91
+ promptTemplate: string;
92
+ textPolicy: string;
93
+ includeLogo: boolean;
94
+ sortOrder: string;
95
+ skeletonEnabled: boolean;
96
+ skeletonContentMode: SkeletonContentMode;
97
+ skeletonBackgroundAssetId: string;
98
+ skeletonBackgroundPreviewUrl: string;
99
+ skeletonMaskAssetId: string;
100
+ skeletonMaskPreviewUrl: string;
101
+ skeletonDropShadow: boolean;
102
+ skeletonLogo: boolean;
103
+ skeletonLogoPlacement: SkeletonLogoPlacement;
104
+ };
105
+
106
+ const NO_COLLECTION = "__none__";
107
+ const NO_SKELETON_BACKGROUND = "__none__";
108
+ const NO_SKELETON_MASK = "__none__";
109
+
110
+ export function meta() {
111
+ return [{ title: messagesByLocale["en-US"].routeTitles.generationPreset }];
112
+ }
113
+
114
+ function skeletonFromPreset(preset: any): PresetSkeletonSpec | null {
115
+ const spec = preset?.settings?.skeletonSpec;
116
+ return spec && typeof spec === "object" ? (spec as PresetSkeletonSpec) : null;
117
+ }
118
+
119
+ function logoPlacementFromLayer(
120
+ layer: SkeletonForegroundLayer,
121
+ ): SkeletonLogoPlacement {
122
+ if (layer.y > 0.5) return layer.x > 0.5 ? "lower-right" : "lower-left";
123
+ return layer.x > 0.5 ? "upper-right" : "upper-left";
124
+ }
125
+
126
+ function logoLayerFromPlacement(
127
+ placement: SkeletonLogoPlacement,
128
+ ): SkeletonForegroundLayer {
129
+ switch (placement) {
130
+ case "upper-left":
131
+ return { source: "canonicalLogo", x: 0.06, y: 0.06, w: 0.16 };
132
+ case "lower-left":
133
+ return { source: "canonicalLogo", x: 0.06, y: 0.82, w: 0.16 };
134
+ case "lower-right":
135
+ return { source: "canonicalLogo", x: 0.78, y: 0.82, w: 0.16 };
136
+ default:
137
+ return { source: "canonicalLogo", x: 0.78, y: 0.06, w: 0.16 };
138
+ }
139
+ }
140
+
141
+ function buildSkeletonSpec(
142
+ form: PresetFormState,
143
+ previous: PresetSkeletonSpec | null = null,
144
+ ): PresetSkeletonSpec | null {
145
+ if (!form.skeletonEnabled || !form.skeletonBackgroundAssetId) return null;
146
+ const foreground = [
147
+ ...(previous?.foreground ?? []).filter(
148
+ (layer) => layer.source !== "canonicalLogo",
149
+ ),
150
+ ...(form.skeletonLogo
151
+ ? [logoLayerFromPlacement(form.skeletonLogoPlacement)]
152
+ : []),
153
+ ];
154
+ return {
155
+ background: {
156
+ type: "asset",
157
+ assetId: form.skeletonBackgroundAssetId,
158
+ },
159
+ ...(form.skeletonMaskAssetId
160
+ ? { mask: { type: "asset" as const, assetId: form.skeletonMaskAssetId } }
161
+ : {}),
162
+ contentMode: form.skeletonContentMode,
163
+ dropShadow:
164
+ form.skeletonContentMode === "cutout"
165
+ ? form.skeletonDropShadow
166
+ : undefined,
167
+ ...(previous?.contentRegion
168
+ ? { contentRegion: previous.contentRegion }
169
+ : {}),
170
+ ...(foreground.length ? { foreground } : {}),
171
+ };
172
+ }
173
+
174
+ function formFromPreset(
175
+ preset: any,
176
+ assets: Array<Record<string, any>> = [],
177
+ ): PresetFormState {
178
+ const skeleton = skeletonFromPreset(preset);
179
+ const skeletonLogoLayer = skeleton?.foreground?.find(
180
+ (layer) => layer.source === "canonicalLogo",
181
+ );
182
+ const skeletonContentMode = skeleton?.contentMode ?? "fill";
183
+ const presetModel = (preset?.model ?? "gemini-3.1-flash-image") as ImageModel;
184
+ const backgroundAssetId =
185
+ skeleton?.background?.type === "asset" ? skeleton.background.assetId : "";
186
+ const backgroundAsset = assets.find(
187
+ (asset) => asset.id === backgroundAssetId,
188
+ );
189
+ const maskAssetId =
190
+ skeleton?.mask?.type === "asset" ? skeleton.mask.assetId : "";
191
+ const maskAsset = assets.find((asset) => asset.id === maskAssetId);
192
+ return {
193
+ title: preset?.title ?? "",
194
+ description: preset?.description ?? "",
195
+ category: preset?.category ?? "social",
196
+ model:
197
+ skeleton &&
198
+ skeletonContentMode === "cutout" &&
199
+ presetModel !== "gpt-image-2"
200
+ ? "gpt-image-1"
201
+ : presetModel,
202
+ aspectRatio: preset?.aspectRatio ?? "1:1",
203
+ imageSize: preset?.imageSize ?? "2K",
204
+ referencePolicy: preset?.referencePolicy ?? "auto",
205
+ collectionId: preset?.collectionId ?? null,
206
+ promptTemplate: preset?.promptTemplate ?? "",
207
+ textPolicy: preset?.textPolicy ?? "",
208
+ includeLogo: preset?.includeLogo === true,
209
+ sortOrder: String(preset?.sortOrder ?? 0),
210
+ skeletonEnabled: Boolean(skeleton && backgroundAssetId),
211
+ skeletonContentMode,
212
+ skeletonBackgroundAssetId: backgroundAssetId,
213
+ skeletonBackgroundPreviewUrl: previewUrlForAsset(backgroundAsset),
214
+ skeletonMaskAssetId: maskAssetId,
215
+ skeletonMaskPreviewUrl: previewUrlForAsset(maskAsset),
216
+ skeletonDropShadow: skeleton?.dropShadow === true,
217
+ skeletonLogo: Boolean(skeletonLogoLayer),
218
+ skeletonLogoPlacement: skeletonLogoLayer
219
+ ? logoPlacementFromLayer(skeletonLogoLayer)
220
+ : "upper-right",
221
+ };
222
+ }
223
+
224
+ function normalizedForm(form: PresetFormState) {
225
+ return {
226
+ ...form,
227
+ title: form.title.trim(),
228
+ description: form.description.trim(),
229
+ promptTemplate: form.promptTemplate.trim(),
230
+ textPolicy: form.textPolicy.trim(),
231
+ sortOrder: Number.isFinite(Number(form.sortOrder))
232
+ ? String(Number(form.sortOrder))
233
+ : "0",
234
+ skeletonBackgroundPreviewUrl: "",
235
+ skeletonMaskPreviewUrl: "",
236
+ };
237
+ }
238
+
239
+ function previewUrlForAsset(asset: any): string {
240
+ return (
241
+ assetMediaUrl(
242
+ asset?.thumbnailUrl ?? asset?.previewUrl ?? asset?.downloadUrl,
243
+ ) ?? ""
244
+ );
245
+ }
246
+
247
+ function isImageAsset(asset: any): boolean {
248
+ return (
249
+ asset?.mediaType === "image" ||
250
+ (typeof asset?.mimeType === "string" && asset.mimeType.startsWith("image/"))
251
+ );
252
+ }
253
+
254
+ function uploadedSkeletonAssetId(
255
+ result: AssetUploadResult | null | undefined,
256
+ ): string | null {
257
+ return (
258
+ result?.assets?.find((asset) => asset.id)?.id ??
259
+ result?.skippedDuplicates?.find((asset) => asset.assetId)?.assetId ??
260
+ null
261
+ );
262
+ }
263
+
264
+ function isEditableRole(role: unknown): boolean {
265
+ return role === "owner" || role === "admin" || role === "editor";
266
+ }
267
+
268
+ function FieldLabel({
269
+ htmlFor,
270
+ children,
271
+ }: {
272
+ htmlFor?: string;
273
+ children: ReactNode;
274
+ }) {
275
+ return (
276
+ <Label
277
+ htmlFor={htmlFor}
278
+ className="text-xs font-medium text-muted-foreground"
279
+ >
280
+ {children}
281
+ </Label>
282
+ );
283
+ }
284
+
285
+ function SkeletonPreview({ form }: { form: PresetFormState }) {
286
+ const logoLayer = logoLayerFromPlacement(form.skeletonLogoPlacement);
287
+ return (
288
+ <div className="relative mt-3 aspect-video overflow-hidden rounded-md border border-border bg-muted">
289
+ {form.skeletonBackgroundPreviewUrl ? (
290
+ <img
291
+ src={form.skeletonBackgroundPreviewUrl}
292
+ alt=""
293
+ className="absolute inset-0 h-full w-full object-cover"
294
+ />
295
+ ) : null}
296
+ <div
297
+ className={cn(
298
+ "absolute bg-background/85 shadow-sm",
299
+ form.skeletonContentMode === "cutout"
300
+ ? "inset-x-[26%] top-[14%] h-[62%] rounded-full"
301
+ : "inset-[12%] rounded-sm",
302
+ )}
303
+ />
304
+ {form.skeletonContentMode === "cutout" && form.skeletonDropShadow ? (
305
+ <div className="absolute bottom-[18%] left-1/2 h-3 w-28 -translate-x-1/2 rounded-full bg-foreground/20 blur-sm" />
306
+ ) : null}
307
+ {form.skeletonLogo ? (
308
+ <div
309
+ className="absolute h-3 rounded-sm bg-foreground/80"
310
+ style={{
311
+ left: `${logoLayer.x * 100}%`,
312
+ top: `${logoLayer.y * 100}%`,
313
+ width: `${logoLayer.w * 100}%`,
314
+ }}
315
+ />
316
+ ) : null}
317
+ </div>
318
+ );
319
+ }
320
+
321
+ export default function GenerationPresetEditorRoute() {
322
+ const t = useT();
323
+ const navigate = useNavigate();
324
+ const { id, presetId } = useParams();
325
+ const libraryId = id ?? "";
326
+ const { data: libraryData, isLoading: libraryLoading } = useActionQuery(
327
+ "get-library",
328
+ { id: libraryId },
329
+ ) as any;
330
+ const { data: presetData, isLoading: presetsLoading } = useActionQuery(
331
+ "list-generation-presets",
332
+ { libraryId },
333
+ ) as any;
334
+ const updatePreset = useActionMutation("update-generation-preset");
335
+ const deletePreset = useActionMutation("delete-generation-preset");
336
+ const [form, setForm] = useState<PresetFormState | null>(null);
337
+ const [initialForm, setInitialForm] = useState<PresetFormState | null>(null);
338
+ const [advancedOpen, setAdvancedOpen] = useState(false);
339
+ const [deleteOpen, setDeleteOpen] = useState(false);
340
+ const [skeletonUploadPending, setSkeletonUploadPending] = useState(false);
341
+ const [skeletonMaskUploadPending, setSkeletonMaskUploadPending] =
342
+ useState(false);
343
+ const skeletonFileInputRef = useRef<HTMLInputElement | null>(null);
344
+ const skeletonMaskFileInputRef = useRef<HTMLInputElement | null>(null);
345
+
346
+ const library = libraryData?.library;
347
+ const assets = useMemo(
348
+ () => (Array.isArray(libraryData?.assets) ? libraryData.assets : []),
349
+ [libraryData?.assets],
350
+ );
351
+ const collections = Array.isArray(libraryData?.collections)
352
+ ? libraryData.collections
353
+ : [];
354
+ const skeletonBackgroundAssets = useMemo(
355
+ () => assets.filter(isImageAsset),
356
+ [assets],
357
+ );
358
+ const presets = Array.isArray(presetData?.presets) ? presetData.presets : [];
359
+ const preset = presets.find((item: any) => item.id === presetId);
360
+ const loading = libraryLoading || presetsLoading;
361
+ const accessRole = library?.accessRole;
362
+ const readOnly = Boolean(accessRole && !isEditableRole(accessRole));
363
+
364
+ useEffect(() => {
365
+ if (!preset) return;
366
+ const next = formFromPreset(preset, assets);
367
+ setForm(next);
368
+ setInitialForm(next);
369
+ }, [preset?.id, preset?.updatedAt]);
370
+
371
+ useEffect(() => {
372
+ function hydratePreview(
373
+ current: PresetFormState | null,
374
+ ): PresetFormState | null {
375
+ if (!current) {
376
+ return current;
377
+ }
378
+ const backgroundAsset = assets.find(
379
+ (item: any) => item.id === current.skeletonBackgroundAssetId,
380
+ );
381
+ const maskAsset = assets.find(
382
+ (item: any) => item.id === current.skeletonMaskAssetId,
383
+ );
384
+ const backgroundPreviewUrl =
385
+ current.skeletonBackgroundPreviewUrl ||
386
+ previewUrlForAsset(backgroundAsset);
387
+ const maskPreviewUrl =
388
+ current.skeletonMaskPreviewUrl || previewUrlForAsset(maskAsset);
389
+ if (
390
+ backgroundPreviewUrl === current.skeletonBackgroundPreviewUrl &&
391
+ maskPreviewUrl === current.skeletonMaskPreviewUrl
392
+ ) {
393
+ return current;
394
+ }
395
+ return {
396
+ ...current,
397
+ skeletonBackgroundPreviewUrl: backgroundPreviewUrl,
398
+ skeletonMaskPreviewUrl: maskPreviewUrl,
399
+ };
400
+ }
401
+ setForm(hydratePreview);
402
+ setInitialForm(hydratePreview);
403
+ }, [assets]);
404
+
405
+ useEffect(() => {
406
+ if (!library?.id || !library?.title || !preset?.id || !preset?.title) {
407
+ return;
408
+ }
409
+ const encodedLibraryId = encodeURIComponent(library.id);
410
+ insertAgentComposerReference({
411
+ label: preset.title,
412
+ icon: "document",
413
+ source: "presets",
414
+ refType: "preset",
415
+ refId: preset.id,
416
+ refPath: `/library/${encodedLibraryId}`,
417
+ slotKey: "preset",
418
+ slotLabel: "Preset",
419
+ metadata: {
420
+ libraryId: library.id,
421
+ libraryTitle: library.title,
422
+ requiredSlotKey: "brand-kit",
423
+ requiredRefId: library.id,
424
+ mediaType: preset.mediaType,
425
+ },
426
+ relatedReferences: [
427
+ {
428
+ label: library.title,
429
+ icon: "folder",
430
+ source: "brandKits",
431
+ refType: "brand-kit",
432
+ refId: library.id,
433
+ refPath: `/library/${encodedLibraryId}`,
434
+ slotKey: "brand-kit",
435
+ slotLabel: "Brand kit",
436
+ clearsSlots: ["preset"],
437
+ metadata: {
438
+ libraryId: library.id,
439
+ },
440
+ },
441
+ ],
442
+ });
443
+ }, [
444
+ library?.id,
445
+ library?.title,
446
+ preset?.id,
447
+ preset?.mediaType,
448
+ preset?.title,
449
+ ]);
450
+
451
+ const supportedRatios = useMemo(
452
+ () =>
453
+ form?.skeletonEnabled
454
+ ? ASPECT_RATIOS
455
+ : form
456
+ ? supportedAspectRatiosForModel(form.model)
457
+ : ASPECT_RATIOS,
458
+ [form?.model, form?.skeletonEnabled],
459
+ );
460
+ const modelOptions = useMemo(
461
+ () =>
462
+ form?.skeletonEnabled && form.skeletonContentMode === "cutout"
463
+ ? IMAGE_MODELS.filter(
464
+ (model) => model === "gpt-image-1" || model === "gpt-image-2",
465
+ )
466
+ : IMAGE_MODELS,
467
+ [form?.skeletonContentMode, form?.skeletonEnabled],
468
+ );
469
+ const dirty = Boolean(
470
+ form &&
471
+ initialForm &&
472
+ JSON.stringify(normalizedForm(form)) !==
473
+ JSON.stringify(normalizedForm(initialForm)),
474
+ );
475
+ const settingsHref = libraryId
476
+ ? `/library/${encodeURIComponent(libraryId)}?tab=settings`
477
+ : "/library";
478
+
479
+ function updateForm(patch: Partial<PresetFormState>) {
480
+ setForm((current) => (current ? { ...current, ...patch } : current));
481
+ }
482
+
483
+ function updateModel(model: ImageModel) {
484
+ setForm((current) => {
485
+ if (!current) return current;
486
+ if (current.skeletonEnabled && current.skeletonContentMode === "cutout") {
487
+ return {
488
+ ...current,
489
+ model:
490
+ model === "gpt-image-1" || model === "gpt-image-2"
491
+ ? model
492
+ : "gpt-image-1",
493
+ };
494
+ }
495
+ const ratios = supportedAspectRatiosForModel(model);
496
+ return {
497
+ ...current,
498
+ model,
499
+ aspectRatio: ratios.includes(current.aspectRatio)
500
+ ? current.aspectRatio
501
+ : ratios[0],
502
+ };
503
+ });
504
+ }
505
+
506
+ function updateSkeletonEnabled(enabled: boolean) {
507
+ setForm((current) => {
508
+ if (!current) return current;
509
+ return {
510
+ ...current,
511
+ skeletonEnabled: enabled,
512
+ model:
513
+ enabled &&
514
+ current.skeletonContentMode === "cutout" &&
515
+ current.model !== "gpt-image-1" &&
516
+ current.model !== "gpt-image-2"
517
+ ? "gpt-image-1"
518
+ : current.model,
519
+ };
520
+ });
521
+ }
522
+
523
+ function updateSkeletonContentMode(contentMode: SkeletonContentMode) {
524
+ setForm((current) =>
525
+ current
526
+ ? {
527
+ ...current,
528
+ skeletonContentMode: contentMode,
529
+ model:
530
+ contentMode === "cutout" &&
531
+ current.model !== "gpt-image-1" &&
532
+ current.model !== "gpt-image-2"
533
+ ? "gpt-image-1"
534
+ : current.model,
535
+ skeletonDropShadow:
536
+ contentMode === "cutout" ? current.skeletonDropShadow : false,
537
+ }
538
+ : current,
539
+ );
540
+ }
541
+
542
+ function updateSkeletonBackgroundAsset(assetId: string) {
543
+ if (assetId === NO_SKELETON_BACKGROUND) {
544
+ updateForm({
545
+ skeletonBackgroundAssetId: "",
546
+ skeletonBackgroundPreviewUrl: "",
547
+ });
548
+ return;
549
+ }
550
+ const asset = skeletonBackgroundAssets.find(
551
+ (item: any) => item.id === assetId,
552
+ );
553
+ updateForm({
554
+ skeletonBackgroundAssetId: assetId,
555
+ skeletonBackgroundPreviewUrl: previewUrlForAsset(asset),
556
+ });
557
+ }
558
+
559
+ function updateSkeletonMaskAsset(assetId: string) {
560
+ if (assetId === NO_SKELETON_MASK) {
561
+ updateForm({
562
+ skeletonMaskAssetId: "",
563
+ skeletonMaskPreviewUrl: "",
564
+ });
565
+ return;
566
+ }
567
+ const asset = skeletonBackgroundAssets.find(
568
+ (item: any) => item.id === assetId,
569
+ );
570
+ updateForm({
571
+ skeletonMaskAssetId: assetId,
572
+ skeletonMaskPreviewUrl: previewUrlForAsset(asset),
573
+ });
574
+ }
575
+
576
+ async function uploadSkeletonImage(
577
+ files: FileList | null,
578
+ target: "background" | "mask" = "background",
579
+ ) {
580
+ const pending =
581
+ target === "mask" ? skeletonMaskUploadPending : skeletonUploadPending;
582
+ if (!files?.length || !libraryId || readOnly || pending) return;
583
+ const file = files[0];
584
+ const localPreviewUrl = URL.createObjectURL(file);
585
+ const body = new FormData();
586
+ body.append("libraryId", libraryId);
587
+ body.append("category", "skeleton");
588
+ body.append("files", file);
589
+ if (target === "mask") {
590
+ setSkeletonMaskUploadPending(true);
591
+ } else {
592
+ setSkeletonUploadPending(true);
593
+ }
594
+ try {
595
+ const response = await fetch(`${appBasePath()}/api/assets/upload`, {
596
+ method: "POST",
597
+ body,
598
+ });
599
+ if (!response.ok) {
600
+ const errorBody = (await response.json().catch(() => null)) as {
601
+ error?: string;
602
+ } | null;
603
+ throw new Error(
604
+ errorBody?.error || `Upload failed (${response.status})`,
605
+ );
606
+ }
607
+ const result = (await response
608
+ .json()
609
+ .catch(() => null)) as AssetUploadResult | null;
610
+ const assetId = uploadedSkeletonAssetId(result);
611
+ if (!assetId) {
612
+ throw new Error(
613
+ result?.errors?.[0]?.message ??
614
+ (target === "mask"
615
+ ? t("brandKitDetail.couldNotUploadSkeletonMask")
616
+ : t("brandKitDetail.couldNotUploadSkeletonImage")),
617
+ );
618
+ }
619
+ updateForm(
620
+ target === "mask"
621
+ ? {
622
+ skeletonMaskAssetId: assetId,
623
+ skeletonMaskPreviewUrl: localPreviewUrl,
624
+ }
625
+ : {
626
+ skeletonBackgroundAssetId: assetId,
627
+ skeletonBackgroundPreviewUrl: localPreviewUrl,
628
+ },
629
+ );
630
+ toast.success(
631
+ target === "mask"
632
+ ? t("brandKitDetail.skeletonMaskUploaded")
633
+ : t("brandKitDetail.skeletonImageUploaded"),
634
+ );
635
+ } catch (error) {
636
+ URL.revokeObjectURL(localPreviewUrl);
637
+ toast.error(
638
+ error instanceof Error
639
+ ? error.message
640
+ : target === "mask"
641
+ ? t("brandKitDetail.couldNotUploadSkeletonMask")
642
+ : t("brandKitDetail.couldNotUploadSkeletonImage"),
643
+ );
644
+ } finally {
645
+ if (target === "mask") {
646
+ setSkeletonMaskUploadPending(false);
647
+ if (skeletonMaskFileInputRef.current) {
648
+ skeletonMaskFileInputRef.current.value = "";
649
+ }
650
+ } else {
651
+ setSkeletonUploadPending(false);
652
+ if (skeletonFileInputRef.current) {
653
+ skeletonFileInputRef.current.value = "";
654
+ }
655
+ }
656
+ }
657
+ }
658
+
659
+ async function save() {
660
+ if (!preset || !form || readOnly || updatePreset.isPending) return;
661
+ const normalized = normalizedForm(form);
662
+ if (!normalized.title) return;
663
+ if (normalized.skeletonEnabled && !normalized.skeletonBackgroundAssetId) {
664
+ toast.error(t("brandKitDetail.skeletonImageRequired"));
665
+ return;
666
+ }
667
+ try {
668
+ const saved = await updatePreset.mutateAsync({
669
+ id: preset.id,
670
+ title: normalized.title,
671
+ description: normalized.description || null,
672
+ category: normalized.category,
673
+ promptTemplate: normalized.promptTemplate || null,
674
+ aspectRatio: normalized.aspectRatio,
675
+ imageSize: normalized.imageSize,
676
+ model: normalized.model,
677
+ textPolicy: normalized.textPolicy,
678
+ referencePolicy: normalized.referencePolicy,
679
+ includeLogo: normalized.includeLogo,
680
+ settings: {
681
+ skeletonSpec: buildSkeletonSpec(
682
+ normalized,
683
+ skeletonFromPreset(preset),
684
+ ),
685
+ },
686
+ collectionId: normalized.collectionId,
687
+ sortOrder: Number(normalized.sortOrder),
688
+ });
689
+ const next = formFromPreset(saved, assets);
690
+ if (
691
+ normalized.skeletonBackgroundAssetId &&
692
+ !next.skeletonBackgroundPreviewUrl
693
+ ) {
694
+ next.skeletonBackgroundPreviewUrl = form.skeletonBackgroundPreviewUrl;
695
+ }
696
+ if (normalized.skeletonMaskAssetId && !next.skeletonMaskPreviewUrl) {
697
+ next.skeletonMaskPreviewUrl = form.skeletonMaskPreviewUrl;
698
+ }
699
+ setForm(next);
700
+ setInitialForm(next);
701
+ toast.success(t("brandKitDetail.generationPresetSaved"));
702
+ } catch (error) {
703
+ toast.error(
704
+ error instanceof Error
705
+ ? error.message
706
+ : t("brandKitDetail.couldNotSavePreset"),
707
+ );
708
+ }
709
+ }
710
+
711
+ async function deleteCurrentPreset() {
712
+ if (!preset || readOnly || deletePreset.isPending) return;
713
+ try {
714
+ await deletePreset.mutateAsync({ id: preset.id });
715
+ toast.success(t("brandKitDetail.generationPresetDeleted"));
716
+ navigate(settingsHref);
717
+ } catch (error) {
718
+ toast.error(
719
+ error instanceof Error
720
+ ? error.message
721
+ : t("brandKitDetail.couldNotDeletePreset"),
722
+ );
723
+ }
724
+ }
725
+
726
+ if (loading) {
727
+ return (
728
+ <div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-6 py-6">
729
+ <Skeleton className="h-10 w-56" />
730
+ <Skeleton className="h-28 w-full" />
731
+ <Skeleton className="h-96 w-full" />
732
+ </div>
733
+ );
734
+ }
735
+
736
+ if (!library || !preset) {
737
+ return (
738
+ <div className="mx-auto flex w-full max-w-3xl flex-col gap-4 px-6 py-10">
739
+ <Button variant="ghost" className="w-fit gap-2" asChild>
740
+ <Link to="/library">
741
+ <IconArrowLeft className="h-4 w-4" />
742
+ {t("brandKitDetail.backToLibrary")}
743
+ </Link>
744
+ </Button>
745
+ <Alert>
746
+ <AlertTitle>{t("brandKitDetail.presetUnavailableTitle")}</AlertTitle>
747
+ <AlertDescription>
748
+ {t("brandKitDetail.presetUnavailableDescription")}
749
+ </AlertDescription>
750
+ </Alert>
751
+ </div>
752
+ );
753
+ }
754
+
755
+ if (!form) {
756
+ return (
757
+ <div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-6 py-6">
758
+ <Skeleton className="h-10 w-56" />
759
+ <Skeleton className="h-28 w-full" />
760
+ <Skeleton className="h-96 w-full" />
761
+ </div>
762
+ );
763
+ }
764
+
765
+ return (
766
+ <div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-6 py-6">
767
+ <div className="flex flex-col gap-4 border-b border-border pb-5 md:flex-row md:items-start md:justify-between">
768
+ <div className="min-w-0">
769
+ <Button variant="ghost" className="-ms-3 mb-3 gap-2" asChild>
770
+ <Link to={settingsHref}>
771
+ <IconArrowLeft className="h-4 w-4" />
772
+ {t("brandKitDetail.backToSettings")}
773
+ </Link>
774
+ </Button>
775
+ <div className="flex flex-wrap items-center gap-2">
776
+ <h1 className="truncate text-2xl font-semibold tracking-tight">
777
+ {form.title || t("brandKitDetail.editGenerationPreset")}
778
+ </h1>
779
+ <Badge variant="outline">{library.title}</Badge>
780
+ {readOnly ? (
781
+ <Badge variant="secondary" className="gap-1">
782
+ <IconLock className="h-3.5 w-3.5" />
783
+ {t("brandKitDetail.readOnly")}
784
+ </Badge>
785
+ ) : null}
786
+ </div>
787
+ <p className="mt-2 max-w-3xl text-sm text-muted-foreground">
788
+ {t("brandKitDetail.editGenerationPresetDescription")}
789
+ </p>
790
+ </div>
791
+ <div className="flex shrink-0 flex-wrap items-center gap-2">
792
+ <Button
793
+ variant="outline"
794
+ className="gap-2 text-destructive hover:text-destructive"
795
+ disabled={readOnly || deletePreset.isPending}
796
+ onClick={() => setDeleteOpen(true)}
797
+ >
798
+ <IconTrash className="h-4 w-4" />
799
+ {t("brandKitDetail.delete")}
800
+ </Button>
801
+ <Button
802
+ className="gap-2"
803
+ disabled={
804
+ readOnly ||
805
+ updatePreset.isPending ||
806
+ !dirty ||
807
+ !form.title.trim() ||
808
+ (form.skeletonEnabled && !form.skeletonBackgroundAssetId)
809
+ }
810
+ onClick={save}
811
+ >
812
+ {updatePreset.isPending ? (
813
+ <Spinner className="h-4 w-4" />
814
+ ) : (
815
+ <IconDeviceFloppy className="h-4 w-4" />
816
+ )}
817
+ {updatePreset.isPending
818
+ ? t("brandKitDetail.saving")
819
+ : t("brandKitDetail.saveChanges")}
820
+ </Button>
821
+ </div>
822
+ </div>
823
+
824
+ {readOnly ? (
825
+ <Alert>
826
+ <IconLock className="h-4 w-4" />
827
+ <AlertTitle>{t("brandKitDetail.viewerModeTitle")}</AlertTitle>
828
+ <AlertDescription>
829
+ {t("brandKitDetail.viewerModeDescription")}
830
+ </AlertDescription>
831
+ </Alert>
832
+ ) : null}
833
+
834
+ <div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_18rem]">
835
+ <section className="grid gap-5">
836
+ <div className="grid gap-2">
837
+ <FieldLabel htmlFor="preset-title">
838
+ {t("brandKitDetail.name")}
839
+ </FieldLabel>
840
+ <Input
841
+ id="preset-title"
842
+ value={form.title}
843
+ disabled={readOnly}
844
+ onChange={(event) => updateForm({ title: event.target.value })}
845
+ placeholder={t("brandKitDetail.campaignLaunch")}
846
+ />
847
+ </div>
848
+ <div className="grid gap-2">
849
+ <FieldLabel htmlFor="preset-description">
850
+ {t("brandKitDetail.description")}
851
+ </FieldLabel>
852
+ <Textarea
853
+ id="preset-description"
854
+ value={form.description}
855
+ disabled={readOnly}
856
+ onChange={(event) =>
857
+ updateForm({ description: event.target.value })
858
+ }
859
+ placeholder={t("brandKitDetail.presetDescriptionPlaceholder")}
860
+ />
861
+ </div>
862
+ <div className="grid gap-4 sm:grid-cols-2">
863
+ <div className="grid gap-2">
864
+ <FieldLabel>{t("brandKitDetail.category")}</FieldLabel>
865
+ <Select
866
+ value={form.category}
867
+ disabled={readOnly}
868
+ onValueChange={(value) =>
869
+ updateForm({ category: value as ImageCategory })
870
+ }
871
+ >
872
+ <SelectTrigger>
873
+ <SelectValue />
874
+ </SelectTrigger>
875
+ <SelectContent>
876
+ {IMAGE_CATEGORIES.map((item) => (
877
+ <SelectItem key={item} value={item}>
878
+ {item}
879
+ </SelectItem>
880
+ ))}
881
+ </SelectContent>
882
+ </Select>
883
+ </div>
884
+ <div className="grid gap-2">
885
+ <FieldLabel>{t("brandKitDetail.aspectRatio")}</FieldLabel>
886
+ <Select
887
+ value={form.aspectRatio}
888
+ disabled={readOnly}
889
+ onValueChange={(value) =>
890
+ updateForm({ aspectRatio: value as AspectRatio })
891
+ }
892
+ >
893
+ <SelectTrigger>
894
+ <SelectValue />
895
+ </SelectTrigger>
896
+ <SelectContent>
897
+ {supportedRatios.map((ratio) => (
898
+ <SelectItem key={ratio} value={ratio}>
899
+ {ratio}
900
+ </SelectItem>
901
+ ))}
902
+ </SelectContent>
903
+ </Select>
904
+ </div>
905
+ </div>
906
+ <div className="grid gap-2">
907
+ <FieldLabel htmlFor="preset-template">
908
+ {t("brandKitDetail.promptTemplate")}
909
+ </FieldLabel>
910
+ <Textarea
911
+ id="preset-template"
912
+ className="min-h-36"
913
+ value={form.promptTemplate}
914
+ disabled={readOnly}
915
+ onChange={(event) =>
916
+ updateForm({ promptTemplate: event.target.value })
917
+ }
918
+ placeholder={t("library.promptTemplatePlaceholder")}
919
+ />
920
+ </div>
921
+ <div className="grid gap-2">
922
+ <FieldLabel htmlFor="preset-text-policy">
923
+ {t("brandKitDetail.textPolicy")}
924
+ </FieldLabel>
925
+ <Textarea
926
+ id="preset-text-policy"
927
+ value={form.textPolicy}
928
+ disabled={readOnly}
929
+ onChange={(event) =>
930
+ updateForm({ textPolicy: event.target.value })
931
+ }
932
+ placeholder={t("brandKitDetail.defaultTextPolicy")}
933
+ />
934
+ </div>
935
+ <label
936
+ htmlFor="preset-include-logo"
937
+ className={cn(
938
+ "flex items-start gap-3 rounded-md border border-border p-3",
939
+ readOnly && "opacity-70",
940
+ )}
941
+ >
942
+ <Checkbox
943
+ id="preset-include-logo"
944
+ checked={form.includeLogo}
945
+ disabled={readOnly}
946
+ onCheckedChange={(checked) =>
947
+ updateForm({ includeLogo: checked === true })
948
+ }
949
+ className="mt-0.5"
950
+ />
951
+ <span className="grid gap-1">
952
+ <span className="text-sm font-medium leading-none">
953
+ {t("brandKitDetail.compositeCanonicalLogo")}
954
+ </span>
955
+ <span className="text-xs text-muted-foreground">
956
+ {t("brandKitDetail.compositeCanonicalLogoHint")}
957
+ </span>
958
+ </span>
959
+ </label>
960
+ <div className="grid gap-4 rounded-md border border-border p-4">
961
+ <label
962
+ htmlFor="preset-skeleton-enabled"
963
+ className={cn("flex items-start gap-3", readOnly && "opacity-70")}
964
+ >
965
+ <Checkbox
966
+ id="preset-skeleton-enabled"
967
+ checked={form.skeletonEnabled}
968
+ disabled={readOnly}
969
+ onCheckedChange={(checked) =>
970
+ updateSkeletonEnabled(checked === true)
971
+ }
972
+ className="mt-0.5"
973
+ />
974
+ <span className="grid gap-1">
975
+ <span className="text-sm font-medium leading-none">
976
+ {t("brandKitDetail.presetSkeleton")}
977
+ </span>
978
+ <span className="text-xs text-muted-foreground">
979
+ {t("brandKitDetail.presetSkeletonHint")}
980
+ </span>
981
+ </span>
982
+ </label>
983
+ {form.skeletonEnabled ? (
984
+ <div className="grid gap-4 sm:grid-cols-2">
985
+ <div className="grid gap-2">
986
+ <FieldLabel>{t("brandKitDetail.contentMode")}</FieldLabel>
987
+ <Select
988
+ value={form.skeletonContentMode}
989
+ disabled={readOnly}
990
+ onValueChange={(value) =>
991
+ updateSkeletonContentMode(value as SkeletonContentMode)
992
+ }
993
+ >
994
+ <SelectTrigger>
995
+ <SelectValue />
996
+ </SelectTrigger>
997
+ <SelectContent>
998
+ <SelectItem value="fill">
999
+ {t("brandKitDetail.contentModeFill")}
1000
+ </SelectItem>
1001
+ <SelectItem value="cutout">
1002
+ {t("brandKitDetail.contentModeCutout")}
1003
+ </SelectItem>
1004
+ </SelectContent>
1005
+ </Select>
1006
+ </div>
1007
+ <div className="grid gap-3 sm:col-span-2">
1008
+ <FieldLabel>{t("brandKitDetail.skeletonImage")}</FieldLabel>
1009
+ <div className="grid gap-3 sm:grid-cols-[minmax(0,1fr)_16rem]">
1010
+ <button
1011
+ type="button"
1012
+ className={cn(
1013
+ "relative aspect-video overflow-hidden rounded-md border border-dashed border-border bg-muted text-left transition-colors hover:border-foreground/40 disabled:cursor-not-allowed disabled:opacity-70",
1014
+ form.skeletonBackgroundPreviewUrl && "border-solid",
1015
+ )}
1016
+ disabled={readOnly || skeletonUploadPending}
1017
+ onClick={() => skeletonFileInputRef.current?.click()}
1018
+ >
1019
+ {form.skeletonBackgroundPreviewUrl ? (
1020
+ <img
1021
+ src={form.skeletonBackgroundPreviewUrl}
1022
+ alt=""
1023
+ className="absolute inset-0 h-full w-full object-cover"
1024
+ />
1025
+ ) : (
1026
+ <span className="absolute inset-0 flex flex-col items-center justify-center gap-2 px-4 text-center text-sm text-muted-foreground">
1027
+ <IconPhotoPlus className="h-7 w-7" />
1028
+ <span>{t("brandKitDetail.uploadSkeletonImage")}</span>
1029
+ </span>
1030
+ )}
1031
+ </button>
1032
+ <div className="grid content-start gap-2">
1033
+ <input
1034
+ ref={skeletonFileInputRef}
1035
+ type="file"
1036
+ accept="image/*"
1037
+ className="hidden"
1038
+ disabled={readOnly || skeletonUploadPending}
1039
+ onChange={(event) =>
1040
+ void uploadSkeletonImage(event.target.files)
1041
+ }
1042
+ />
1043
+ <Button
1044
+ type="button"
1045
+ variant="outline"
1046
+ className="gap-2"
1047
+ disabled={readOnly || skeletonUploadPending}
1048
+ onClick={() => skeletonFileInputRef.current?.click()}
1049
+ >
1050
+ {skeletonUploadPending ? (
1051
+ <Spinner className="h-4 w-4" />
1052
+ ) : (
1053
+ <IconUpload className="h-4 w-4" />
1054
+ )}
1055
+ {skeletonUploadPending
1056
+ ? t("brandKitDetail.uploadingSkeletonImage")
1057
+ : t("brandKitDetail.uploadSkeletonImage")}
1058
+ </Button>
1059
+ <Select
1060
+ value={
1061
+ form.skeletonBackgroundAssetId ||
1062
+ NO_SKELETON_BACKGROUND
1063
+ }
1064
+ disabled={readOnly || !skeletonBackgroundAssets.length}
1065
+ onValueChange={updateSkeletonBackgroundAsset}
1066
+ >
1067
+ <SelectTrigger>
1068
+ <SelectValue
1069
+ placeholder={t(
1070
+ "brandKitDetail.chooseSkeletonImage",
1071
+ )}
1072
+ />
1073
+ </SelectTrigger>
1074
+ <SelectContent>
1075
+ <SelectItem value={NO_SKELETON_BACKGROUND}>
1076
+ {t("brandKitDetail.noSkeletonImage")}
1077
+ </SelectItem>
1078
+ {skeletonBackgroundAssets.map((asset: any) => (
1079
+ <SelectItem key={asset.id} value={asset.id}>
1080
+ {asset.title || asset.id}
1081
+ </SelectItem>
1082
+ ))}
1083
+ </SelectContent>
1084
+ </Select>
1085
+ <p className="text-xs text-muted-foreground">
1086
+ {t("brandKitDetail.skeletonImageHint")}
1087
+ </p>
1088
+ </div>
1089
+ </div>
1090
+ </div>
1091
+ {form.skeletonContentMode === "cutout" &&
1092
+ form.model === "gpt-image-2" ? (
1093
+ <div className="grid gap-3 sm:col-span-2">
1094
+ <FieldLabel>{t("brandKitDetail.skeletonMask")}</FieldLabel>
1095
+ <div className="grid gap-3 sm:grid-cols-[minmax(0,1fr)_16rem]">
1096
+ <button
1097
+ type="button"
1098
+ className={cn(
1099
+ "relative aspect-video overflow-hidden rounded-md border border-dashed border-border bg-muted text-left transition-colors hover:border-foreground/40 disabled:cursor-not-allowed disabled:opacity-70",
1100
+ form.skeletonMaskPreviewUrl && "border-solid",
1101
+ )}
1102
+ disabled={readOnly || skeletonMaskUploadPending}
1103
+ onClick={() =>
1104
+ skeletonMaskFileInputRef.current?.click()
1105
+ }
1106
+ >
1107
+ {form.skeletonMaskPreviewUrl ? (
1108
+ <img
1109
+ src={form.skeletonMaskPreviewUrl}
1110
+ alt=""
1111
+ className="absolute inset-0 h-full w-full object-cover"
1112
+ />
1113
+ ) : (
1114
+ <span className="absolute inset-0 flex flex-col items-center justify-center gap-2 px-4 text-center text-sm text-muted-foreground">
1115
+ <IconPhotoPlus className="h-7 w-7" />
1116
+ <span>
1117
+ {t("brandKitDetail.uploadSkeletonMask")}
1118
+ </span>
1119
+ </span>
1120
+ )}
1121
+ </button>
1122
+ <div className="grid content-start gap-2">
1123
+ <input
1124
+ ref={skeletonMaskFileInputRef}
1125
+ type="file"
1126
+ accept="image/*"
1127
+ className="hidden"
1128
+ disabled={readOnly || skeletonMaskUploadPending}
1129
+ onChange={(event) =>
1130
+ void uploadSkeletonImage(event.target.files, "mask")
1131
+ }
1132
+ />
1133
+ <Button
1134
+ type="button"
1135
+ variant="outline"
1136
+ className="gap-2"
1137
+ disabled={readOnly || skeletonMaskUploadPending}
1138
+ onClick={() =>
1139
+ skeletonMaskFileInputRef.current?.click()
1140
+ }
1141
+ >
1142
+ {skeletonMaskUploadPending ? (
1143
+ <Spinner className="h-4 w-4" />
1144
+ ) : (
1145
+ <IconUpload className="h-4 w-4" />
1146
+ )}
1147
+ {skeletonMaskUploadPending
1148
+ ? t("brandKitDetail.uploadingSkeletonMask")
1149
+ : t("brandKitDetail.uploadSkeletonMask")}
1150
+ </Button>
1151
+ <Select
1152
+ value={form.skeletonMaskAssetId || NO_SKELETON_MASK}
1153
+ disabled={
1154
+ readOnly || !skeletonBackgroundAssets.length
1155
+ }
1156
+ onValueChange={updateSkeletonMaskAsset}
1157
+ >
1158
+ <SelectTrigger>
1159
+ <SelectValue
1160
+ placeholder={t(
1161
+ "brandKitDetail.chooseSkeletonMask",
1162
+ )}
1163
+ />
1164
+ </SelectTrigger>
1165
+ <SelectContent>
1166
+ <SelectItem value={NO_SKELETON_MASK}>
1167
+ {t("brandKitDetail.noSkeletonMask")}
1168
+ </SelectItem>
1169
+ {skeletonBackgroundAssets.map((asset: any) => (
1170
+ <SelectItem key={asset.id} value={asset.id}>
1171
+ {asset.title || asset.id}
1172
+ </SelectItem>
1173
+ ))}
1174
+ </SelectContent>
1175
+ </Select>
1176
+ <p className="text-xs text-muted-foreground">
1177
+ {t("brandKitDetail.skeletonMaskHint")}
1178
+ </p>
1179
+ </div>
1180
+ </div>
1181
+ </div>
1182
+ ) : null}
1183
+ {form.skeletonContentMode === "cutout" ? (
1184
+ <label
1185
+ htmlFor="preset-skeleton-shadow"
1186
+ className={cn(
1187
+ "flex items-start gap-3 rounded-md border border-border p-3 sm:col-span-2",
1188
+ readOnly && "opacity-70",
1189
+ )}
1190
+ >
1191
+ <Checkbox
1192
+ id="preset-skeleton-shadow"
1193
+ checked={form.skeletonDropShadow}
1194
+ disabled={readOnly}
1195
+ onCheckedChange={(checked) =>
1196
+ updateForm({ skeletonDropShadow: checked === true })
1197
+ }
1198
+ className="mt-0.5"
1199
+ />
1200
+ <span className="grid gap-1">
1201
+ <span className="text-sm font-medium leading-none">
1202
+ {t("brandKitDetail.contactShadow")}
1203
+ </span>
1204
+ <span className="text-xs text-muted-foreground">
1205
+ {t("brandKitDetail.contactShadowHint")}
1206
+ </span>
1207
+ </span>
1208
+ </label>
1209
+ ) : null}
1210
+ <label
1211
+ htmlFor="preset-skeleton-logo"
1212
+ className={cn(
1213
+ "flex items-start gap-3 rounded-md border border-border p-3",
1214
+ readOnly && "opacity-70",
1215
+ )}
1216
+ >
1217
+ <Checkbox
1218
+ id="preset-skeleton-logo"
1219
+ checked={form.skeletonLogo}
1220
+ disabled={readOnly || !library?.canonicalLogoAssetId}
1221
+ onCheckedChange={(checked) =>
1222
+ updateForm({ skeletonLogo: checked === true })
1223
+ }
1224
+ className="mt-0.5"
1225
+ />
1226
+ <span className="grid gap-1">
1227
+ <span className="text-sm font-medium leading-none">
1228
+ {t("brandKitDetail.placeLogoInSkeleton")}
1229
+ </span>
1230
+ <span className="text-xs text-muted-foreground">
1231
+ {t("brandKitDetail.placeLogoInSkeletonHint")}
1232
+ </span>
1233
+ </span>
1234
+ </label>
1235
+ <div className="grid gap-2">
1236
+ <FieldLabel>{t("brandKitDetail.logoPlacement")}</FieldLabel>
1237
+ <Select
1238
+ value={form.skeletonLogoPlacement}
1239
+ disabled={readOnly || !form.skeletonLogo}
1240
+ onValueChange={(value) =>
1241
+ updateForm({
1242
+ skeletonLogoPlacement: value as SkeletonLogoPlacement,
1243
+ })
1244
+ }
1245
+ >
1246
+ <SelectTrigger>
1247
+ <SelectValue />
1248
+ </SelectTrigger>
1249
+ <SelectContent>
1250
+ <SelectItem value="upper-right">
1251
+ {t("brandKitDetail.upperRight")}
1252
+ </SelectItem>
1253
+ <SelectItem value="upper-left">
1254
+ {t("brandKitDetail.upperLeft")}
1255
+ </SelectItem>
1256
+ <SelectItem value="lower-right">
1257
+ {t("brandKitDetail.lowerRight")}
1258
+ </SelectItem>
1259
+ <SelectItem value="lower-left">
1260
+ {t("brandKitDetail.lowerLeft")}
1261
+ </SelectItem>
1262
+ </SelectContent>
1263
+ </Select>
1264
+ </div>
1265
+ </div>
1266
+ ) : null}
1267
+ </div>
1268
+
1269
+ <Separator />
1270
+
1271
+ <div className="grid gap-4">
1272
+ <Button
1273
+ type="button"
1274
+ variant="ghost"
1275
+ className="w-fit gap-2 px-0"
1276
+ onClick={() => setAdvancedOpen((current) => !current)}
1277
+ >
1278
+ <IconChevronDown
1279
+ className={cn(
1280
+ "h-4 w-4 transition-transform",
1281
+ advancedOpen && "rotate-180",
1282
+ )}
1283
+ />
1284
+ {advancedOpen
1285
+ ? t("brandKitDetail.hideAdvancedOptions")
1286
+ : t("brandKitDetail.showAdvancedOptions")}
1287
+ </Button>
1288
+ {advancedOpen ? (
1289
+ <div className="grid gap-4 rounded-md border border-border p-4 sm:grid-cols-2">
1290
+ <div className="grid gap-2">
1291
+ <FieldLabel>{t("brandKitDetail.model")}</FieldLabel>
1292
+ <Select
1293
+ value={form.model}
1294
+ disabled={readOnly}
1295
+ onValueChange={(value) => updateModel(value as ImageModel)}
1296
+ >
1297
+ <SelectTrigger>
1298
+ <SelectValue />
1299
+ </SelectTrigger>
1300
+ <SelectContent>
1301
+ {modelOptions.map((item) => (
1302
+ <SelectItem key={item} value={item}>
1303
+ {item}
1304
+ </SelectItem>
1305
+ ))}
1306
+ </SelectContent>
1307
+ </Select>
1308
+ </div>
1309
+ <div className="grid gap-2">
1310
+ <FieldLabel>{t("brandKitDetail.imageSize")}</FieldLabel>
1311
+ <Select
1312
+ value={form.imageSize}
1313
+ disabled={readOnly}
1314
+ onValueChange={(value) =>
1315
+ updateForm({ imageSize: value as ImageSize })
1316
+ }
1317
+ >
1318
+ <SelectTrigger>
1319
+ <SelectValue />
1320
+ </SelectTrigger>
1321
+ <SelectContent>
1322
+ {IMAGE_SIZES.map((item) => (
1323
+ <SelectItem key={item} value={item}>
1324
+ {item}
1325
+ </SelectItem>
1326
+ ))}
1327
+ </SelectContent>
1328
+ </Select>
1329
+ </div>
1330
+ <div className="grid gap-2">
1331
+ <FieldLabel>{t("brandKitDetail.referencePolicy")}</FieldLabel>
1332
+ <Select
1333
+ value={form.referencePolicy}
1334
+ disabled={readOnly}
1335
+ onValueChange={(value) =>
1336
+ updateForm({
1337
+ referencePolicy:
1338
+ value as GenerationPresetReferencePolicy,
1339
+ })
1340
+ }
1341
+ >
1342
+ <SelectTrigger>
1343
+ <SelectValue />
1344
+ </SelectTrigger>
1345
+ <SelectContent>
1346
+ {GENERATION_PRESET_REFERENCE_POLICIES.map((item) => (
1347
+ <SelectItem key={item} value={item}>
1348
+ {item}
1349
+ </SelectItem>
1350
+ ))}
1351
+ </SelectContent>
1352
+ </Select>
1353
+ </div>
1354
+ <div className="grid gap-2">
1355
+ <FieldLabel>{t("brandKitDetail.collection")}</FieldLabel>
1356
+ <Select
1357
+ value={form.collectionId ?? NO_COLLECTION}
1358
+ disabled={readOnly}
1359
+ onValueChange={(value) =>
1360
+ updateForm({
1361
+ collectionId: value === NO_COLLECTION ? null : value,
1362
+ })
1363
+ }
1364
+ >
1365
+ <SelectTrigger>
1366
+ <SelectValue />
1367
+ </SelectTrigger>
1368
+ <SelectContent>
1369
+ <SelectItem value={NO_COLLECTION}>
1370
+ {t("brandKitDetail.noCollection")}
1371
+ </SelectItem>
1372
+ {collections.map((collection: any) => (
1373
+ <SelectItem key={collection.id} value={collection.id}>
1374
+ {collection.title}
1375
+ </SelectItem>
1376
+ ))}
1377
+ </SelectContent>
1378
+ </Select>
1379
+ </div>
1380
+ <div className="grid gap-2">
1381
+ <FieldLabel htmlFor="preset-sort-order">
1382
+ {t("brandKitDetail.sortOrder")}
1383
+ </FieldLabel>
1384
+ <Input
1385
+ id="preset-sort-order"
1386
+ type="number"
1387
+ value={form.sortOrder}
1388
+ disabled={readOnly}
1389
+ onChange={(event) =>
1390
+ updateForm({ sortOrder: event.target.value })
1391
+ }
1392
+ />
1393
+ </div>
1394
+ </div>
1395
+ ) : null}
1396
+ </div>
1397
+ </section>
1398
+
1399
+ <aside className="h-fit rounded-md border border-border p-4">
1400
+ <div className="text-sm font-medium">
1401
+ {t("brandKitDetail.presetSummary")}
1402
+ </div>
1403
+ <dl className="mt-3 grid gap-3 text-sm">
1404
+ <div>
1405
+ <dt className="text-xs text-muted-foreground">
1406
+ {t("brandKitDetail.model")}
1407
+ </dt>
1408
+ <dd className="mt-1 break-words">{form.model}</dd>
1409
+ </div>
1410
+ <div className="grid grid-cols-2 gap-3">
1411
+ <div>
1412
+ <dt className="text-xs text-muted-foreground">
1413
+ {t("brandKitDetail.aspectRatio")}
1414
+ </dt>
1415
+ <dd className="mt-1">{form.aspectRatio}</dd>
1416
+ </div>
1417
+ <div>
1418
+ <dt className="text-xs text-muted-foreground">
1419
+ {t("brandKitDetail.imageSize")}
1420
+ </dt>
1421
+ <dd className="mt-1">{form.imageSize}</dd>
1422
+ </div>
1423
+ </div>
1424
+ <div>
1425
+ <dt className="text-xs text-muted-foreground">
1426
+ {t("brandKitDetail.referencePolicy")}
1427
+ </dt>
1428
+ <dd className="mt-1">{form.referencePolicy}</dd>
1429
+ </div>
1430
+ <div>
1431
+ <dt className="text-xs text-muted-foreground">
1432
+ {t("brandKitDetail.presetSkeleton")}
1433
+ </dt>
1434
+ <dd className="mt-1">
1435
+ {form.skeletonEnabled
1436
+ ? t("brandKitDetail.skeletonEnabled")
1437
+ : t("brandKitDetail.skeletonOff")}
1438
+ </dd>
1439
+ {form.skeletonEnabled ? <SkeletonPreview form={form} /> : null}
1440
+ </div>
1441
+ </dl>
1442
+ </aside>
1443
+ </div>
1444
+
1445
+ <AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
1446
+ <AlertDialogContent>
1447
+ <AlertDialogHeader>
1448
+ <AlertDialogTitle>
1449
+ {t("brandKitDetail.deleteGenerationPreset")}
1450
+ </AlertDialogTitle>
1451
+ <AlertDialogDescription>
1452
+ {t("brandKitDetail.deleteGenerationPresetDescription")}
1453
+ </AlertDialogDescription>
1454
+ </AlertDialogHeader>
1455
+ <AlertDialogFooter>
1456
+ <AlertDialogCancel>{t("brandKitDetail.cancel")}</AlertDialogCancel>
1457
+ <AlertDialogAction
1458
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
1459
+ disabled={deletePreset.isPending}
1460
+ onClick={(event) => {
1461
+ event.preventDefault();
1462
+ void deleteCurrentPreset();
1463
+ }}
1464
+ >
1465
+ {deletePreset.isPending
1466
+ ? t("brandKitDetail.deleting")
1467
+ : t("brandKitDetail.delete")}
1468
+ </AlertDialogAction>
1469
+ </AlertDialogFooter>
1470
+ </AlertDialogContent>
1471
+ </AlertDialog>
1472
+ </div>
1473
+ );
1474
+ }