@interware/content-generator 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/_chunks/{App-h4_txTsI.mjs → App-DD1MzaWm.mjs} +2 -2
  2. package/dist/_chunks/{App-Cwr5ra5m.js → App-DspaMfyW.js} +2 -2
  3. package/dist/_chunks/{ImageStudioTabs-B-mOwDfY.mjs → ImageStudioTabs-DwaLxRjl.mjs} +367 -61
  4. package/dist/_chunks/{ImageStudioTabs-CG4KTn1S.js → ImageStudioTabs-uXSBkFeu.js} +365 -59
  5. package/dist/_chunks/{Settings-BT-EqvXs.mjs → Settings-C6TbUrgU.mjs} +170 -2
  6. package/dist/_chunks/{Settings-D4NGBTk1.js → Settings-ps0o7-fC.js} +169 -1
  7. package/dist/_chunks/{StudioApp-xaEy-uIk.mjs → StudioApp-B5I1OfGw.mjs} +2 -2
  8. package/dist/_chunks/{StudioApp-_ViSAwCm.js → StudioApp-B7kGpqqo.js} +2 -2
  9. package/dist/_chunks/{index-NA9OuMGs.js → index-BudahdfX.js} +3 -3
  10. package/dist/_chunks/{index-D67xWUPz.mjs → index-zenHkX-p.mjs} +3 -3
  11. package/dist/admin/index.js +1 -1
  12. package/dist/admin/index.mjs +1 -1
  13. package/dist/admin/src/components/settings/DriveSettings.d.ts +77 -0
  14. package/dist/admin/src/features/drive/DriveTab.d.ts +42 -0
  15. package/dist/admin/src/features/image-studio/ImageStudioTabs.d.ts +3 -0
  16. package/dist/admin/src/features/library/FileCard.d.ts +5 -2
  17. package/dist/admin/src/features/library/driveSync.d.ts +41 -0
  18. package/dist/admin/src/features/templates/editor/ColorField.d.ts +4 -2
  19. package/dist/admin/src/features/templates/export/templateOrigin.d.ts +10 -0
  20. package/dist/admin/src/features/templates/model/palette.d.ts +2 -2
  21. package/dist/admin/src/lib/imageLimits.d.ts +9 -0
  22. package/dist/admin/src/lib/use-cases/drive/index.d.ts +3 -0
  23. package/dist/server/index.js +1414 -51
  24. package/dist/server/index.mjs +1412 -49
  25. package/dist/server/src/bootstrap.d.ts +2 -0
  26. package/dist/server/src/content-types/drive-link/index.d.ts +62 -0
  27. package/dist/server/src/content-types/drive-link/schema.d.ts +68 -0
  28. package/dist/server/src/content-types/drive-op/index.d.ts +46 -0
  29. package/dist/server/src/content-types/drive-op/schema.d.ts +49 -0
  30. package/dist/server/src/content-types/index.d.ts +106 -0
  31. package/dist/server/src/controllers/drive.d.ts +30 -0
  32. package/dist/server/src/controllers/index.d.ts +130 -0
  33. package/dist/server/src/drive/client.d.ts +32 -0
  34. package/dist/server/src/drive/clientConfig.d.ts +27 -0
  35. package/dist/server/src/drive/common.d.ts +6 -0
  36. package/dist/server/src/drive/fakes.d.ts +104 -0
  37. package/dist/server/src/drive/files.d.ts +136 -0
  38. package/dist/server/src/drive/lifecycles.d.ts +7 -0
  39. package/dist/server/src/drive/links.d.ts +23 -0
  40. package/dist/server/src/drive/model.d.ts +99 -0
  41. package/dist/server/src/drive/oauth.d.ts +47 -0
  42. package/dist/server/src/drive/pull.d.ts +23 -0
  43. package/dist/server/src/drive/strapiPorts.d.ts +10 -0
  44. package/dist/server/src/drive/sync.d.ts +40 -0
  45. package/dist/server/src/index.d.ts +294 -0
  46. package/dist/server/src/services/drive.d.ts +141 -0
  47. package/dist/server/src/services/index.d.ts +55 -0
  48. package/package.json +1 -1
@@ -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-zenHkX-p.mjs";
9
+ import { L as LibraryImagePicker, P as PromptComposer, a as LEGACY_ROUTES, S as StudioRedirect, l as legacyRedirect } from "./ImageStudioTabs-DwaLxRjl.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-BudahdfX.js");
11
+ const ImageStudioTabs = require("./ImageStudioTabs-uXSBkFeu.js");
12
12
  const reactIntl = require("react-intl");
13
13
  const formik = require("formik");
14
14
  const Yup = require("yup");
