@interware/content-generator 0.7.0 → 0.7.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.
@@ -5,8 +5,8 @@ import { P as PluginProviders } from "./PluginProviders-DWMpcAIh.mjs";
5
5
  import { useMemo, useState, useEffect, useRef, useCallback } from "react";
6
6
  import { Box, Flex, Typography, Button, Badge, TextInput, Loader, Table, Thead, Tr, Th, VisuallyHidden, Tbody, Td, TextButton, IconButton, Link, LinkButton, Field, Radio, Modal, Textarea, Combobox, ComboboxOption, Toggle, Grid } from "@strapi/design-system";
7
7
  import { Plus, Search, Check, Pencil, ExternalLink, ArrowLeft, Image, Clock, WarningCircle, Trash, Sparkle, PlusCircle } from "@strapi/icons";
8
- import { P as PLUGIN_ID, S as STUDIO_PATH, p as pluginPermissions } from "./index-D67xWUPz.mjs";
9
- import { L as LibraryImagePicker, P as PromptComposer, a as LEGACY_ROUTES, S as StudioRedirect, l as legacyRedirect } from "./ImageStudioTabs-B-mOwDfY.mjs";
8
+ import { P as PLUGIN_ID, S as STUDIO_PATH, p as pluginPermissions } from "./index-D6UAvh53.mjs";
9
+ import { L as LibraryImagePicker, P as PromptComposer, a as LEGACY_ROUTES, S as StudioRedirect, l as legacyRedirect } from "./ImageStudioTabs-Umf3GODW.mjs";
10
10
  import { useIntl } from "react-intl";
11
11
  import { useFormik } from "formik";
12
12
  import * as Yup from "yup";
@@ -7,8 +7,8 @@ const PluginProviders = require("./PluginProviders-D79TeGSE.js");
7
7
  const react = require("react");
8
8
  const designSystem = require("@strapi/design-system");
9
9
  const icons = require("@strapi/icons");
10
- const index = require("./index-NA9OuMGs.js");
11
- const ImageStudioTabs = require("./ImageStudioTabs-CG4KTn1S.js");
10
+ const index = require("./index-C2s9Jhcc.js");
11
+ const ImageStudioTabs = require("./ImageStudioTabs-BFEF9ggK.js");
12
12
  const reactIntl = require("react-intl");
13
13
  const formik = require("formik");
14
14
  const Yup = require("yup");
@@ -2,7 +2,7 @@
2
2
  const jsxRuntime = require("react/jsx-runtime");
3
3
  const designSystem = require("@strapi/design-system");
4
4
  const reactRouterDom = require("react-router-dom");
5
- const index = require("./index-NA9OuMGs.js");
5
+ const index = require("./index-C2s9Jhcc.js");
6
6
  const react = require("react");
7
7
  const icons = require("@strapi/icons");
8
8
  const styledComponents = require("styled-components");
@@ -740,6 +740,9 @@ const RemoveBackgroundButton = ({ src, onDone }) => {
740
740
  )
741
741
  ] });
742
742
  };
743
+ const IMAGE_MAX_MB = 30;
744
+ const IMAGE_MAX_BYTES = IMAGE_MAX_MB * 1024 * 1024;
745
+ const IMAGE_FORMATS_HINT = `PNG, JPG o WEBP de hasta ${IMAGE_MAX_MB} MB.`;
743
746
  const NameDialog = ({ open, title, label, initial = "", confirmLabel, validate, onClose, onConfirm }) => {
744
747
  const [name, setName] = react.useState(initial);
745
748
  const [error, setError] = react.useState(null);
@@ -1253,7 +1256,10 @@ const EditEmpty = ({ disabled, onUpload, onOpenLibrary, onOpenRecent }) => {
1253
1256
  style: { width: "100%", maxWidth: 600, border: `2px dashed ${over ? c.primary600 : c.neutral200}` },
1254
1257
  children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "center", gap: 3, style: { textAlign: "center" }, children: [
1255
1258
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "beta", tag: "h2", children: "Elige la imagen que quieres editar" }),
1256
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: "Arrástrala aquí, súbela o elígela de la biblioteca. PNG, JPG o WEBP de hasta 10 MB." }),
1259
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { textColor: "neutral600", children: [
1260
+ "Arrástrala aquí, súbela o elígela de la biblioteca. ",
1261
+ IMAGE_FORMATS_HINT
1262
+ ] }),
1257
1263
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 3, paddingTop: 2, children: [
1258
1264
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Upload, {}), disabled, onClick: () => input.current?.click(), children: "Subir imagen" }),
1259
1265
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Folder, {}), disabled, onClick: onOpenLibrary, children: "Elegir de la biblioteca" })
@@ -1328,7 +1334,7 @@ const ReferencePicker = ({
1328
1334
  onChange,
1329
1335
  onPickFromLibrary,
1330
1336
  label = "Imágenes de referencia",
1331
- hint = "PNG, JPG o WEBP de hasta 10 MB.",
1337
+ hint = IMAGE_FORMATS_HINT,
1332
1338
  noun = REFERENCES
1333
1339
  }) => {
1334
1340
  const input = react.useRef(null);
@@ -1522,8 +1528,8 @@ const GeneratorTab = () => {
1522
1528
  };
1523
1529
  const uploadBase = (file) => {
1524
1530
  if (!file) return;
1525
- if (!IMAGE_MIMES.includes(file.type) || file.size > 10 * 1024 * 1024) {
1526
- notify("warning", "Usa una imagen PNG, JPG o WEBP de hasta 10 MB.");
1531
+ if (!IMAGE_MIMES.includes(file.type) || file.size > IMAGE_MAX_BYTES) {
1532
+ notify("warning", `Usa una imagen ${IMAGE_FORMATS_HINT}`);
1527
1533
  return;
1528
1534
  }
1529
1535
  const reader = new FileReader();
@@ -1598,7 +1604,7 @@ const GeneratorTab = () => {
1598
1604
  ReferencePicker,
1599
1605
  {
1600
1606
  label: "Referencias de estilo",
1601
- hint: "Guían el estilo, no se copian. PNG, JPG o WEBP de hasta 10 MB.",
1607
+ hint: `Guían el estilo, no se copian. ${IMAGE_FORMATS_HINT}`,
1602
1608
  files: references,
1603
1609
  max: model?.maxReferences ?? 0,
1604
1610
  modelLabel: model?.label ?? "Este modelo",
@@ -2187,17 +2193,6 @@ const LibraryTab = () => {
2187
2193
  /* @__PURE__ */ jsxRuntime.jsx(RemoveBackgroundDialog, { open: !!bgTarget, src: bgTarget?.src ?? "", onClose: () => setBgTarget(null), onApply: saveNoBackground })
2188
2194
  ] });
2189
2195
  };
2190
- const PALETTE = [
2191
- { label: "Navy", value: "#1F2A44" },
2192
- { label: "Rojo", value: "#E4002B" },
2193
- { label: "Teal", value: "#00C7B1" },
2194
- { label: "Teal suave", value: "#92CFC7" },
2195
- { label: "Amarillo", value: "#FFC844" },
2196
- { label: "Amarillo suave", value: "#F8DF8D" },
2197
- { label: "Gris", value: "#5B6473" },
2198
- { label: "Papel", value: "#EEF1F5" },
2199
- { label: "Blanco", value: "#FFFFFF" }
2200
- ];
2201
2196
  const NAVY = "#1F2A44";
2202
2197
  const RED$1 = "#E4002B";
2203
2198
  const TEAL$1 = "#00C7B1";
@@ -3937,44 +3932,63 @@ const SaveTemplateDialog = ({ open, defaultName, defaultFolder, folders, onClose
3937
3932
  ] })
3938
3933
  ] }) });
