diff --git a/src/app/api/brandbook/google-fonts/route.ts b/src/app/api/brandbook/google-fonts/route.ts
new file mode 100644
index 0000000..0416b2d
--- /dev/null
+++ b/src/app/api/brandbook/google-fonts/route.ts
@@ -0,0 +1,44 @@
+/**
+ * GET ?q=&category=&limit=&offset= — full free Google Fonts catalog.
+ */
+import { NextRequest, NextResponse } from 'next/server';
+import { createClient } from '@/lib/supabase/server';
+import { listGoogleFonts } from '@/lib/google-fonts-catalog';
+
+export const dynamic = 'force-dynamic';
+
+export async function GET(req: NextRequest) {
+ const supabase = await createClient();
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
+ if (!user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ const sp = req.nextUrl.searchParams;
+ try {
+ const result = await listGoogleFonts({
+ q: sp.get('q') || undefined,
+ category: sp.get('category') || undefined,
+ limit: Number(sp.get('limit') || 80) || 80,
+ offset: Number(sp.get('offset') || 0) || 0,
+ });
+ return NextResponse.json({
+ ok: true,
+ source: 'google-fonts',
+ license: 'All Google Fonts are free and open source (OFL / Apache / Ubuntu…)',
+ ...result,
+ });
+ } catch (e) {
+ return NextResponse.json(
+ {
+ error:
+ e instanceof Error
+ ? e.message
+ : 'No se pudo cargar el catálogo de Google Fonts',
+ },
+ { status: 502 }
+ );
+ }
+}
diff --git a/src/components/BrandZone.tsx b/src/components/BrandZone.tsx
index fec8bb3..be3f828 100644
--- a/src/components/BrandZone.tsx
+++ b/src/components/BrandZone.tsx
@@ -30,6 +30,7 @@ import {
type ColorToken,
type FontToken,
} from '@/lib/brandbook';
+import GoogleFontsGallery from '@/components/GoogleFontsGallery';
function Field({
label,
@@ -806,18 +807,47 @@ export default function BrandZone({ tenantLabel }: { tenantLabel: string }) {
-
+
+
f.family)}
+ onPick={(font, pickRole) => {
+ const exists = book.fonts_kit.some(
+ (f) =>
+ f.family.toLowerCase() === font.family.toLowerCase()
+ );
+ if (exists) {
+ setOkMsg(`${font.family} ya está en el kit`);
+ return;
+ }
+ const weights = font.variants
+ .filter((v) => /^\d+$/.test(v))
+ .slice(0, 6)
+ .join(',');
+ const token: FontToken = {
+ id: newId('font'),
+ family: font.family,
+ role: pickRole,
+ weights: weights || '400,700',
+ source: 'google',
+ url_or_notes: `https://fonts.google.com/specimen/${encodeURIComponent(
+ font.family.replace(/ /g, '+')
+ )}`,
+ };
+ patch({ fonts_kit: [...book.fonts_kit, token] });
+ setOkMsg(`${font.family} añadida al kit (${pickRole})`);
+ }}
+ />
- Tipografías
+ Tipografías del kit
diff --git a/src/components/GoogleFontsGallery.tsx b/src/components/GoogleFontsGallery.tsx
new file mode 100644
index 0000000..3a94699
--- /dev/null
+++ b/src/components/GoogleFontsGallery.tsx
@@ -0,0 +1,244 @@
+'use client';
+
+import React, { useEffect, useMemo, useState } from 'react';
+import { Check, Loader2, Search, Type } from 'lucide-react';
+
+export type GoogleFontPick = {
+ family: string;
+ category: string;
+ variants: string[];
+};
+
+type ApiFont = {
+ family: string;
+ category: string;
+ variants: string[];
+ subsets: string[];
+ popularity: number;
+};
+
+function loadPreviewCss(families: string[]) {
+ if (typeof document === 'undefined' || !families.length) return;
+ const id = 'gf-brand-preview';
+ let link = document.getElementById(id) as HTMLLinkElement | null;
+ // CSS2 API: multiple families, limited weights for preview
+ const parts = families.slice(0, 40).map((f) => {
+ const fam = encodeURIComponent(f).replace(/%20/g, '+');
+ return `family=${fam}:wght@400;700`;
+ });
+ const href = `https://fonts.googleapis.com/css2?${parts.join('&')}&display=swap`;
+ if (!link) {
+ link = document.createElement('link');
+ link.id = id;
+ link.rel = 'stylesheet';
+ document.head.appendChild(link);
+ }
+ if (link.href !== href) link.href = href;
+}
+
+export default function GoogleFontsGallery({
+ selectedFamilies,
+ onPick,
+}: {
+ selectedFamilies: string[];
+ onPick: (font: GoogleFontPick, role: 'display' | 'headline' | 'body' | 'ui') => void;
+}) {
+ const [q, setQ] = useState('');
+ const [category, setCategory] = useState('all');
+ const [categories, setCategories] = useState
([]);
+ const [fonts, setFonts] = useState([]);
+ const [total, setTotal] = useState(0);
+ const [offset, setOffset] = useState(0);
+ const [loading, setLoading] = useState(true);
+ const [err, setErr] = useState(null);
+ const [role, setRole] = useState<'display' | 'headline' | 'body' | 'ui'>(
+ 'body'
+ );
+ const limit = 60;
+
+ const selectedSet = useMemo(
+ () => new Set(selectedFamilies.map((f) => f.toLowerCase())),
+ [selectedFamilies]
+ );
+
+ useEffect(() => {
+ let cancelled = false;
+ const t = setTimeout(async () => {
+ setLoading(true);
+ setErr(null);
+ try {
+ const params = new URLSearchParams({
+ q,
+ category,
+ limit: String(limit),
+ offset: String(offset),
+ });
+ const r = await fetch(`/api/brandbook/google-fonts?${params}`);
+ const j = await r.json();
+ if (!r.ok) throw new Error(j.error || 'Error catálogo');
+ if (cancelled) return;
+ setFonts(j.fonts || []);
+ setTotal(j.total || 0);
+ setCategories(j.categories || []);
+ } catch (e) {
+ if (!cancelled) {
+ setErr(e instanceof Error ? e.message : 'Error');
+ setFonts([]);
+ }
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ }, q ? 250 : 0);
+ return () => {
+ cancelled = true;
+ clearTimeout(t);
+ };
+ }, [q, category, offset]);
+
+ useEffect(() => {
+ loadPreviewCss(fonts.map((f) => f.family));
+ }, [fonts]);
+
+ return (
+
+
+
+
+ Galería Google Fonts (gratis)
+
+
+ {total.toLocaleString('es-ES')} tipografías libres · OFL / Apache
+
+
+
+
+
+
+ {
+ setOffset(0);
+ setQ(e.target.value);
+ }}
+ placeholder="Buscar fuente (Inter, Playfair, Roboto…)"
+ className="w-full rounded-lg border border-gray-200 bg-white pl-8 pr-3 py-2 text-sm"
+ />
+
+
+
+
+
+ {err ? (
+
{err}
+ ) : null}
+
+ {loading ? (
+
+
+ Cargando catálogo de Google Fonts…
+
+ ) : (
+
+ {fonts.map((f) => {
+ const on = selectedSet.has(f.family.toLowerCase());
+ return (
+
+ );
+ })}
+
+ )}
+
+
+
+
+ {total
+ ? `${offset + 1}–${Math.min(offset + limit, total)} de ${total}`
+ : '—'}
+
+
+
+
+ );
+}
diff --git a/src/lib/google-fonts-catalog.ts b/src/lib/google-fonts-catalog.ts
new file mode 100644
index 0000000..4393249
--- /dev/null
+++ b/src/lib/google-fonts-catalog.ts
@@ -0,0 +1,112 @@
+/**
+ * Full free Google Fonts catalog (entire Google Fonts library is free/OSS).
+ * Fetched server-side from fonts.google.com metadata; cached in memory.
+ */
+
+export type GoogleFontEntry = {
+ family: string;
+ category: string;
+ variants: string[];
+ subsets: string[];
+ popularity: number;
+};
+
+type Cache = {
+ at: number;
+ fonts: GoogleFontEntry[];
+};
+
+const TTL_MS = 24 * 60 * 60 * 1000;
+let cache: Cache | null = null;
+
+function specimenUrl(family: string): string {
+ return `https://fonts.google.com/specimen/${encodeURIComponent(
+ family.replace(/ /g, '+')
+ )}`;
+}
+
+export function googleFontSpecimenUrl(family: string): string {
+ return specimenUrl(family);
+}
+
+export function googleFontCssUrl(
+ family: string,
+ weights = '400;500;600;700'
+): string {
+ const fam = family.replace(/ /g, '+');
+ return `https://fonts.googleapis.com/css2?family=${fam}:wght@${weights}&display=swap`;
+}
+
+async function fetchCatalog(): Promise {
+ if (cache && Date.now() - cache.at < TTL_MS) return cache.fonts;
+
+ // Official metadata (all free fonts on Google Fonts)
+ const r = await fetch('https://fonts.google.com/metadata/fonts', {
+ headers: {
+ Accept: 'application/json',
+ 'User-Agent': '4Ventures-CentroDeMando/1.0',
+ },
+ next: { revalidate: 86400 },
+ });
+ if (!r.ok) {
+ throw new Error(`Google Fonts metadata ${r.status}`);
+ }
+ let text = await r.text();
+ // Response is prefixed with )]}'
+ if (text.startsWith(")]}'")) {
+ text = text.replace(/^\)\]\}'\n?/, '');
+ }
+ const json = JSON.parse(text) as {
+ familyMetadataList?: Array<{
+ family?: string;
+ category?: string;
+ fonts?: Record;
+ subsets?: string[];
+ popularity?: number;
+ }>;
+ };
+ const list = json.familyMetadataList || [];
+ const fonts: GoogleFontEntry[] = list
+ .filter((f) => f.family)
+ .map((f, i) => ({
+ family: String(f.family),
+ category: String(f.category || 'sans-serif'),
+ variants: f.fonts ? Object.keys(f.fonts) : ['400', '700'],
+ subsets: Array.isArray(f.subsets) ? f.subsets.map(String) : ['latin'],
+ popularity: typeof f.popularity === 'number' ? f.popularity : i,
+ }))
+ .sort((a, b) => a.popularity - b.popularity || a.family.localeCompare(b.family));
+
+ cache = { at: Date.now(), fonts };
+ return fonts;
+}
+
+export async function listGoogleFonts(opts?: {
+ q?: string;
+ category?: string;
+ limit?: number;
+ offset?: number;
+}): Promise<{
+ total: number;
+ fonts: GoogleFontEntry[];
+ categories: string[];
+}> {
+ const all = await fetchCatalog();
+ const categories = [...new Set(all.map((f) => f.category))].sort();
+ const q = (opts?.q || '').trim().toLowerCase();
+ const cat = (opts?.category || '').trim().toLowerCase();
+ let filtered = all;
+ if (cat && cat !== 'all') {
+ filtered = filtered.filter((f) => f.category.toLowerCase() === cat);
+ }
+ if (q) {
+ filtered = filtered.filter((f) => f.family.toLowerCase().includes(q));
+ }
+ const offset = Math.max(0, opts?.offset || 0);
+ const limit = Math.min(200, Math.max(1, opts?.limit || 60));
+ return {
+ total: filtered.length,
+ fonts: filtered.slice(offset, offset + limit),
+ categories,
+ };
+}