@@ -1,9 +1,9 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
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";
2
+ import { Modal, Flex, Button, Loader, Typography, Grid, Box, lightTheme, Checkbox, Field, TextInput, Dialog, SingleSelect, SingleSelectOption, Link, Badge, IconButtonGroup, IconButton, Toggle, Menu, Table, Thead, Tr, Th, VisuallyHidden, Tbody, Td, TextButton, LinkButton, 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-zenHkX-p.mjs";
5
5
  import { useMemo, useState, useEffect, useId, useRef, useReducer, useCallback, useSyncExternalStore, useLayoutEffect, memo, forwardRef } from "react";
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";
6
+ import { ChevronRight, Folder, Magic, Pencil, Upload, Sparkle, Plus, Cross, Download, ArrowClockwise, House, ChevronDown, FilePdf, File as File$1, More, ExternalLink, 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";
8
8
  import { useFetchClient, useNotification } from "@strapi/strapi/admin";
9
9
  import { toPng, toSvg, toJpeg } from "html-to-image";
@@ -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);
@@ -1168,7 +1171,7 @@ const closestRatio = (width, height, ratios) => {
1168
1171
  };
1169
1172
  return ratios.reduce((best, r) => Math.abs(value(r) - target) < Math.abs(value(best) - target) ? r : best);
1170
1173
  };
1171
- const BASE = "/strapi-content-generator-gemini/image-studio";
1174
+ const BASE$2 = "/strapi-content-generator-gemini/image-studio";
1172
1175
  const toResult = ({ image, mimeType, model, grounding }) => ({
1173
1176
  src: `data:${mimeType};base64,${image}`,
1174
1177
  model,
@@ -1180,7 +1183,7 @@ const errorMessage = (err, fallback) => {
1180
1183
  };
1181
1184
  const createGeneratorApi = (client) => ({
1182
1185
  async models() {
1183
- const { data } = await client.get(`${BASE}/models`);
1186
+ const { data } = await client.get(`${BASE$2}/models`);
1184
1187
  return data;
1185
1188
  },
1186
1189
  async generate(p) {
@@ -1193,7 +1196,7 @@ const createGeneratorApi = (client) => ({
1193
1196
  form.append("allowPeople", String(p.allowPeople));
1194
1197
  form.append("grounding", String(p.grounding));
1195
1198
  p.references.forEach((f) => form.append("references", f, f.name));
1196
- const { data } = await client.post(`${BASE}/generate`, form);
1199
+ const { data } = await client.post(`${BASE$2}/generate`, form);
1197
1200
  return toResult(data);
1198
1201
  },
1199
1202
  async edit(p) {
@@ -1206,11 +1209,11 @@ const createGeneratorApi = (client) => ({
1206
1209
  p.references.forEach((f) => form.append("references", f, f.name));
1207
1210
  if (p.aspectRatio) form.append("aspectRatio", p.aspectRatio);
1208
1211
  form.append("image", new File([blob], `base.${blob.type.split("/")[1] ?? "png"}`, { type: blob.type }));
1209
- const { data } = await client.post(`${BASE}/edit`, form);
1212
+ const { data } = await client.post(`${BASE$2}/edit`, form);
1210
1213
  return toResult(data);
1211
1214
  },
1212
1215
  async expand(prompt) {
1213
- const { data } = await client.post(`${BASE}/expand`, { prompt });
1216
+ const { data } = await client.post(`${BASE$2}/expand`, { prompt });
1214
1217
  return data.prompt;
1215
1218
  }
1216
1219
  });
@@ -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",
@@ -1829,7 +1835,7 @@ const isImage = (f) => (f.mime ?? "").startsWith("image/");
1829
1835
  const canRemoveBackground = (f) => isImage(f) && f.mime !== "image/svg+xml";
1830
1836
  const isGenerated = (f) => (f.caption ?? "").startsWith("AI generated content");
1831
1837
  const formatSize = (kb = 0) => kb >= 1024 ? `${(kb / 1024).toFixed(1)} MB` : `${Math.round(kb)} KB`;
1832
- const FileCard = ({ file, onAction, busy }) => {
1838
+ const FileCard = ({ file, onAction, busy, drive }) => {
1833
1839
  const c = useThemeColors();
1834
1840
  const thumb2 = file.formats?.thumbnail?.url ?? file.formats?.small?.url ?? file.url;
1835
1841
  return /* @__PURE__ */ jsxs(
@@ -1868,7 +1874,8 @@ const FileCard = ({ file, onAction, busy }) => {
1868
1874
  /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
1869
1875
  /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: formatSize(file.size) }),
1870
1876
  isGenerated(file) && /* @__PURE__ */ jsx(Badge, { children: "Generada" }),
1871
- busy && /* @__PURE__ */ jsx(Badge, { active: true, children: busy })
1877
+ busy && /* @__PURE__ */ jsx(Badge, { active: true, children: busy }),
1878
+ drive?.label && /* @__PURE__ */ jsx(Badge, { variant: drive.tone, title: drive.title, children: drive.label })
1872
1879
  ] })
1873
1880
  ] }),
1874
1881
  /* @__PURE__ */ jsxs(Menu.Root, { children: [
@@ -1880,6 +1887,8 @@ const FileCard = ({ file, onAction, busy }) => {
1880
1887
  /* @__PURE__ */ jsx(Menu.Item, { onSelect: () => onAction("replace", file), children: "Reemplazar" }),
1881
1888
  /* @__PURE__ */ jsx(Menu.Item, { onSelect: () => onAction("move", file), children: "Mover a…" }),
1882
1889
  canRemoveBackground(file) && /* @__PURE__ */ jsx(Menu.Item, { onSelect: () => onAction("removeBackground", file), disabled: !!busy, children: "Quitar fondo" }),
1890
+ drive?.action === "mark" && /* @__PURE__ */ jsx(Menu.Item, { onSelect: () => onAction("syncDrive", file), children: "Sincronizar con Drive" }),
1891
+ drive?.action === "unmark" && /* @__PURE__ */ jsx(Menu.Item, { onSelect: () => onAction("unsyncDrive", file), children: "Dejar de sincronizar" }),
1883
1892
  /* @__PURE__ */ jsx(Menu.Item, { onSelect: () => onAction("delete", file), variant: "danger", children: "Eliminar" })
1884
1893
  ] })
1885
1894
  ] })