3939
3934
  };
3935
+ const parseHex = (raw) => {
3936
+ const hex = raw.trim().replace(/^#/, "");
3937
+ if (/^[0-9a-f]{3}$/i.test(hex)) return `#${[...hex].map((c) => c + c).join("")}`.toUpperCase();
3938
+ if (/^[0-9a-f]{6}$/i.test(hex)) return `#${hex}`.toUpperCase();
3939
+ return null;
3940
+ };
3940
3941
  const ColorField = ({ label, value, onChange }) => {
3941
3942
  const tc = useThemeColors();
3943
+ const current = parseHex(value ?? "") ?? "#000000";
3944
+ const [draft, setDraft] = react.useState(current);
3945
+ react.useEffect(() => setDraft((d) => parseHex(d) === current ? d : current), [current]);
3946
+ const apply = (text) => {
3947
+ const hex = parseHex(text);
3948
+ if (hex && hex !== current) onChange(hex);
3949
+ };
3950
+ const typeHex = (text) => {
3951
+ setDraft(text);
3952
+ if (text.trim().replace(/^#/, "").length === 6) apply(text);
3953
+ };
3954
+ const leave = () => {
3955
+ apply(draft);
3956
+ setDraft(parseHex(draft) ?? current);
3957
+ };
3942
3958
  return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 1, children: [
3943
3959
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", children: label }),
3944
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, wrap: "wrap", alignItems: "center", children: [
3945
- PALETTE.map((c) => {
3946
- const active = value?.toUpperCase() === c.value;
3947
- return /* @__PURE__ */ jsxRuntime.jsx(
3948
- designSystem.Box,
3949
- {
3950
- tag: "button",
3951
- color: "neutral800",
3952
- type: "button",
3953
- "aria-label": `${label}: ${c.label}`,
3954
- "aria-pressed": active,
3955
- title: c.label,
3956
- onClick: () => onChange(c.value),
3957
- style: {
3958
- width: 22,
3959
- height: 22,
3960
- borderRadius: 4,
3961
- cursor: "pointer",
3962
- background: c.value,
3963
- border: active ? `2px solid ${tc.primary600}` : `1px solid ${tc.neutral200}`,
3964
- padding: 0
3965
- }
3966
- },
3967
- c.value
3968
- );
3969
- }),
3960
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
3970
3961
  /* @__PURE__ */ jsxRuntime.jsx(
3971
3962
  "input",
3972
3963
  {
3973
3964
  type: "color",
3974
- "aria-label": `${label} personalizado`,
3975
- value: /^#[0-9a-f]{6}$/i.test(value ?? "") ? value : "#000000",
3965
+ "aria-label": `${label}: selector`,
3966
+ title: "Abrir selector de color",
3967
+ value: current.toLowerCase(),
3976
3968
  onChange: (e) => onChange(e.target.value.toUpperCase()),
3977
- style: { width: 26, height: 26, border: "none", padding: 0, background: "none", cursor: "pointer" }
3969
+ style: {
3970
+ flexShrink: 0,
3971
+ width: 32,
3972
+ height: 32,
3973
+ padding: 2,
3974
+ borderRadius: 4,
3975
+ cursor: "pointer",
3976
+ border: `1px solid ${tc.neutral200}`,
3977
+ background: tc.neutral0
3978
+ }
3979
+ }
3980
+ ),
3981
+ /* @__PURE__ */ jsxRuntime.jsx(
3982
+ designSystem.TextInput,
3983
+ {
3984
+ "aria-label": `${label}: hexadecimal`,
3985
+ size: "S",
3986
+ value: draft,
3987
+ placeholder: "#1F2A44",
3988
+ maxLength: 7,
3989
+ spellCheck: false,
3990
+ onChange: (e) => typeHex(e.target.value),
3991
+ onBlur: leave
3978
3992
  }
3979
3993
  )
3980
3994
  ] })
@@ -1,7 +1,7 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
  import { Modal, Flex, Button, Loader, Typography, Grid, Box, lightTheme, Checkbox, Field, TextInput, Dialog, SingleSelect, SingleSelectOption, Link, Badge, IconButtonGroup, IconButton, Toggle, Menu, NumberInput, Divider, Textarea, Tabs } from "@strapi/design-system";
3
3
  import { useLocation, Navigate, useSearchParams } from "react-router-dom";
4
- import { S as STUDIO_PATH } from "./index-D67xWUPz.mjs";
4
+ import { S as STUDIO_PATH } from "./index-D6UAvh53.mjs";
5
5
  import { useMemo, useState, useEffect, useId, useRef, useReducer, useCallback, useSyncExternalStore, useLayoutEffect, memo, forwardRef } from "react";
6
6
  import { ChevronRight, Folder, Magic, Pencil, Upload, Sparkle, Plus, Cross, Download, ArrowClockwise, House, ChevronDown, FilePdf, File as File$1, More, Trash, User, Star, SquaresFour, Quotes, PriceTag, Paragraph, NumberList, Minus, Italic, Image as Image$1, HeadingTwo, HeadingOne, Hashtag, Feather, Drag, BulletList, ArrowLeft, Check, ArrowsCounterClockwise, ArrowLineLeft, ArrowLineRight, Expand, WarningCircle, Play, Phone, Mail, PinMap, Earth, PresentationChart, ChartPie, ChartBubble, Cog, Database, Cloud, Globe, Server, Lightbulb, Crown, SealCheck, Briefcase, Rocket, Shield, CheckCircle, Lightning, Heart, Collapse, Bold, ArrowUp, ArrowDown, Duplicate, Lock, EyeStriked, Eye } from "@strapi/icons";
7
7
  import { useTheme } from "styled-components";
@@ -737,6 +737,9 @@ const RemoveBackgroundButton = ({ src, onDone }) => {
737
737
  )
738
738
  ] });