@@ -1888,10 +1897,255 @@ const FileCard = ({ file, onAction, busy }) => {
1888
1897
  }
1889
1898
  );
1890
1899
  };
1900
+ const BASE$1 = "/strapi-content-generator-gemini/drive";
1901
+ const FOLDER = "application/vnd.google-apps.folder";
1902
+ const NATIVE_TYPES = {
1903
+ "application/vnd.google-apps.document": "Documento de Google",
1904
+ "application/vnd.google-apps.spreadsheet": "Hoja de cálculo de Google",
1905
+ "application/vnd.google-apps.presentation": "Presentación de Google",
1906
+ "application/vnd.google-apps.form": "Formulario de Google",
1907
+ "application/vnd.google-apps.drawing": "Dibujo de Google",
1908
+ "application/vnd.google-apps.shortcut": "Acceso directo"
1909
+ };
1910
+ const typeLabel = (mime) => {
1911
+ if (mime === FOLDER) return "Carpeta";
1912
+ if (NATIVE_TYPES[mime]) return NATIVE_TYPES[mime];
1913
+ if (mime.startsWith("application/vnd.google-apps.")) return "Archivo de Google";
1914
+ if (mime === "application/pdf") return "PDF";
1915
+ if (mime.startsWith("image/")) return "Imagen";
1916
+ if (mime.startsWith("video/")) return "Video";
1917
+ if (mime.startsWith("audio/")) return "Audio";
1918
+ return "Archivo";
1919
+ };
1920
+ const TypeIcon = ({ mime }) => mime === FOLDER ? /* @__PURE__ */ jsx(Folder, { width: 16, height: 16, "aria-hidden": true }) : mime === "application/pdf" ? /* @__PURE__ */ jsx(FilePdf, { width: 16, height: 16, "aria-hidden": true }) : /* @__PURE__ */ jsx(File$1, { width: 16, height: 16, "aria-hidden": true });
1921
+ const formatDate = (iso) => iso ? new Date(iso).toLocaleString("es-MX", { dateStyle: "medium", timeStyle: "short", timeZone: "America/Mexico_City" }) : "—";
1922
+ const confirmText = (item, count) => {
1923
+ const parts = [`«${item.name}» irá a la papelera de Drive, donde se puede recuperar durante 30 días.`];
1924
+ if (item.mimeType === FOLDER && count) {
1925
+ const total = count.files + count.folders;
1926
+ parts.push(
1927
+ total === 0 ? "La carpeta está vacía." : `Contiene ${count.truncated ? "más de " : ""}${total.toLocaleString("es-MX")} ${total === 1 ? "elemento" : "elementos"} (${count.files} archivos y ${count.folders} carpetas), que también se van.`
1928
+ );
1929
+ }
1930
+ if (item.synced || count?.synced) parts.push("Está sincronizado: también se borrará de la Biblioteca del CMS.");
1931
+ return parts.join(" ");
1932
+ };
1933
+ const serverMessage$1 = (error, fallback) => error?.response?.data?.error?.message ?? (error instanceof Error && error.message ? error.message : fallback);
1934
+ const useDriveBrowserApi = () => {
1935
+ const { get, del } = useFetchClient();
1936
+ return useMemo(
1937
+ () => ({
1938
+ browse: async (folderId, pageToken) => {
1939
+ try {
1940
+ return (await get(`${BASE$1}/folders/${encodeURIComponent(folderId)}`, { params: pageToken ? { pageToken } : {} })).data;
1941
+ } catch (error) {
1942
+ throw new Error(serverMessage$1(error, "No se pudo leer Google Drive."));
1943
+ }
1944
+ },
1945
+ count: async (id) => (await get(`${BASE$1}/items/${encodeURIComponent(id)}/count`)).data,
1946
+ trash: async (id) => {
1947
+ try {
1948
+ await del(`${BASE$1}/items/${encodeURIComponent(id)}`);
1949
+ } catch (error) {
1950
+ throw new Error(serverMessage$1(error, "No se pudo mandar a la papelera."));
1951
+ }
1952
+ }
1953
+ }),
1954
+ [get, del]
1955
+ );
1956
+ };
1957
+ const DriveTab = ({ api: injected, notify }) => {
1958
+ const fallbackApi = useDriveBrowserApi();
1959
+ const api = injected ?? fallbackApi;
1960
+ const { toggleNotification } = useNotification();
1961
+ const say = notify ?? ((type, message) => toggleNotification({ type, message }));
1962
+ const [folderId, setFolderId] = useState("root");
1963
+ const [page, setPage] = useState(null);
1964
+ const [error, setError] = useState(null);
1965
+ const [loading2, setLoading] = useState(true);
1966
+ const [loadingMore, setLoadingMore] = useState(false);
1967
+ const [doomed, setDoomed] = useState(null);
1968
+ const load = useCallback(async () => {
1969
+ setLoading(true);
1970
+ setError(null);
1971
+ try {
1972
+ setPage(await api.browse(folderId));
1973
+ } catch (err) {
1974
+ setError(err instanceof Error ? err.message : "No se pudo leer Google Drive.");
1975
+ } finally {
1976
+ setLoading(false);
1977
+ }
1978
+ }, [api, folderId]);
1979
+ useEffect(() => {
1980
+ load();
1981
+ }, [load]);
1982
+ const more = async () => {
1983
+ if (!page?.nextPageToken) return;
1984
+ setLoadingMore(true);
1985
+ try {
1986
+ const next = await api.browse(folderId, page.nextPageToken);
1987
+ setPage({ ...next, items: [...page.items, ...next.items] });
1988
+ } finally {
1989
+ setLoadingMore(false);
1990
+ }
1991
+ };
1992
+ const askDelete = async (item) => {
1993
+ setDoomed({ item, count: null });
1994
+ if (item.mimeType === FOLDER) {
1995
+ try {
1996
+ const count = await api.count(item.id);
1997
+ setDoomed((d) => d?.item.id === item.id ? { item, count } : d);
1998
+ } catch {
1999
+ }
2000
+ }
2001
+ };
2002
+ const confirmDelete = async () => {
2003
+ if (!doomed) return;
2004
+ const { item } = doomed;
2005
+ setDoomed(null);
2006
+ try {
2007
+ await api.trash(item.id);
2008
+ say("success", `«${item.name}» se mandó a la papelera de Drive.`);
2009
+ await load();
2010
+ } catch (err) {
2011
+ say("danger", err instanceof Error ? err.message : "No se pudo mandar a la papelera.");
2012
+ }
2013
+ };
2014
+ if (error) return /* @__PURE__ */ jsx(EmptyNote, { children: error });
2015
+ return /* @__PURE__ */ jsxs(Box, { children: [
2016
+ /* @__PURE__ */ jsxs(Flex, { "data-testid": "drive-breadcrumb", gap: 1, alignItems: "center", wrap: "wrap", paddingBottom: 4, children: [
2017
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "S", onClick: () => setFolderId("root"), children: "Mi unidad" }),
2018
+ (page?.path ?? []).map((p, i, all) => /* @__PURE__ */ jsxs(Flex, { gap: 1, alignItems: "center", children: [
2019
+ /* @__PURE__ */ jsx(ChevronRight, { width: 10, height: 10, "aria-hidden": true }),
2020
+ i === all.length - 1 ? /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", children: p.name }) : /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "S", onClick: () => setFolderId(p.id), children: p.name })
2021
+ ] }, p.id))
2022
+ ] }),
2023
+ loading2 ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: "Cargando Google Drive…" }) }) : !page?.items.length ? /* @__PURE__ */ jsx(EmptyNote, { children: "Esta carpeta de Drive está vacía." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2024
+ /* @__PURE__ */ jsxs(Table, { colCount: 6, rowCount: page.items.length + 1, children: [
2025
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
2026
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Nombre" }) }),
2027
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Tipo" }) }),
2028
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Modificado" }) }),
2029
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Tamaño" }) }),
2030
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Estado" }) }),
2031
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(VisuallyHidden, { children: "Acciones" }) })
2032
+ ] }) }),
2033
+ /* @__PURE__ */ jsx(Tbody, { children: page.items.map((item) => /* @__PURE__ */ jsxs(Tr, { children: [
2034
+ /* @__PURE__ */ jsx(Td, { style: { maxWidth: 420, whiteSpace: "normal" }, children: /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
2035
+ /* @__PURE__ */ jsx(TypeIcon, { mime: item.mimeType }),
2036
+ item.mimeType === FOLDER ? /* @__PURE__ */ jsx(TextButton, { onClick: () => setFolderId(item.id), style: { textAlign: "left", height: "auto" }, children: /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", textColor: "neutral800", style: { overflowWrap: "anywhere" }, children: item.name }) }) : /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", style: { overflowWrap: "anywhere" }, children: item.name })
2037
+ ] }) }),
2038
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral700", children: typeLabel(item.mimeType) }) }),
2039
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral700", style: { whiteSpace: "nowrap" }, children: formatDate(item.modifiedTime) }) }),
2040
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral700", children: item.size === null ? "—" : formatSize(item.size / 1e3) }) }),
2041
+ /* @__PURE__ */ jsx(Td, { children: item.synced && /* @__PURE__ */ jsx(Badge, { variant: "success", children: "Sincronizado" }) }),
2042
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, justifyContent: "end", children: [
2043
+ item.webViewLink && /* @__PURE__ */ jsx(
2044
+ LinkButton,
2045
+ {
2046
+ variant: "tertiary",
2047
+ size: "S",
2048
+ href: item.webViewLink,
2049
+ target: "_blank",
2050
+ rel: "noreferrer noopener",
2051
+ endIcon: /* @__PURE__ */ jsx(ExternalLink, {}),
2052
+ children: "Abrir en Drive"
2053
+ }
2054
+ ),
2055
+ /* @__PURE__ */ jsx(IconButton, { label: `Eliminar ${item.name}`, variant: "ghost", onClick: () => askDelete(item), children: /* @__PURE__ */ jsx(Trash, {}) })
2056
+ ] }) })
2057
+ ] }, item.id)) })
2058
+ ] }),
2059
+ page.nextPageToken && /* @__PURE__ */ jsx(Flex, { justifyContent: "center", paddingTop: 4, children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", onClick: more, loading: loadingMore, children: "Mostrar más" }) })
2060
+ ] }),
2061
+ /* @__PURE__ */ jsx(
2062
+ ConfirmDialog,
2063
+ {
2064
+ open: !!doomed,
2065
+ title: "¿Mandar a la papelera de Drive?",
2066
+ confirmLabel: "Mandar a la papelera",
2067
+ onClose: () => setDoomed(null),
2068
+ onConfirm: confirmDelete,
2069
+ children: doomed ? confirmText(doomed.item, doomed.count) : ""
2070
+ }
2071
+ )
2072
+ ] });
2073
+ };
2074
+ const BASE = "/strapi-content-generator-gemini/drive";
2075
+ const POLL_MS = 1e4;
2076
+ const LABELS = {
2077
+ synced: { label: "Drive", tone: "success" },
2078
+ pending: { label: "Drive: pendiente", tone: "neutral" },
2079
+ error: { label: "Drive: error", tone: "danger" },
2080
+ skipped: { label: "Drive: omitido", tone: "warning" }
2081
+ };
2082
+ const driveView = (links, key, parentInside) => {
2083
+ if (!links?.connected) return null;
2084
+ const item = links.items[key];
2085
+ if (item?.inside) {
2086
+ return { ...LABELS[item.status], title: item.error ?? void 0, action: item.root ? "unmark" : null };
2087
+ }
2088
+ if (parentInside) return { ...LABELS.pending, action: null };
2089
+ return { label: null, tone: "neutral", action: "mark" };
2090
+ };
2091
+ const serverMessage = (error, fallback) => error?.response?.data?.error?.message ?? fallback;
2092
+ const useDriveSync = (watching = false) => {
2093
+ const { get, post, del } = useFetchClient();
2094
+ const [links, setLinks] = useState(null);
2095
+ const refresh = useCallback(async () => {
2096
+ try {
2097
+ const { data } = await get(`${BASE}/links`);
2098
+ setLinks(data);
2099
+ } catch {
2100
+ setLinks(null);
2101
+ }
2102
+ }, [get]);
2103
+ useEffect(() => {
2104
+ refresh();
2105
+ }, [refresh]);
2106
+ const busy = !!links?.connected && (watching || links.pending > 0 || Object.values(links.items).some((i) => i.status === "pending"));
2107
+ useEffect(() => {
2108
+ if (!busy) return;
2109
+ const timer = setInterval(refresh, POLL_MS);
2110
+ return () => clearInterval(timer);
2111
+ }, [busy, refresh]);
2112
+ useEffect(() => {
2113
+ const onVisible = () => document.visibilityState === "visible" && refresh();
2114
+ document.addEventListener("visibilitychange", onVisible);
2115
+ return () => document.removeEventListener("visibilitychange", onVisible);
2116
+ }, [refresh]);
2117
+ return useMemo(
2118
+ () => ({
2119
+ links,
2120
+ refresh,
2121
+ async mark(cmsType, cmsId) {
2122
+ try {
2123
+ await post(`${BASE}/links`, { cmsType, cmsId });
2124
+ } catch (error) {
2125
+ throw new Error(serverMessage(error, "No se pudo sincronizar con Drive."));
2126
+ } finally {
2127
+ await refresh();
2128
+ }
2129
+ },
2130
+ async unmark(cmsType, cmsId) {
2131
+ try {
2132
+ await del(`${BASE}/links/${cmsType}/${cmsId}`);
2133
+ } catch (error) {
2134
+ throw new Error(serverMessage(error, "No se pudo dejar de sincronizar."));
2135
+ } finally {
2136
+ await refresh();
2137
+ }
2138
+ }
2139
+ }),
2140
+ [links, refresh, post, del]
2141
+ );
2142
+ };
1891
2143
  const status = (err) => err?.response?.status ?? err?.status;