739
739
  };
740
+ const IMAGE_MAX_MB = 30;
741
+ const IMAGE_MAX_BYTES = IMAGE_MAX_MB * 1024 * 1024;
742
+ const IMAGE_FORMATS_HINT = `PNG, JPG o WEBP de hasta ${IMAGE_MAX_MB} MB.`;
740
743
  const NameDialog = ({ open, title, label, initial = "", confirmLabel, validate, onClose, onConfirm }) => {
741
744
  const [name, setName] = useState(initial);
742
745
  const [error, setError] = useState(null);
@@ -1250,7 +1253,10 @@ const EditEmpty = ({ disabled, onUpload, onOpenLibrary, onOpenRecent }) => {
1250
1253
  style: { width: "100%", maxWidth: 600, border: `2px dashed ${over ? c.primary600 : c.neutral200}` },
1251
1254
  children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "center", gap: 3, style: { textAlign: "center" }, children: [
1252
1255
  /* @__PURE__ */ jsx(Typography, { variant: "beta", tag: "h2", children: "Elige la imagen que quieres editar" }),
1253
- /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: "Arrástrala aquí, súbela o elígela de la biblioteca. PNG, JPG o WEBP de hasta 10 MB." }),
1256
+ /* @__PURE__ */ jsxs(Typography, { textColor: "neutral600", children: [
1257
+ "Arrástrala aquí, súbela o elígela de la biblioteca. ",
1258
+ IMAGE_FORMATS_HINT
1259
+ ] }),
1254
1260
  /* @__PURE__ */ jsxs(Flex, { gap: 3, paddingTop: 2, children: [
1255
1261
  /* @__PURE__ */ jsx(Button, { variant: "tertiary", startIcon: /* @__PURE__ */ jsx(Upload, {}), disabled, onClick: () => input.current?.click(), children: "Subir imagen" }),
1256
1262
  /* @__PURE__ */ jsx(Button, { startIcon: /* @__PURE__ */ jsx(Folder, {}), disabled, onClick: onOpenLibrary, children: "Elegir de la biblioteca" })
@@ -1325,7 +1331,7 @@ const ReferencePicker = ({
1325
1331
  onChange,
1326
1332
  onPickFromLibrary,
1327
1333
  label = "Imágenes de referencia",
1328
- hint = "PNG, JPG o WEBP de hasta 10 MB.",
1334
+ hint = IMAGE_FORMATS_HINT,
1329
1335
  noun = REFERENCES
1330
1336
  }) => {
1331
1337
  const input = useRef(null);
@@ -1519,8 +1525,8 @@ const GeneratorTab = () => {
1519
1525
  };
1520
1526
  const uploadBase = (file) => {
1521
1527
  if (!file) return;
1522
- if (!IMAGE_MIMES.includes(file.type) || file.size > 10 * 1024 * 1024) {
1523
- notify("warning", "Usa una imagen PNG, JPG o WEBP de hasta 10 MB.");
1528
+ if (!IMAGE_MIMES.includes(file.type) || file.size > IMAGE_MAX_BYTES) {
1529
+ notify("warning", `Usa una imagen ${IMAGE_FORMATS_HINT}`);
1524
1530
  return;
1525
1531
  }
1526
1532
  const reader = new FileReader();
@@ -1595,7 +1601,7 @@ const GeneratorTab = () => {
1595
1601
  ReferencePicker,
1596
1602
  {
1597
1603
  label: "Referencias de estilo",
1598
- hint: "Guían el estilo, no se copian. PNG, JPG o WEBP de hasta 10 MB.",
1604
+ hint: `Guían el estilo, no se copian. ${IMAGE_FORMATS_HINT}`,
1599
1605
  files: references,
1600
1606
  max: model?.maxReferences ?? 0,
1601
1607
  modelLabel: model?.label ?? "Este modelo",
@@ -2184,17 +2190,6 @@ const LibraryTab = () => {
2184
2190
  /* @__PURE__ */ jsx(RemoveBackgroundDialog, { open: !!bgTarget, src: bgTarget?.src ?? "", onClose: () => setBgTarget(null), onApply: saveNoBackground })
2185
2191
  ] });
2186
2192
  };
2187
- const PALETTE = [
2188
- { label: "Navy", value: "#1F2A44" },
2189
- { label: "Rojo", value: "#E4002B" },
2190
- { label: "Teal", value: "#00C7B1" },
2191
- { label: "Teal suave", value: "#92CFC7" },
2192
- { label: "Amarillo", value: "#FFC844" },
2193
- { label: "Amarillo suave", value: "#F8DF8D" },
2194
- { label: "Gris", value: "#5B6473" },
2195
- { label: "Papel", value: "#EEF1F5" },
2196
- { label: "Blanco", value: "#FFFFFF" }
2197
- ];
2198
2193
  const NAVY = "#1F2A44";
2199
2194
  const RED$1 = "#E4002B";
2200
2195
  const TEAL$1 = "#00C7B1";
@@ -3934,44 +3929,63 @@ const SaveTemplateDialog = ({ open, defaultName, defaultFolder, folders, onClose
3934
3929
  ] })
3935
3930
  ] }) });
3936
3931
  };
3932
+ const parseHex = (raw) => {
3933
+ const hex = raw.trim().replace(/^#/, "");
3934
+ if (/^[0-9a-f]{3}$/i.test(hex)) return `#${[...hex].map((c) => c + c).join("")}`.toUpperCase();
3935
+ if (/^[0-9a-f]{6}$/i.test(hex)) return `#${hex}`.toUpperCase();
3936
+ return null;
3937
+ };
3937
3938
  const ColorField = ({ label, value, onChange }) => {
3938
3939
  const tc = useThemeColors();
3940
+ const current = parseHex(value ?? "") ?? "#000000";
3941
+ const [draft, setDraft] = useState(current);
3942
+ useEffect(() => setDraft((d) => parseHex(d) === current ? d : current), [current]);
3943
+ const apply = (text) => {
3944
+ const hex = parseHex(text);
3945
+ if (hex && hex !== current) onChange(hex);
3946
+ };
3947
+ const typeHex = (text) => {
3948
+ setDraft(text);
3949
+ if (text.trim().replace(/^#/, "").length === 6) apply(text);
3950
+ };
3951
+ const leave = () => {
3952
+ apply(draft);
3953
+ setDraft(parseHex(draft) ?? current);
3954
+ };
3939
3955
  return /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 1, children: [
3940
3956
  /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", children: label }),
3941
- /* @__PURE__ */ jsxs(Flex, { gap: 1, wrap: "wrap", alignItems: "center", children: [
3942
- PALETTE.map((c) => {
3943
- const active = value?.toUpperCase() === c.value;
3944
- return /* @__PURE__ */ jsx(
3945
- Box,
3946
- {
3947
- tag: "button",
3948
- color: "neutral800",
3949
- type: "button",
3950
- "aria-label": `${label}: ${c.label}`,
3951
- "aria-pressed": active,
3952
- title: c.label,
3953
- onClick: () => onChange(c.value),
3954
- style: {
3955
- width: 22,
3956
- height: 22,
3957
- borderRadius: 4,
3958
- cursor: "pointer",
3959
- background: c.value,
3960
- border: active ? `2px solid ${tc.primary600}` : `1px solid ${tc.neutral200}`,
3961
- padding: 0
3962
- }
3963
- },
3964
- c.value
3965
- );
3966
- }),
3957
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
3967
3958
  /* @__PURE__ */ jsx(
3968
3959
  "input",
3969
3960
  {
3970
3961
  type: "color",
3971
- "aria-label": `${label} personalizado`,
3972
- value: /^#[0-9a-f]{6}$/i.test(value ?? "") ? value : "#000000",
3962
+ "aria-label": `${label}: selector`,
3963
+ title: "Abrir selector de color",
3964
+ value: current.toLowerCase(),
3973
3965
  onChange: (e) => onChange(e.target.value.toUpperCase()),
3974
- style: { width: 26, height: 26, border: "none", padding: 0, background: "none", cursor: "pointer" }
3966
+ style: {
3967
+ flexShrink: 0,
3968
+ width: 32,
3969
+ height: 32,
3970
+ padding: 2,
3971
+ borderRadius: 4,
3972
+ cursor: "pointer",
3973
+ border: `1px solid ${tc.neutral200}`,
3974
+ background: tc.neutral0
3975
+ }
3976
+ }
3977
+ ),
3978
+ /* @__PURE__ */ jsx(
3979
+ TextInput,
3980
+ {
3981
+ "aria-label": `${label}: hexadecimal`,
3982
+ size: "S",
3983
+ value: draft,
3984
+ placeholder: "#1F2A44",
3985
+ maxLength: 7,
3986
+ spellCheck: false,
3987
+ onChange: (e) => typeHex(e.target.value),
3988
+ onBlur: leave
3975
3989
  }
3976
3990
  )
3977
3991
  ] })
@@ -5,7 +5,7 @@ import { useCallback, useState, useEffect } from "react";
5
5
  import { Box, Grid, Field, Flex, Button, Typography, Textarea } from "@strapi/design-system";
6
6
  import { Formik } from "formik";
7
7
  import { P as PluginProviders } from "./PluginProviders-DWMpcAIh.mjs";
8
- import { P as PLUGIN_ID } from "./index-D67xWUPz.mjs";
8
+ import { P as PLUGIN_ID } from "./index-D6UAvh53.mjs";
9
9
  const useSettings = () => {
10
10
  const { get, post, del } = useFetchClient();
11
11
  const getSettings = useCallback(
@@ -7,7 +7,7 @@ const react = require("react");
7
7
  const designSystem = require("@strapi/design-system");
8
8
  const formik = require("formik");
9
9
  const PluginProviders = require("./PluginProviders-D79TeGSE.js");
10
- const index = require("./index-NA9OuMGs.js");
10
+ const index = require("./index-C2s9Jhcc.js");
11
11
  const useSettings = () => {
12
12
  const { get, post, del } = admin.useFetchClient();
13
13
  const getSettings = react.useCallback(
@@ -2,9 +2,9 @@ import { jsxs, jsx } from "react/jsx-runtime";
2
2
  import { Page } from "@strapi/strapi/admin";
3
3
  import { Routes, Route } from "react-router-dom";
4
4
  import { P as PluginProviders } from "./PluginProviders-DWMpcAIh.mjs";
5
- import { p as pluginPermissions } from "./index-D67xWUPz.mjs";
5
+ import { p as pluginPermissions } from "./index-D6UAvh53.mjs";
6
6
  import { Box } from "@strapi/design-system";
7
- import { I as ImageStudioTabs } from "./ImageStudioTabs-B-mOwDfY.mjs";
7
+ import { I as ImageStudioTabs } from "./ImageStudioTabs-Umf3GODW.mjs";
8
8
  const ImageStudio = () => /* @__PURE__ */ jsxs(Page.Main, { children: [
9
9
  /* @__PURE__ */ jsx(Page.Title, { children: "Estudio de imágenes" }),
10
10
  /* @__PURE__ */ jsx(Box, { style: { height: "100vh" }, children: /* @__PURE__ */ jsx(ImageStudioTabs, {}) })
@@ -4,9 +4,9 @@ const jsxRuntime = require("react/jsx-runtime");
4
4
  const admin = require("@strapi/strapi/admin");
5
5
  const reactRouterDom = require("react-router-dom");
6
6
  const PluginProviders = require("./PluginProviders-D79TeGSE.js");
7
- const index = require("./index-NA9OuMGs.js");
7
+ const index = require("./index-C2s9Jhcc.js");
8
8
  const designSystem = require("@strapi/design-system");
9
- const ImageStudioTabs = require("./ImageStudioTabs-CG4KTn1S.js");
9
+ const ImageStudioTabs = require("./ImageStudioTabs-BFEF9ggK.js");
10
10
  const ImageStudio = () => /* @__PURE__ */ jsxRuntime.jsxs(admin.Page.Main, { children: [
11
11
  /* @__PURE__ */ jsxRuntime.jsx(admin.Page.Title, { children: "Estudio de imágenes" }),
12
12
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { height: "100vh" }, children: /* @__PURE__ */ jsxRuntime.jsx(ImageStudioTabs.ImageStudioTabs, {}) })
@@ -57,7 +57,7 @@ const index = {
57
57
  defaultMessage: "Generador de contenido"
58
58
  },
59
59
  Component: async () => {
60
- const { App } = await Promise.resolve().then(() => require("./App-Cwr5ra5m.js"));
60
+ const { App } = await Promise.resolve().then(() => require("./App-ugsCmlsC.js"));
61
61
  return App;
62
62
  },
63
63
  permissions: [
@@ -72,7 +72,7 @@ const index = {
72
72
  defaultMessage: "Estudio de imágenes"
73
73
  },
74
74
  Component: async () => {
75
- const { StudioApp } = await Promise.resolve().then(() => require("./StudioApp-_ViSAwCm.js"));
75
+ const { StudioApp } = await Promise.resolve().then(() => require("./StudioApp-DoC0z4eh.js"));
76
76
  return StudioApp;
77
77
  },
78
78
  permissions: [
@@ -92,7 +92,7 @@ const index = {
92
92
  defaultMessage: PLUGIN_ID
93
93
  },
94
94
  Component: async () => {
95
- const component = await Promise.resolve().then(() => require("./Settings-D4NGBTk1.js"));
95
+ const component = await Promise.resolve().then(() => require("./Settings-Sk78JUBS.js"));
96
96
  return component.default;
97
97
  },
98
98
  to: `plugins/${PLUGIN_ID}/settings`,
@@ -56,7 +56,7 @@ const index = {
56
56
  defaultMessage: "Generador de contenido"
57
57
  },
58
58
  Component: async () => {
59
- const { App } = await import("./App-h4_txTsI.mjs");
59
+ const { App } = await import("./App-CTh6owSM.mjs");
60
60
  return App;
61
61
  },
62
62
  permissions: [
@@ -71,7 +71,7 @@ const index = {
71
71
  defaultMessage: "Estudio de imágenes"
72
72
  },
73
73
  Component: async () => {
74
- const { StudioApp } = await import("./StudioApp-xaEy-uIk.mjs");
74
+ const { StudioApp } = await import("./StudioApp-CrGn-9Gg.mjs");
75
75
  return StudioApp;
76
76
  },
77
77
  permissions: [
@@ -91,7 +91,7 @@ const index = {
91
91
  defaultMessage: PLUGIN_ID
92
92
  },
93
93
  Component: async () => {
94
- const component = await import("./Settings-BT-EqvXs.mjs");
94
+ const component = await import("./Settings-CdGW2O8h.mjs");
95
95
  return component.default;
96
96
  },
97
97
  to: `plugins/${PLUGIN_ID}/settings`,
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
- const index = require("../_chunks/index-NA9OuMGs.js");
2
+ const index = require("../_chunks/index-C2s9Jhcc.js");
3
3
  module.exports = index.index;
@@ -1,4 +1,4 @@
1
- import { i } from "../_chunks/index-D67xWUPz.mjs";
1
+ import { i } from "../_chunks/index-D6UAvh53.mjs";
2
2
  export {
3
3
  i as default
4
4
  };
@@ -3,9 +3,11 @@ interface ColorFieldProps {
3
3
  value: string;
4
4
  onChange: (value: string) => void;
5
5
  }
6
+ /** «#0af», «0AF», «00aaff» o «#00AAFF» → «#00AAFF». Cualquier otra cosa → null. */
7
+ export declare const parseHex: (raw: string) => string | null;
6
8
  /**
7
- * Selector de color (snowflake: el DS no tiene uno). Muestras de la paleta InterWare y, para casos
8
- * puntuales, el selector nativo del navegador.
9
+ * Selector de color (snowflake: el DS no tiene uno). La muestra abre el selector de color del
10
+ * navegador y el campo acepta un hexadecimal escrito o pegado; solo se aplica cuando es válido.
9
11
  */
10
12
  export declare const ColorField: ({ label, value, onChange }: ColorFieldProps) => import("react/jsx-runtime").JSX.Element;
11
13
  export {};
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Paleta InterWare (paleta canónica del brochure). Es la única que ofrecen las muestras del
3
- * editor y la única que usan las plantillas base.
2
+ * Paleta InterWare (paleta canónica del brochure). Es la única que usan las plantillas base; en el
3
+ * editor el color es libre (selector o hexadecimal).
4
4
  */
5
5
  export declare const PALETTE: readonly [{
6
6
  readonly label: "Navy";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Tope por imagen de referencia o a editar; lo comparten el panel y el servidor. Medido en Vertex
3
+ * (2026-10-07, `scripts/vertex-diag-size.mjs`): una imagen generada en 2K pesa ~8 MB y en 4K hasta
4
+ * ~24 MB, y Google aceptó editarla en línea pese a los 7 MB que dice su documentación. 30 MB deja
5
+ * iterar sobre una 4K con margen.
6
+ */
7
+ export declare const IMAGE_MAX_MB = 30;
8
+ export declare const IMAGE_MAX_BYTES: number;
9
+ export declare const IMAGE_FORMATS_HINT = "PNG, JPG o WEBP de hasta 30 MB.";
@@ -266,14 +266,11 @@ const safetySettings = (safety) => HARM_CATEGORIES.map((category) => ({
266
266
  category,
267
267
  threshold: safety ? genai.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE : genai.HarmBlockThreshold.BLOCK_ONLY_HIGH
268
268
  }));
269
- const PEOPLE = {
270
- none: "ALLOW_NONE",
271
- adults: genai.PersonGeneration.ALLOW_ADULT
272
- };
269
+ const NO_PEOPLE = "ALLOW_NONE";
273
270
  const imageConfig = ({ aspectRatio, imageSize, allowPeople }, backend = "gemini-api") => ({
274
271
  ...aspectRatio ? { aspectRatio } : {},
275
272
  ...imageSize ? { imageSize } : {},
276
- ...backend === "vertex" ? { personGeneration: allowPeople ? PEOPLE.adults : PEOPLE.none } : {}
273
+ ...backend === "vertex" && !allowPeople ? { personGeneration: NO_PEOPLE } : {}
277
274
  });
278
275
  const promptPart = (text2, allowPeople, backend = "gemini-api") => ({
279
276
  text: backend === "gemini-api" && !allowPeople ? `${text2} ${PEOPLE_CLAUSE}` : text2
@@ -339,6 +336,10 @@ const EXPAND_INSTRUCTION = (concept) => [
339
336
  `Concepto: ${concept}`
340
337
  ].join("\n");
341
338
  class BlockedError extends Error {
339
+ constructor(message, detail) {
340
+ super(message);
341
+ this.detail = detail;
342
+ }
342
343
  }
343
344
  const sourceOf = (chunk) => {
344
345
  if (chunk.web?.uri) return { title: chunk.web.title || new URL(chunk.web.uri).hostname, uri: chunk.web.uri };
@@ -360,12 +361,21 @@ const BLOCK_REASONS = {
360
361
  PROHIBITED_CONTENT: "El modelo bloqueó la solicitud por contenido no permitido.",
361
362
  IMAGE_SAFETY: "El modelo bloqueó la imagen resultante por seguridad."
362
363
  };
364
+ const blockMessage = (reason, googleMessage) => {
365
+ const code = googleMessage.match(/Support code:\s*(\d+)/i)?.[1];
366
+ const suffix = code ? ` (Código de Google: ${code})` : "";
367
+ if (/PersonGeneration/i.test(googleMessage)) {
368
+ return /input image/i.test(googleMessage) ? `Google rechazó la imagen de referencia por la configuración de «Personas». Prueba con otra imagen o cambia «Personas».${suffix}` : `Google filtró la imagen generada por la configuración de «Personas». Cambia el prompt o «Personas».${suffix}`;
369
+ }
370
+ return `${BLOCK_REASONS[reason] ?? "El modelo no devolvió una imagen. Intenta con otro prompt."}${suffix}`;
371
+ };
363
372
  const imageFrom = (res, model) => {
364
373
  const parts = res.candidates?.[0]?.content?.parts ?? [];
365
374
  const part = parts.find((p) => p.inlineData?.data && (p.inlineData.mimeType ?? "").startsWith("image/"));
366
375
  if (part?.inlineData?.data) return { image: part.inlineData.data, mimeType: part.inlineData.mimeType ?? "image/png", model };
367
- const reason = res.promptFeedback?.blockReason ?? res.candidates?.[0]?.finishReason;
368
- throw new BlockedError(BLOCK_REASONS[String(reason)] ?? "El modelo no devolvió una imagen. Intenta con otro prompt.");
376
+ const reason = String(res.promptFeedback?.blockReason ?? res.candidates?.[0]?.finishReason ?? "");
377
+ const googleMessage = [res.candidates?.[0]?.finishMessage, res.promptFeedback?.blockReasonMessage].filter(Boolean).join(" ");
378
+ throw new BlockedError(blockMessage(reason, googleMessage), [reason, googleMessage].filter(Boolean).join(": ") || void 0);
369
379
  };
370
380
  const inline = async (file, read) => inlinePart(await read(file.filepath), file.mimetype ?? "image/png");
371
381
  const userTurn = (parts) => [{ role: "user", parts }];
@@ -418,10 +428,11 @@ const imageStudio$1 = ({ strapi }) => createImageStudio(async () => {
418
428
  const { ai, backend } = await strapi.plugin("strapi-content-generator-gemini").service("service").geminiClient();
419
429
  return { models: ai.models, backend };
420
430
  });
431
+ const IMAGE_MAX_MB = 30;
432
+ const IMAGE_MAX_BYTES = IMAGE_MAX_MB * 1024 * 1024;
421
433
  class InputError extends Error {
422
434
  }
423
435
  const ALLOWED = ["image/png", "image/jpeg", "image/webp"];
424
- const MAX_BYTES = 10 * 1024 * 1024;
425
436
  const str = (v) => typeof v === "string" ? v : "";
426
437
  const bool = (v) => v === true || v === "true";
427
438
  const list = (v) => v ? Array.isArray(v) ? v : [v] : [];
@@ -432,7 +443,7 @@ const clean = (text2, labels) => {
432
443
  };
433
444
  const checkImage = (f) => {
434
445
  if (!ALLOWED.includes(f.mimetype ?? "")) throw new InputError("Las imágenes deben ser PNG, JPG o WEBP.");
435
- if (f.size > MAX_BYTES) throw new InputError("Cada imagen puede pesar hasta 10 MB.");
446
+ if (f.size > IMAGE_MAX_BYTES) throw new InputError(`Cada imagen puede pesar hasta ${IMAGE_MAX_MB} MB.`);
436
447
  return f;
437
448
  };
438
449
  const parseGenerateInput = (fields, files) => {
@@ -486,7 +497,10 @@ const imageStudio = ({ strapi }) => {
486
497
  ctx.body = await work();
487
498
  } catch (err) {
488
499
  if (err instanceof InputError) return fail$3(ctx, 400, "BadRequestError", err.message);
489
- if (err instanceof BlockedError) return fail$3(ctx, 422, "UnprocessableEntityError", err.message);
500
+ if (err instanceof BlockedError) {
501
+ strapi.log.warn(`[image-studio] bloqueo: ${err.detail ?? err.message}`);
502
+ return fail$3(ctx, 422, "UnprocessableEntityError", err.message);
503
+ }
490
504
  strapi.log.error(`[image-studio] ${err instanceof Error ? err.message : String(err)}`);
491
505
  return fail$3(ctx, 502, "BadGatewayError", "El servicio de imágenes no respondió. Intenta de nuevo en un momento.");
492
506
  }
@@ -1,6 +1,6 @@
1
1
  import * as fs from "fs";
2
2
  import { promises } from "fs";
3
- import { HarmCategory, PersonGeneration, Modality, HarmBlockThreshold, GoogleGenAI, VideoGenerationReferenceType } from "@google/genai";
3
+ import { HarmCategory, Modality, HarmBlockThreshold, GoogleGenAI, VideoGenerationReferenceType } from "@google/genai";
4
4
  const PLUGIN_ID = "strapi-content-generator-gemini";
5
5
  const FOLDER_UID = "plugin::upload.folder";
6
6
  const hasValidPath = (path) => /^(\/\d+)+$/.test(path);
@@ -248,14 +248,11 @@ const safetySettings = (safety) => HARM_CATEGORIES.map((category) => ({
248
248
  category,
249
249
  threshold: safety ? HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE : HarmBlockThreshold.BLOCK_ONLY_HIGH
250
250
  }));
251
- const PEOPLE = {
252
- none: "ALLOW_NONE",
253
- adults: PersonGeneration.ALLOW_ADULT
254
- };
251
+ const NO_PEOPLE = "ALLOW_NONE";
255
252
  const imageConfig = ({ aspectRatio, imageSize, allowPeople }, backend = "gemini-api") => ({
256
253
  ...aspectRatio ? { aspectRatio } : {},
257
254
  ...imageSize ? { imageSize } : {},
258
- ...backend === "vertex" ? { personGeneration: allowPeople ? PEOPLE.adults : PEOPLE.none } : {}
255
+ ...backend === "vertex" && !allowPeople ? { personGeneration: NO_PEOPLE } : {}
259
256
  });
260
257
  const promptPart = (text2, allowPeople, backend = "gemini-api") => ({
261
258
  text: backend === "gemini-api" && !allowPeople ? `${text2} ${PEOPLE_CLAUSE}` : text2
@@ -321,6 +318,10 @@ const EXPAND_INSTRUCTION = (concept) => [
321
318
  `Concepto: ${concept}`
322
319
  ].join("\n");
323
320
  class BlockedError extends Error {
321
+ constructor(message, detail) {
322
+ super(message);
323
+ this.detail = detail;
324
+ }
324
325
  }
325
326
  const sourceOf = (chunk) => {
326
327
  if (chunk.web?.uri) return { title: chunk.web.title || new URL(chunk.web.uri).hostname, uri: chunk.web.uri };
@@ -342,12 +343,21 @@ const BLOCK_REASONS = {
342
343
  PROHIBITED_CONTENT: "El modelo bloqueó la solicitud por contenido no permitido.",
343
344
  IMAGE_SAFETY: "El modelo bloqueó la imagen resultante por seguridad."
344
345
  };
346
+ const blockMessage = (reason, googleMessage) => {
347
+ const code = googleMessage.match(/Support code:\s*(\d+)/i)?.[1];
348
+ const suffix = code ? ` (Código de Google: ${code})` : "";
349
+ if (/PersonGeneration/i.test(googleMessage)) {
350
+ return /input image/i.test(googleMessage) ? `Google rechazó la imagen de referencia por la configuración de «Personas». Prueba con otra imagen o cambia «Personas».${suffix}` : `Google filtró la imagen generada por la configuración de «Personas». Cambia el prompt o «Personas».${suffix}`;
351
+ }
352
+ return `${BLOCK_REASONS[reason] ?? "El modelo no devolvió una imagen. Intenta con otro prompt."}${suffix}`;
353
+ };
345
354
  const imageFrom = (res, model) => {
346
355
  const parts = res.candidates?.[0]?.content?.parts ?? [];
347
356
  const part = parts.find((p) => p.inlineData?.data && (p.inlineData.mimeType ?? "").startsWith("image/"));
348
357
  if (part?.inlineData?.data) return { image: part.inlineData.data, mimeType: part.inlineData.mimeType ?? "image/png", model };
349
- const reason = res.promptFeedback?.blockReason ?? res.candidates?.[0]?.finishReason;
350
- throw new BlockedError(BLOCK_REASONS[String(reason)] ?? "El modelo no devolvió una imagen. Intenta con otro prompt.");
358
+ const reason = String(res.promptFeedback?.blockReason ?? res.candidates?.[0]?.finishReason ?? "");
359
+ const googleMessage = [res.candidates?.[0]?.finishMessage, res.promptFeedback?.blockReasonMessage].filter(Boolean).join(" ");
360
+ throw new BlockedError(blockMessage(reason, googleMessage), [reason, googleMessage].filter(Boolean).join(": ") || void 0);
351
361
  };
352
362
  const inline = async (file, read) => inlinePart(await read(file.filepath), file.mimetype ?? "image/png");
353
363
  const userTurn = (parts) => [{ role: "user", parts }];
@@ -400,10 +410,11 @@ const imageStudio$1 = ({ strapi }) => createImageStudio(async () => {
400
410
  const { ai, backend } = await strapi.plugin("strapi-content-generator-gemini").service("service").geminiClient();
401
411
  return { models: ai.models, backend };
402
412
  });
413
+ const IMAGE_MAX_MB = 30;
414
+ const IMAGE_MAX_BYTES = IMAGE_MAX_MB * 1024 * 1024;
403
415
  class InputError extends Error {
404
416
  }
405
417
  const ALLOWED = ["image/png", "image/jpeg", "image/webp"];
406
- const MAX_BYTES = 10 * 1024 * 1024;
407
418
  const str = (v) => typeof v === "string" ? v : "";
408
419
  const bool = (v) => v === true || v === "true";
409
420
  const list = (v) => v ? Array.isArray(v) ? v : [v] : [];
@@ -414,7 +425,7 @@ const clean = (text2, labels) => {
414
425
  };
415
426
  const checkImage = (f) => {
416
427
  if (!ALLOWED.includes(f.mimetype ?? "")) throw new InputError("Las imágenes deben ser PNG, JPG o WEBP.");
417
- if (f.size > MAX_BYTES) throw new InputError("Cada imagen puede pesar hasta 10 MB.");
428
+ if (f.size > IMAGE_MAX_BYTES) throw new InputError(`Cada imagen puede pesar hasta ${IMAGE_MAX_MB} MB.`);
418
429
  return f;
419
430
  };
420
431
  const parseGenerateInput = (fields, files) => {
@@ -468,7 +479,10 @@ const imageStudio = ({ strapi }) => {
468
479
  ctx.body = await work();
469
480
  } catch (err) {
470
481
  if (err instanceof InputError) return fail$3(ctx, 400, "BadRequestError", err.message);
471
- if (err instanceof BlockedError) return fail$3(ctx, 422, "UnprocessableEntityError", err.message);
482
+ if (err instanceof BlockedError) {
483
+ strapi.log.warn(`[image-studio] bloqueo: ${err.detail ?? err.message}`);
484
+ return fail$3(ctx, 422, "UnprocessableEntityError", err.message);
485
+ }
472
486
  strapi.log.error(`[image-studio] ${err instanceof Error ? err.message : String(err)}`);
473
487
  return fail$3(ctx, 502, "BadGatewayError", "El servicio de imágenes no respondió. Intenta de nuevo en un momento.");
474
488
  }
@@ -4,8 +4,13 @@ import type { Core } from '@strapi/strapi';
4
4
  import type { GoogleGenAI } from '@google/genai';
5
5
  import type { Backend } from '../image-studio/geminiConfig';
6
6
  import type { EditInput, GenerateInput } from '../image-studio/validate';
7
- /** El modelo no devolvió imagen (bloqueo de seguridad u otra causa): el controlador responde 422. */
7
+ /**
8
+ * El modelo no devolvió imagen (bloqueo de seguridad u otra causa): el controlador responde 422 con
9
+ * `message` y registra `detail` (motivo y mensaje de Google, con su código de soporte).
10
+ */
8
11
  export declare class BlockedError extends Error {
12
+ readonly detail?: string;
13
+ constructor(message: string, detail?: string);
9
14
  }
10
15
  type ModelsApi = Pick<GoogleGenAI['models'], 'generateContent' | 'list'>;
11
16
  type ReadFile = (path: string) => Promise<Buffer>;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.7.0",
2
+ "version": "0.7.2",
3
3
  "keywords": [],
4
4
  "type": "commonjs",
5
5
  "publishConfig": {