1892
2144
  const LibraryTab = () => {
1893
2145
  const c = useThemeColors();
1894
2146
  const { api, notify } = useLibraryApi();
2147
+ const [drivePending, setDrivePending] = useState(false);
2148
+ const drive = useDriveSync(drivePending);
1895
2149
  const [tree, setTree] = useState([]);
1896
2150
  const [counts, setCounts] = useState({});
1897
2151
  const [current, setCurrent] = useState(null);
@@ -1935,7 +2189,20 @@ const LibraryTab = () => {
1935
2189
  useEffect(() => {
1936
2190
  loadFiles();
1937
2191
  }, [loadFiles]);
1938
- const refresh = () => Promise.all([loadTree(), loadFiles()]);
2192
+ const refresh = () => Promise.all([loadTree(), loadFiles(), drive.refresh()]);
2193
+ const toggleDrive = async (mode, cmsType, cmsId) => {
2194
+ try {
2195
+ if (mode === "mark") {
2196
+ await drive.mark(cmsType, cmsId);
2197
+ notify("success", "Se sincronizará con Drive en unos segundos.");
2198
+ } else {
2199
+ await drive.unmark(cmsType, cmsId);
2200
+ notify("success", "Dejó de sincronizarse. No se borró nada en Drive ni aquí.");
2201
+ }
2202
+ } catch (err) {
2203
+ notify("danger", err instanceof Error ? err.message : "No se pudo completar la operación con Drive.");
2204
+ }
2205
+ };
1939
2206
  const run = async (action, ok) => {
1940
2207
  try {
1941
2208
  await action();
@@ -1992,12 +2259,24 @@ const LibraryTab = () => {
1992
2259
  if (action === "rename") return setDialog({ kind: "renameFile", file });
1993
2260
  if (action === "move") return setDialog({ kind: "moveFile", file });
1994
2261
  if (action === "delete") return setDialog({ kind: "deleteFile", file });
2262
+ if (action === "syncDrive") return toggleDrive("mark", "file", file.id);
2263
+ if (action === "unsyncDrive") return toggleDrive("unmark", "file", file.id);
1995
2264
  };
1996
2265
  const validateFolder = (parent, except) => (name) => cleanFolderName(name) !== except && nameTaken(tree, parent, name) ? "Ya existe una carpeta con ese nombre aquí." : null;
2266
+ const pendingShown = (() => {
2267
+ const inside = current !== null && !!drive.links?.items[`folder:${current}`]?.inside;
2268
+ const isPending = (key) => driveView(drive.links, key, inside)?.label === "Drive: pendiente";
2269
+ return files.some((f) => isPending(`file:${f.id}`)) || childrenOf(tree, current).some((f) => isPending(`folder:${f.id}`));
2270
+ })();
2271
+ useEffect(() => {
2272
+ setDrivePending(pendingShown);
2273
+ }, [pendingShown]);
1997
2274
  if (forbidden) {
1998
2275
  return /* @__PURE__ */ jsx(EmptyNote, { children: "Para usar la biblioteca necesitas permiso de la Media Library de Strapi. Pídelo a un administrador." });
1999
2276
  }
2000
2277
  const subfolders = childrenOf(tree, current);
2278
+ const currentInside = current !== null && !!drive.links?.items[`folder:${current}`]?.inside;
2279
+ const folderDrive = current !== null ? driveView(drive.links, `folder:${current}`, !!(folder && path.length > 1 && drive.links?.items[`folder:${path[path.length - 2].id}`]?.inside)) : null;
2001
2280
  return /* @__PURE__ */ jsxs(Flex, { gap: 4, alignItems: "stretch", children: [
2002
2281
  /* @__PURE__ */ jsx(Box, { width: "280px", shrink: 0, background: "neutral0", padding: 3, hasRadius: true, style: { maxHeight: "calc(100vh - 220px)", overflowY: "auto" }, children: /* @__PURE__ */ jsx(
2003
2282
  FolderTree,
@@ -2044,6 +2323,9 @@ const LibraryTab = () => {
2044
2323
  ] }, f.id))
2045
2324
  ] }),
2046
2325
  /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
2326
+ folder && folderDrive?.label && /* @__PURE__ */ jsx(Badge, { variant: folderDrive.tone, title: folderDrive.title, children: folderDrive.label }),
2327
+ folder && folderDrive?.action === "mark" && /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => toggleDrive("mark", "folder", folder.id), children: "Sincronizar con Drive" }),
2328
+ folder && folderDrive?.action === "unmark" && /* @__PURE__ */ jsx(Button, { variant: "tertiary", onClick: () => toggleDrive("unmark", "folder", folder.id), children: "Dejar de sincronizar" }),
2047
2329
  folder && /* @__PURE__ */ jsxs(Fragment, { children: [
2048
2330
  /* @__PURE__ */ jsx(Button, { variant: "tertiary", startIcon: /* @__PURE__ */ jsx(Pencil, {}), onClick: () => setDialog({ kind: "renameFolder" }), children: "Renombrar carpeta" }),
2049
2331
  /* @__PURE__ */ jsx(Button, { variant: "danger-light", startIcon: /* @__PURE__ */ jsx(Trash, {}), onClick: () => setDialog({ kind: "deleteFolder" }), children: "Eliminar carpeta" })
@@ -2068,11 +2350,15 @@ const LibraryTab = () => {
2068
2350
  children: /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
2069
2351
  /* @__PURE__ */ jsx(Folder, { width: 16, height: 16, "aria-hidden": true }),
2070
2352
  /* @__PURE__ */ jsx(Typography, { ellipsis: true, style: { flex: 1 }, children: f.name }),
2353
+ (() => {
2354
+ const view = driveView(drive.links, `folder:${f.id}`, currentInside);
2355
+ return view?.label ? /* @__PURE__ */ jsx(Badge, { size: "S", variant: view.tone, title: view.title, children: view.label }) : null;
2356
+ })(),
2071
2357
  /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: counts[f.id]?.files ?? 0 })
2072
2358
  ] })
2073
2359
  }
2074
2360
  ) }, f.id)) }),
2075
- loading2 ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: "Cargando archivos…" }) }) : files.length === 0 ? /* @__PURE__ */ jsx(EmptyNote, { children: subfolders.length ? "No hay archivos sueltos en esta carpeta." : "Carpeta vacía. Arrastra archivos aquí o usa «Subir archivos»." }) : /* @__PURE__ */ jsx(Grid.Root, { gap: 4, children: files.map((f) => /* @__PURE__ */ jsx(Grid.Item, { col: 3, m: 4, s: 6, xs: 12, children: /* @__PURE__ */ jsx(FileCard, { file: f, onAction: onFileAction, busy: bgBusy[f.id] }) }, f.id)) }),
2361
+ loading2 ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: "Cargando archivos…" }) }) : files.length === 0 ? /* @__PURE__ */ jsx(EmptyNote, { children: subfolders.length ? "No hay archivos sueltos en esta carpeta." : "Carpeta vacía. Arrastra archivos aquí o usa «Subir archivos»." }) : /* @__PURE__ */ jsx(Grid.Root, { gap: 4, children: files.map((f) => /* @__PURE__ */ jsx(Grid.Item, { col: 3, m: 4, s: 6, xs: 12, children: /* @__PURE__ */ jsx(FileCard, { file: f, onAction: onFileAction, busy: bgBusy[f.id], drive: driveView(drive.links, `file:${f.id}`, currentInside) }) }, f.id)) }),
2076
2362
  dragOver && /* @__PURE__ */ jsx(Flex, { position: "absolute", justifyContent: "center", background: "primary100", hasRadius: true, style: { inset: 0, border: `2px dashed ${c.primary600}`, opacity: 0.95 }, children: /* @__PURE__ */ jsxs(Typography, { variant: "beta", textColor: "primary700", children: [
2077
2363
  "Suelta para subir a «",
2078
2364
  folder?.name ?? ROOT_LABEL,
@@ -2184,17 +2470,6 @@ const LibraryTab = () => {
2184
2470
  /* @__PURE__ */ jsx(RemoveBackgroundDialog, { open: !!bgTarget, src: bgTarget?.src ?? "", onClose: () => setBgTarget(null), onApply: saveNoBackground })
2185
2471
  ] });
2186
2472
  };
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
2473
  const NAVY = "#1F2A44";
2199
2474
  const RED$1 = "#E4002B";
2200
2475
  const TEAL$1 = "#00C7B1";
@@ -2765,6 +3040,15 @@ const editorReducer = (state, action) => {
2765
3040
  }
2766
3041
  }
2767
3042
  };
3043
+ const ORIGIN_URL = "/strapi-content-generator-gemini/drive/origin";
3044
+ const recordTemplateOrigin = async (post, files, template) => {
3045
+ for (const file of files) {
3046
+ try {
3047
+ await post(ORIGIN_URL, { fileId: file.id, templateDocumentId: template.documentId ?? template.id, templateName: template.name });
3048
+ } catch {
3049
+ }
3050
+ }
3051
+ };
2768
3052
  const useEditorIO = () => {
2769
3053
  const { post } = useFetchClient();
2770
3054
  const { toggleNotification } = useNotification();
@@ -2797,7 +3081,8 @@ const useEditorIO = () => {
2797
3081
  if (!node) return false;
2798
3082
  try {
2799
3083
  const blob = await renderPiece(node, template, "png");
2800
- await api.saveBlob(blob, /\.png$/i.test(name) ? name : `${name}.png`, folderId);
3084
+ const saved = await api.saveBlob(blob, /\.png$/i.test(name) ? name : `${name}.png`, folderId);
3085
+ await recordTemplateOrigin((u, body) => post(u, body), saved, template);
2801
3086
  toggleNotification({ type: "success", message: "Pieza guardada en la biblioteca." });
2802
3087
  return true;
2803
3088
  } catch {
@@ -2805,7 +3090,7 @@ const useEditorIO = () => {
2805
3090
  return false;
2806
3091
  }
2807
3092
  },
2808
- [api, toggleNotification]
3093
+ [api, post, toggleNotification]
2809
3094
  );
2810
3095
  return { loadImage: loadImage2, exportAs, saveToLibrary };
2811
3096
  };
@@ -3934,44 +4219,63 @@ const SaveTemplateDialog = ({ open, defaultName, defaultFolder, folders, onClose
3934
4219
  ] })
3935
4220
  ] }) });
3936
4221
  };
4222
+ const parseHex = (raw) => {
4223
+ const hex = raw.trim().replace(/^#/, "");
4224
+ if (/^[0-9a-f]{3}$/i.test(hex)) return `#${[...hex].map((c) => c + c).join("")}`.toUpperCase();
4225
+ if (/^[0-9a-f]{6}$/i.test(hex)) return `#${hex}`.toUpperCase();
4226
+ return null;
4227
+ };
3937
4228
  const ColorField = ({ label, value, onChange }) => {
3938
4229
  const tc = useThemeColors();
4230
+ const current = parseHex(value ?? "") ?? "#000000";
4231
+ const [draft, setDraft] = useState(current);
4232
+ useEffect(() => setDraft((d) => parseHex(d) === current ? d : current), [current]);
4233
+ const apply = (text) => {
4234
+ const hex = parseHex(text);
4235
+ if (hex && hex !== current) onChange(hex);
4236
+ };
4237
+ const typeHex = (text) => {
4238
+ setDraft(text);
4239
+ if (text.trim().replace(/^#/, "").length === 6) apply(text);
4240
+ };
4241
+ const leave = () => {
4242
+ apply(draft);
4243
+ setDraft(parseHex(draft) ?? current);
4244
+ };
3939
4245
  return /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 1, children: [
3940
4246
  /* @__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
- }),
4247
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
3967
4248
  /* @__PURE__ */ jsx(
3968
4249
  "input",
3969
4250
  {
3970
4251
  type: "color",
3971
- "aria-label": `${label} personalizado`,
3972
- value: /^#[0-9a-f]{6}$/i.test(value ?? "") ? value : "#000000",
4252
+ "aria-label": `${label}: selector`,
4253
+ title: "Abrir selector de color",
4254
+ value: current.toLowerCase(),
3973
4255
  onChange: (e) => onChange(e.target.value.toUpperCase()),
3974
- style: { width: 26, height: 26, border: "none", padding: 0, background: "none", cursor: "pointer" }
4256
+ style: {
4257
+ flexShrink: 0,
4258
+ width: 32,
4259
+ height: 32,
4260
+ padding: 2,
4261
+ borderRadius: 4,
4262
+ cursor: "pointer",
4263
+ border: `1px solid ${tc.neutral200}`,
4264
+ background: tc.neutral0
4265
+ }
4266
+ }
4267
+ ),
4268
+ /* @__PURE__ */ jsx(
4269
+ TextInput,
4270
+ {
4271
+ "aria-label": `${label}: hexadecimal`,
4272
+ size: "S",
4273
+ value: draft,
4274
+ placeholder: "#1F2A44",
4275
+ maxLength: 7,
4276
+ spellCheck: false,
4277
+ onChange: (e) => typeHex(e.target.value),
4278
+ onBlur: leave
3975
4279
  }
3976
4280
  )
3977
4281
  ] })
@@ -5231,7 +5535,8 @@ const TemplatesTab = () => {
5231
5535
  const STUDIO_TABS = [
5232
5536
  { value: "generator", label: "Generador" },
5233
5537
  { value: "templates", label: "Plantillas" },
5234
- { value: "library", label: "Imágenes y documentos" }
5538
+ { value: "library", label: "Imágenes y documentos" },
5539
+ { value: "drive", label: "Google Drive" }
5235
5540
  ];
5236
5541
  const PANEL = { height: "100%", overflowY: "auto" };
5237
5542
  const isStudioTab = (v) => STUDIO_TABS.some((t) => t.value === v);
@@ -5259,7 +5564,8 @@ const ImageStudioTabs = () => {
5259
5564
  /* @__PURE__ */ jsxs(Box, { flex: "1", style: { minHeight: 0 }, children: [
5260
5565
  /* @__PURE__ */ jsx(Tabs.Content, { value: "generator", style: PANEL, children: /* @__PURE__ */ jsx(Box, { padding: 6, children: /* @__PURE__ */ jsx(GeneratorTab, {}) }) }),
5261
5566
  /* @__PURE__ */ jsx(Tabs.Content, { value: "templates", style: PANEL, children: /* @__PURE__ */ jsx(TemplatesTab, {}) }),
5262
- /* @__PURE__ */ jsx(Tabs.Content, { value: "library", style: PANEL, children: /* @__PURE__ */ jsx(Box, { padding: 6, children: /* @__PURE__ */ jsx(LibraryTab, {}) }) })
5567
+ /* @__PURE__ */ jsx(Tabs.Content, { value: "library", style: PANEL, children: /* @__PURE__ */ jsx(Box, { padding: 6, children: /* @__PURE__ */ jsx(LibraryTab, {}) }) }),
5568
+ /* @__PURE__ */ jsx(Tabs.Content, { value: "drive", style: PANEL, children: /* @__PURE__ */ jsx(Box, { padding: 6, children: tab === "drive" && /* @__PURE__ */ jsx(DriveTab, {}) }) })
5263
5569
  ] })
5264
5570
  ]
5265
5571
  }