/* --- Icons.jsx --- */
(() => {
/* Tecto UI Kit — icon set (Lucide-style, 1.75 stroke). Exposed on window.TectoIcons */
const React = window.React;
function Ico({ size = 18, children, ...rest }) {
return (
);
}
const Play = (p) => ;
const Download = (p) => ;
const Save = (p) => ;
const Sun = (p) => ;
const Moon = (p) => ;
const FileText = (p) => ;
const Folder = (p) => ;
const Clock = (p) => ;
const Settings = (p) => ;
const Search = (p) => ;
const PanelLeft = (p) => ;
const Columns = (p) => ;
const Plus = (p) => ;
const Check = (p) => ;
const X = (p) => ;
const Alert = (p) => ;
const Lock = (p) => ;
const Zap = (p) => ;
const Menu = (p) => ;
const More = (p) => ;
const Copy = (p) => ;
const Trash = (p) => ;
const Chevron = (p) => ;
const ZoomIn = (p) => ;
const Maximize = (p) => ;
const GitHub = (p) => ;
const Layout = (p) => ;
const Image = (p) => ;
const Building = (p) => ;
const Calendar = (p) => ;
const Hash = (p) => ;
const Type = (p) => ;
const Sparkle = (p) => ;
const Stamp = (p) => ;
window.TectoIcons = { Play, Download, Save, Sun, Moon, FileText, Folder, Clock, Settings, Search, PanelLeft, Columns, Plus, Check, X, Alert, Lock, Zap, Menu, More, Copy, Trash, Chevron, ZoomIn, Maximize, GitHub, Layout, Image, Building, Calendar, Hash, Type, Sparkle, Stamp };
})();
/* --- Chrome.jsx --- */
(() => {
/* Tecto UI Kit — logo lockup + app chrome (TopBar, LeftRail, StatusBar). window.TectoChrome */
const React = window.React;
(function injectChromeCSS() {
if (document.getElementById('tecto-kit-chrome-css')) return;
const css = `
.tk-logo { display: inline-flex; align-items: center; gap: 9px; }
.tk-logo__mark { width: 26px; height: 26px; border-radius: 7px; flex: none; }
.tk-logo__word { font-family: var(--font-serif); font-size: 19px; font-weight: 600; letter-spacing: -0.02em; color: var(--ink-strong); }
.tk-topbar { display: flex; align-items: center; gap: 10px; height: var(--topbar-h); padding: 0 12px; flex: none;
background: var(--surface); border-bottom: 1px solid var(--border); }
.tk-crumb { display: flex; align-items: center; gap: 7px; font-family: var(--font-sans); font-size: var(--text-sm); color: var(--ink-muted); }
.tk-crumb b { color: var(--ink-strong); font-weight: 600; font-family: var(--font-mono); font-size: var(--text-sm); }
.tk-topbar__spacer { flex: 1; }
.tk-topbar__grp { display: flex; align-items: center; gap: 8px; }
.tk-avatar { width: 28px; height: 28px; border-radius: 50%; background: var(--accent); color: var(--accent-on);
display: inline-flex; align-items: center; justify-content: center; font-family: var(--font-sans); font-size: 12px; font-weight: 600; flex: none; }
.tk-rail { width: var(--rail-w); flex: none; background: var(--surface-sunken); border-right: 1px solid var(--border);
display: flex; flex-direction: column; align-items: center; padding: 10px 0; gap: 4px; }
.tk-rail__btn { width: 38px; height: 38px; border-radius: var(--radius-md); border: none; background: transparent; cursor: pointer;
display: inline-flex; align-items: center; justify-content: center; color: var(--ink-muted);
transition: background var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out); position: relative; }
.tk-rail__btn:hover { background: var(--surface-hover); color: var(--ink); }
.tk-rail__btn--active { background: var(--accent-subtle-bg); color: var(--accent-subtle-fg); }
.tk-rail__btn--active::before { content: ""; position: absolute; left: -10px; top: 9px; bottom: 9px; width: 3px; border-radius: 3px; background: var(--accent); }
.tk-rail__sp { flex: 1; }
.tk-status { display: flex; align-items: center; gap: 14px; height: var(--statusbar-h); padding: 0 14px; flex: none;
background: var(--surface-sunken); border-top: 1px solid var(--border); font-family: var(--font-mono); font-size: 11px; color: var(--ink-muted); }
.tk-status__sp { flex: 1; }
.tk-status b { color: var(--ink); font-weight: 500; }
`;
const el = document.createElement('style');
el.id = 'tecto-kit-chrome-css';
el.textContent = css;
document.head.appendChild(el);
})();
function Logo({ word = true }) {
return (
{word && Tecto}
);
}
function TopBar({ crumb, theme, onToggleTheme }) {
const I = window.TectoIcons;
const { IconButton, Tooltip } = window.TectoDS;
return (
{(crumb || []).map((c, i) => (
{i > 0 && }
{i === crumb.length - 1 ? {c} : {c}}
))}
} />
: } onClick={onToggleTheme} />
T
);
}
function LeftRail({ view, setView, onLogout }) {
const I = window.TectoIcons;
const { Tooltip } = window.TectoDS;
const items = [
{ id: 'docs', label: 'Documentos', icon: I.Folder },
{ id: 'templates', label: 'Plantillas', icon: I.Layout },
{ id: 'assets', label: 'Assets', icon: I.Image },
{ id: 'docview', label: 'Documentación', icon: I.FileText },
{ id: 'editor', label: 'Editor de plantilla', icon: I.PanelLeft },
];
return (
{items.map((it) => (
))}
);
}
function StatusBar({ engine }) {
return (
Tectonic 0.15
{engine}
UTF-8
LaTeX
Ln 23, Col 4
112 palabras
main.tex · guardado
);
}
window.TectoChrome = { Logo, TopBar, LeftRail, StatusBar };
})();
/* --- CorpDocs.jsx --- */
(() => {
/* Tecto UI Kit — corporate document renders (the PDF output). window.TectoCorp */
const React = window.React;
const DEFAULT_BRAND = {
name: 'Acme Estudio',
initials: 'A',
color: '#1f3a5f',
tint: '#eef2f7',
rfc: 'ACM850101AA1',
address: 'Av. Reforma 222, CDMX · acme.studio',
};
const DEFAULT_DATA = {
numero: '0123',
fecha: '7 jun 2026',
validez: '22 jun 2026',
cliente: 'Globex Corporation',
clienteRfc: 'GLO910215QX3',
moneda: 'USD',
iva: true,
items: [
{ desc: 'Diseño e implementación de API REST', qty: 1, price: 3200 },
{ desc: 'Integración con pasarela de pago', qty: 1, price: 1400 },
{ desc: 'Soporte y mantenimiento (mensual)', qty: 3, price: 400 },
],
notas: 'Validez de la oferta: 15 días. Precios en USD, no incluyen retenciones. 50% anticipo, 50% contra entrega.',
};
function fmtMoney(n, moneda) {
const s = n.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
return `$${s} ${moneda}`;
}
function totals(data) {
const subtotal = data.items.reduce((a, it) => a + it.qty * it.price, 0);
const iva = data.iva ? subtotal * 0.16 : 0;
return { subtotal, iva, total: subtotal + iva };
}
function CotizacionDoc({ data = DEFAULT_DATA, brand = DEFAULT_BRAND }) {
const t = totals(data);
const ink = '#1a2430';
const muted = '#5d6b7a';
return (
{/* Header */}
{brand.initials}
{brand.name}
{brand.address}
COTIZACIÓN
N.º {data.numero}
{/* Meta */}
Para
{data.cliente}
RFC {data.clienteRfc}
{[['Fecha', data.fecha], ['Válida hasta', data.validez], ['Moneda', data.moneda]].map(([k, v]) => (
{k}{v}
))}
{/* Items */}
DescripciónCantP. UnitImporte
{data.items.map((it, i) => (
{it.desc}
{it.qty}
{it.price.toLocaleString('es-MX', { minimumFractionDigits: 2 })}
{(it.qty * it.price).toLocaleString('es-MX', { minimumFractionDigits: 2 })}
))}
{/* Totals */}
|
{data.iva &&
|
}
Total
{fmtMoney(t.total, data.moneda)}
{/* Notes */}
{/* Footer */}
{brand.name} · RFC {brand.rfc}
{brand.address}
1 / 1
);
}
function Row({ k, v, muted }) {
return (
{k}
{v}
);
}
window.TectoCorp = { CotizacionDoc, fmtMoney, totals, DEFAULT_BRAND, DEFAULT_DATA };
})();
/* --- WorkspaceScreens.jsx --- */
(() => {
/* Tecto UI Kit — document-workspace screens: Documentos, Plantillas, Assets. window.TectoWorkspace */
const React = window.React;
(function injectWsCSS() {
if (document.getElementById('tecto-kit-ws-css')) return;
const css = `
.tk-ws { flex: 1; min-height: 0; overflow: auto; background: var(--bg); }
.tk-ws__in { max-width: 960px; margin: 0 auto; padding: 30px 36px 70px; }
.tk-ws__head { display: flex; align-items: flex-end; gap: 14px; margin-bottom: 6px; }
.tk-ws__title { font-family: var(--font-serif); font-size: 28px; font-weight: 600; letter-spacing: -0.02em; color: var(--ink-strong); margin: 0; }
.tk-ws__sub { font-family: var(--font-sans); font-size: var(--text-sm); color: var(--ink-muted); margin: 3px 0 0; }
.tk-ws__sp { flex: 1; }
.tk-ws__sec { font-family: var(--font-mono); font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-subtle); margin: 28px 0 13px; }
.tk-quick { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.tk-quick__c { display: flex; align-items: center; gap: 11px; padding: 13px 14px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); cursor: pointer; transition: border-color var(--dur-1) var(--ease-out), box-shadow var(--dur-2) var(--ease-out), transform var(--dur-2) var(--ease-out); }
.tk-quick__c:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); transform: translateY(-1px); }
.tk-quick__ic { width: 34px; height: 34px; border-radius: 8px; flex: none; display: flex; align-items: center; justify-content: center; color: #fff; }
.tk-quick__t b { display: block; font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; color: var(--ink-strong); }
.tk-quick__t span { font-family: var(--font-mono); font-size: 10.5px; color: var(--ink-muted); }
.tk-rows { display: flex; flex-direction: column; gap: 3px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 6px; box-shadow: var(--shadow-sm); }
.tk-tpls { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.tk-tplc { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); overflow: hidden; cursor: pointer; transition: border-color var(--dur-1) var(--ease-out), box-shadow var(--dur-2) var(--ease-out), transform var(--dur-2) var(--ease-out); }
.tk-tplc:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); transform: translateY(-2px); }
.tk-tplc__thumb { height: 132px; background: var(--bg-subtle); padding: 16px 16px 0; display: flex; justify-content: center; }
.tk-tplc__page { width: 100%; background: #fff; border-radius: 4px 4px 0 0; box-shadow: var(--shadow-md); padding: 11px; }
.tk-tplc__band { height: 8px; border-radius: 2px; width: 46%; }
.tk-tplc__ln { height: 3px; border-radius: 2px; background: #e4e7eb; margin-top: 6px; }
.tk-tplc__chip { height: 11px; width: 38%; border-radius: 2px; margin: 9px 0 0 auto; }
.tk-tplc__foot { display: flex; align-items: center; gap: 8px; padding: 11px 13px; }
.tk-tplc__foot b { font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; color: var(--ink-strong); }
.tk-tplc__foot span { font-family: var(--font-mono); font-size: 10.5px; color: var(--ink-muted); margin-left: auto; }
.tk-tplc--new { border-style: dashed; box-shadow: none; display: flex; align-items: center; justify-content: center; min-height: 188px; }
.tk-tplc--new:hover { border-color: var(--accent); background: var(--accent-subtle-bg); }
.tk-tplc__new { display: flex; flex-direction: column; align-items: center; gap: 4px; color: var(--ink-muted); }
.tk-tplc__new span { font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; color: var(--ink); margin-top: 4px; }
.tk-tplc__new em { font-family: var(--font-mono); font-size: 10px; font-style: normal; color: var(--ink-subtle); }
.tk-assets { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.tk-asset { display: flex; flex-direction: column; gap: 9px; padding: 13px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); }
.tk-asset__box { height: 76px; border-radius: var(--radius-sm); border: 1px dashed var(--border-strong); display: flex; align-items: center; justify-content: center; background: var(--surface-sunken); }
.tk-asset__n { font-family: var(--font-mono); font-size: 11px; color: var(--ink); }
.tk-asset__m { font-family: var(--font-sans); font-size: 10.5px; color: var(--ink-subtle); }
.tk-fonts { display: flex; flex-direction: column; gap: 3px; }
`;
const el = document.createElement('style');
el.id = 'tecto-kit-ws-css';
el.textContent = css;
document.head.appendChild(el);
})();
const TEMPLATES = [
{ name: 'Cotización', cls: 'cotizacion.cls', color: '#1f3a5f', icon: 'FileText' },
{ name: 'Factura', cls: 'factura.cls', color: '#1f6b4f', icon: 'Hash' },
{ name: 'Contrato', cls: 'contrato.cls', color: '#5b4636', icon: 'Stamp' },
{ name: 'Propuesta', cls: 'propuesta.cls', color: '#6d3a8f', icon: 'Sparkle' },
{ name: 'SRS', cls: 'srs.cls', color: '#2a5f8f', icon: 'Layout' },
{ name: 'Informe', cls: 'informe.cls', color: '#8a5a12', icon: 'FileText' },
];
function Documentos({ onNew, onOpenDoc }) {
const I = window.TectoIcons;
const { Button, StatusPill, DocRow, Badge } = window.TectoDS;
return (
Documentos
Elige una plantilla, llena los datos, descarga el PDF. Sin tocar LaTeX.
} onClick={() => onNew('Cotización')}>Nueva cotización
Crear nuevo
{TEMPLATES.map((t) => {
const Icon = I[t.icon] || I.FileText;
return (
onNew(t.name)}>
{t.name}{t.cls}
);
})}
Recientes
onOpenDoc('Cotización')} aside={} />
onOpenDoc('Cotización')} aside={} />
onOpenDoc('Cotización')} aside={} />
onOpenDoc('Cotización')} aside={} />
);
}
function Plantillas({ onNew, onEdit }) {
const I = window.TectoIcons;
const { Button, Badge, IconButton } = window.TectoDS;
return (
Plantillas
Tú las creas: una carpeta con su .cls, sus campos y sus assets (propios o globales). El Generador las usa con datos.
} onClick={() => onEdit('nueva')}>Nueva plantilla
Tus plantillas
onEdit('nueva')}>
Nueva plantillacarpeta + .cls + assets
{TEMPLATES.map((t) => (
onNew(t.name)}>
{t.name}{t.cls}
} onClick={(e) => { e.stopPropagation(); onEdit(t.name); }} />
))}
);
}
function Assets() {
const I = window.TectoIcons;
const { Badge } = window.TectoDS;
return (
Assets
Logos, fuentes e imágenes que comparten todas tus plantillas.
Logos
{[['acme-logo.svg', 'SVG · 1:1'], ['acme-horizontal.svg', 'SVG · 4:1'], ['acme-mono.svg', 'SVG · mono'], ['favicon.png', 'PNG · 64px']].map(([n, m]) => (
))}
Fuentes
{[['IBM Plex Serif', 'titulares'], ['IBM Plex Sans', 'cuerpo'], ['IBM Plex Mono', 'datos'], ['Söhne', 'marca · opcional']].map(([n, m]) => (
))}
Imágenes
{[['portada.jpg', '1920×1080'], ['sello.png', 'PNG · α'], ['firma.png', 'PNG · α'], ['marca-agua.svg', 'SVG']].map(([n, m]) => (
))}
);
}
window.TectoWorkspace = { Documentos, Plantillas, Assets };
})();
/* --- Generator.jsx --- */
(() => {
/* Tecto UI Kit — document Generator: form (left) → live corporate PDF (right). window.TectoGenerator */
const React = window.React;
(function injectGenCSS() {
if (document.getElementById('tecto-kit-gen-css')) return;
const css = `
.tk-gen { flex: 1; min-height: 0; display: flex; }
.tk-gen__form { width: 432px; flex: none; min-height: 0; overflow: auto; background: var(--surface); border-right: 1px solid var(--border); }
.tk-gen__form-in { padding: 20px 22px 60px; display: flex; flex-direction: column; gap: 16px; }
.tk-gen__head { display: flex; align-items: center; gap: 10px; }
.tk-gen__head h2 { font-family: var(--font-serif); font-size: 20px; font-weight: 600; letter-spacing: -0.01em; color: var(--ink-strong); margin: 0; }
.tk-gen__sec { font-family: var(--font-mono); font-size: 10px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-subtle); margin: 6px 0 -4px; }
.tk-gen__grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.tk-gen__item { display: grid; grid-template-columns: 1fr 52px 84px 28px; gap: 7px; align-items: center; }
.tk-gen__item-h { display: grid; grid-template-columns: 1fr 52px 84px 28px; gap: 7px; font-family: var(--font-mono); font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-subtle); padding: 0 2px; }
.tk-gen__totals { display: flex; flex-direction: column; gap: 4px; padding: 12px 14px; background: var(--surface-sunken); border: 1px solid var(--border); border-radius: var(--radius-md); font-family: var(--font-mono); font-size: 12px; }
.tk-gen__totals .r { display: flex; justify-content: space-between; }
.tk-gen__totals .t { font-weight: 600; color: var(--ink-strong); font-size: 14px; padding-top: 6px; margin-top: 2px; border-top: 1px solid var(--border-strong); }
.tk-gen__preview { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; background: var(--bg-subtle); }
.tk-gen__bar { display: flex; align-items: center; gap: 8px; height: 44px; padding: 0 14px; flex: none; background: var(--surface); border-bottom: 1px solid var(--border); }
.tk-gen__bar b { font-family: var(--font-mono); font-size: 12px; color: var(--ink-muted); }
.tk-gen__sp { flex: 1; }
.tk-gen__stage { flex: 1; min-height: 0; overflow: auto; padding: 28px; display: flex; justify-content: center; align-items: flex-start; position: relative; }
.tk-gen__page { width: 480px; background: var(--paper); box-shadow: var(--shadow-lg); border-radius: 2px; }
.tk-gen__overlay { position: absolute; inset: 0; display: flex; flex-direction: column; gap: 13px; align-items: center; justify-content: center; background: color-mix(in srgb, var(--bg-subtle) 78%, transparent); backdrop-filter: blur(1.5px); }
.tk-gen__overlay span { font-family: var(--font-mono); font-size: 13px; color: var(--ink-muted); }
`;
const el = document.createElement('style');
el.id = 'tecto-kit-gen-css';
el.textContent = css;
document.head.appendChild(el);
})();
function Generator({ data, setData, status, onCompile, onDownload, pdfUrl }) {
const I = window.TectoIcons;
const { Input, Select, Switch, Textarea, Button, IconButton, Badge, StatusPill } = window.TectoDS;
const TA = Textarea || ((p) => );
const { CotizacionDoc, fmtMoney, totals } = window.TectoCorp;
const t = totals(data);
const set = (k, v) => setData({ ...data, [k]: v });
const setItem = (i, k, v) => {
const items = data.items.map((it, j) => j === i ? { ...it, [k]: k === 'desc' ? v : (parseFloat(v) || 0) } : it);
setData({ ...data, items });
};
const addItem = () => setData({ ...data, items: [...data.items, { desc: '', qty: 1, price: 0 }] });
const delItem = (i) => setData({ ...data, items: data.items.filter((_, j) => j !== i) });
return (
cotizacion.cls
Nueva cotización
Cliente
set('cliente', e.target.value)} />
set('clienteRfc', e.target.value)} />
set('numero', e.target.value)} />
Vigencia
} value={data.fecha} onChange={(e) => set('fecha', e.target.value)} />
} value={data.validez} onChange={(e) => set('validez', e.target.value)} />
cotizacion-{data.numero}.pdf
} onClick={onCompile} loading={status === 'running'}>
{status === 'running' ? 'Compilando…' : 'Compilar'}
} onClick={onDownload} />
{pdfUrl ?
:
}
{status === 'running' && (
Componiendo con Tectonic…
)}
);
}
window.TectoGenerator = { Generator };
})();
/* --- TemplateEditor.jsx --- */
(() => {
/* Tecto UI Kit — Template authoring: .cls editor + Campos/Assets inspector. window.TectoTemplate */
const React = window.React;
(function injectTplCSS() {
if (document.getElementById('tecto-kit-tpl-css')) return;
const css = `
.tk-tpl-ws { flex: 1; min-height: 0; display: flex; }
.tk-tpl-ws > .tk-editor { flex: 1.1; min-width: 0; border-right: 1px solid var(--border); }
.tk-insp { width: 282px; flex: none; min-height: 0; overflow: auto; background: var(--surface); border-right: 1px solid var(--border); }
.tk-insp__in { padding: 16px 16px 50px; display: flex; flex-direction: column; gap: 14px; }
.tk-insp__h { display: flex; align-items: center; gap: 9px; }
.tk-insp__h .ic { width: 30px; height: 30px; border-radius: 7px; background: #1f3a5f; color: #fff; display: flex; align-items: center; justify-content: center; flex: none; }
.tk-insp__h b { font-family: var(--font-serif); font-size: 16px; font-weight: 600; color: var(--ink-strong); display: block; line-height: 1.1; }
.tk-insp__h span { font-family: var(--font-mono); font-size: 10.5px; color: var(--ink-muted); }
.tk-insp__sec { display: flex; align-items: center; gap: 7px; font-family: var(--font-mono); font-size: 10px; font-weight: 600; letter-spacing: 0.07em; text-transform: uppercase; color: var(--ink-subtle); margin: 4px 0 -2px; }
.tk-insp__sec .ln { flex: 1; height: 1px; background: var(--border); }
.tk-field { display: flex; align-items: center; gap: 8px; padding: 7px 9px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface); }
.tk-field code { font-family: var(--font-mono); font-size: 12px; color: var(--code-cmd); flex: 1; }
.tk-asset-row { display: flex; align-items: center; gap: 9px; padding: 7px 9px; border: 1px solid var(--border); border-radius: var(--radius-md); }
.tk-asset-row .ic { width: 24px; height: 28px; border-radius: 3px; border: 1px solid var(--border-strong); background: var(--surface-sunken); display: flex; align-items: center; justify-content: center; flex: none; color: var(--ink-subtle); }
.tk-asset-row b { font-family: var(--font-mono); font-size: 11.5px; color: var(--ink); flex: 1; }
.tk-asset-row span { font-family: var(--font-sans); font-size: 10px; color: var(--ink-subtle); }
.tk-global { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 9px 11px; background: var(--accent-subtle-bg); border: 1px solid var(--border-accent); border-radius: var(--radius-md); }
.tk-global b { font-family: var(--font-sans); font-size: 12px; font-weight: 600; color: var(--accent-subtle-fg); }
.tk-global span { font-family: var(--font-sans); font-size: 10.5px; color: var(--accent-subtle-fg); opacity: 0.85; }
`;
const el = document.createElement('style');
el.id = 'tecto-kit-tpl-css';
el.textContent = css;
document.head.appendChild(el);
})();
const CLS_LINES = String.raw`% cotizacion.cls — plantilla corporativa (la creas tú)
\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{cotizacion}[2026/06 Cotizacion Acme]
\LoadClass[11pt]{article}
\RequirePackage[margin=2.2cm]{geometry}
\RequirePackage{xcolor, graphicx, array}
% --- Marca de la empresa ---
\definecolor{marca}{HTML}{1F3A5F}
\newcommand{\logo}{\includegraphics[height=11mm]{assets/logo.pdf}}
% --- Campos: los llena el formulario del Generador ---
\newcommand{\cliente}[1]{\def\@cliente{#1}}
\newcommand{\rfccliente}[1]{\def\@rfc{#1}}
\newcommand{\numero}[1]{\def\@num{#1}}
\newcommand{\moneda}[1]{\def\@mon{#1}}
\newenvironment{conceptos}
{\begin{tabular}{p{8cm} r r r}}
{\end{tabular}}
\newcommand{\maketitlepage}{%
{\logo\hfill\color{marca}\Huge COTIZACIÓN}%
\par\rule{\linewidth}{1.5pt}%
}
\endinput`.split('\n');
function Field({ name, type }) {
const { Badge, IconButton } = window.TectoDS;
const I = window.TectoIcons;
return (
\{name}
{type}
);
}
function TemplateInspector({ onToast }) {
const I = window.TectoIcons;
const { Button, Switch, IconButton } = window.TectoDS;
const [global, setGlobal] = React.useState(false);
const fields = [
['cliente', 'texto'], ['rfccliente', 'texto'], ['numero', 'texto'],
['fecha', 'fecha'], ['moneda', 'selección'], ['conceptos', 'tabla'], ['iva', 'sí/no'], ['notas', 'texto largo'],
];
return (
cotizacion/plantillas/cotizacion/
{fields.map(([n, t]) =>
)}
} onClick={() => onToast({ tone: 'info', title: 'Nuevo campo', msg: 'Define \\comando y su tipo.' })}>Añadir campo
Usar assets globaleslogos y fuentes compartidos
setGlobal(e.target.checked)} />
{!global && (
logo.pdfpropio
sello.pngpropio
)}
} onClick={() => onToast({ tone: 'info', title: 'Subir asset', msg: global ? 'Gestiona globales en Assets.' : 'Se guarda en /plantillas/cotizacion/assets/.' })}>{global ? 'Gestionar globales' : 'Subir a esta plantilla'}
);
}
function TemplateWorkspace({ status, onCompile, onDownload, onToast }) {
const { Editor } = window.TectoEditor;
const { CotizacionDoc } = window.TectoCorp;
const I = window.TectoIcons;
const { Button, IconButton, StatusPill } = window.TectoDS;
return (
vista previa · datos de ejemplo
} onClick={onCompile} loading={status === 'running'}>
{status === 'running' ? 'Compilando…' : 'Compilar'}
} onClick={onDownload} />
{status === 'running' && (
Compilando con Tectonic…
)}
);
}
window.TectoTemplate = { TemplateWorkspace, TemplateInspector, CLS_LINES };
})();
/* --- Editor.jsx --- */
(() => {
/* Tecto UI Kit — code Editor pane (Monaco-like) with LaTeX highlighting. window.TectoEditor */
const React = window.React;
(function injectEditorCSS() {
if (document.getElementById('tecto-kit-editor-css')) return;
const css = `
.tk-editor { display: flex; flex-direction: column; min-height: 0; background: var(--code-bg); }
.tk-editor__tabs { display: flex; align-items: stretch; height: 36px; background: var(--surface-sunken); border-bottom: 1px solid var(--border); flex: none; }
.tk-editor__tab { display: inline-flex; align-items: center; gap: 7px; padding: 0 13px; font-family: var(--font-mono); font-size: var(--text-xs); color: var(--ink-muted); border-right: 1px solid var(--border); cursor: default; }
.tk-editor__tab--active { background: var(--code-bg); color: var(--ink-strong); box-shadow: inset 0 -2px 0 var(--accent); }
.tk-editor__tab .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
.tk-editor__scroll { flex: 1; min-height: 0; overflow: auto; padding: 8px 0 40px; }
.tk-editor__line { display: flex; align-items: flex-start; min-height: 20px; font-family: var(--font-mono); font-size: 13px; line-height: 20px; }
.tk-editor__line:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); }
.tk-editor__line--active { background: color-mix(in srgb, var(--accent) 8%, transparent); }
.tk-editor__gutter { flex: none; width: 46px; padding-right: 14px; text-align: right; color: var(--code-gutter); user-select: none; font-size: 12px; }
.tk-editor__code { flex: 1; white-space: pre-wrap; word-break: break-word; padding-right: 16px; color: var(--code-text); }
.tk-tok-cmd { color: var(--code-cmd); }
.tk-tok-brace { color: var(--code-bracket); }
.tk-tok-math { color: var(--code-math); }
.tk-tok-comment { color: var(--code-comment); font-style: italic; }
.tk-tok-string { color: var(--code-string); }
.tk-caret { display: inline-block; width: 2px; height: 15px; background: var(--accent); margin-left: 1px; vertical-align: -2px; animation: tk-blink 1.1s steps(1) infinite; }
@keyframes tk-blink { 50% { opacity: 0; } }
`;
const el = document.createElement('style');
el.id = 'tecto-kit-editor-css';
el.textContent = css;
document.head.appendChild(el);
})();
function tokenize(line) {
const out = [];
const re = /(%.*$)|(\\[a-zA-Z@]+\*?|\\.)|(\$[^$]*\$)|([{}\[\]])/g;
let last = 0, m;
while ((m = re.exec(line)) !== null) {
if (m.index > last) out.push({ t: line.slice(last, m.index), c: null });
let cls = null;
if (m[1]) cls = 'tk-tok-comment';
else if (m[2]) cls = 'tk-tok-cmd';
else if (m[3]) cls = 'tk-tok-math';
else if (m[4]) cls = 'tk-tok-brace';
out.push({ t: m[0], c: cls });
last = m.index + m[0].length;
}
if (last < line.length) out.push({ t: line.slice(last), c: null });
return out;
}
function Editor({ lines, activeLine = 11, caretLine = 23, filename = 'main.tex', secondary = 'refs.bib' }) {
return (
{filename}
{secondary &&
{secondary}
}
{lines.map((ln, i) => {
const n = i + 1;
const toks = tokenize(ln);
return (
{n}
{toks.map((tk, j) => tk.c ? {tk.t} : {tk.t})}
{n === caretLine && }
);
})}
);
}
window.TectoEditor = { Editor };
})();
/* --- Integration + App --- */
(() => {
const BRAND = {
name: 'Synset Solutions',
initials: 'SS',
color: '#1f3a5f',
tint: '#eef2f7',
rfc: 'SYN850101AA1',
address: 'Santo Domingo, RD',
email: 'hola@synsetsolutions.com',
tel: '+1 (809) 000-0000',
};
function buildTemplateData(formData) {
const { totals } = window.TectoCorp;
const t = totals(formData);
const items = [
{ id: 'docs', label: 'Documentos', icon: I.Folder },
{ id: 'templates', label: 'Plantillas', icon: I.Layout },
{ id: 'assets', label: 'Assets', icon: I.Image },
{ id: 'docview', label: 'Documentación', icon: I.FileText },
{ id: 'editor', label: 'Editor de plantilla', icon: I.PanelLeft },
];
while (items.length < 3) items.push({ desc: '', qty: 0, price: 0 });
const fmt = (n) => n.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
return {
empresa_nombre: BRAND.name,
empresa_direccion: BRAND.address,
empresa_email: BRAND.email,
empresa_tel: BRAND.tel,
numero_cotizacion: formData.numero,
fecha: formData.fecha,
cliente_nombre: formData.cliente || '',
cliente_empresa: formData.clienteEmpresa || '',
cliente_email: formData.clienteEmail || '',
descripcion_proyecto: formData.descripcion || '',
item_1_desc: items[0].desc,
item_1_hrs: String(items[0].qty),
item_1_total: fmt(items[0].qty * items[0].price),
item_2_desc: items[1].desc,
item_2_hrs: String(items[1].qty),
item_2_total: fmt(items[1].qty * items[1].price),
item_3_desc: items[2].desc,
item_3_hrs: String(items[2].qty),
item_3_total: fmt(items[2].qty * items[2].price),
subtotal: fmt(t.subtotal),
itbis: fmt(t.iva),
total: fmt(t.total),
moneda: formData.moneda,
condiciones_pago: formData.notas,
validez: formData.validez,
};
}
const today = new Date();
const pad = (n) => String(n).padStart(2, '0');
const todayStr = `${pad(today.getDate())} ${['ene','feb','mar','abr','may','jun','jul','ago','sep','oct','nov','dic'][today.getMonth()]} ${today.getFullYear()}`;
const validStr = (() => {
const v = new Date(today); v.setDate(v.getDate() + 15);
return `${pad(v.getDate())} ${['ene','feb','mar','abr','may','jun','jul','ago','sep','oct','nov','dic'][v.getMonth()]} ${v.getFullYear()}`;
})();
const DEFAULT_DATA = {
numero: `${today.getFullYear()}${pad(today.getMonth()+1)}${pad(today.getDate())}-001`,
fecha: todayStr,
validez: validStr,
cliente: '',
clienteEmpresa: '',
clienteEmail: '',
descripcion: '',
moneda: 'USD',
iva: true,
items: [{ desc: '', qty: 1, price: 0 }],
notas: 'Validez de la oferta: 15 días. 50% de anticipo al inicio, 50% contra entrega.',
};
const escapeLaTeX = (val) => String(val)
.replace(/·/g, '--').replace(/—/g, '---').replace(/–/g, '--')
.replace(/\\/g, '\\textbackslash{}')
.replace(/&/g,'\\&').replace(/%/g,'\\%').replace(/#/g,'\\#')
.replace(/_/g,'\\_').replace(/\^/g,'\\^{}')
.replace(/~/g,'\\textasciitilde{}');
const renderTex = (template, vars) => template.replace(/\{\{([^}]+)\}\}/g, (_, key) =>
escapeLaTeX(vars[key.trim()] ?? ''));
/* Tecto UI Kit — app orchestrator: document workspace + generator + editor. window.TectoApp */
const React = window.React;
(function injectAppCSS() {
if (document.getElementById('tecto-kit-app-css')) return;
const css = `
.tk-app { height: 100%; display: flex; flex-direction: column; background: var(--bg); color: var(--ink); }
.tk-body { flex: 1; min-height: 0; display: flex; }
.tk-main { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 0; }
.tk-split { flex: 1; min-height: 0; display: flex; }
.tk-split > .tk-editor { flex: 1.05; min-width: 0; border-right: 1px solid var(--border); }
.tk-split > .tk-prev { flex: 1; min-width: 0; }
.tk-toasts { position: fixed; right: 18px; bottom: 18px; z-index: 200; display: flex; flex-direction: column; gap: 10px; }
.tk-login { height: 100%; display: flex; align-items: center; justify-content: center; background: var(--bg);
background-image: radial-gradient(circle at 1px 1px, var(--border) 1px, transparent 0); background-size: 22px 22px; }
.tk-login__card { width: 360px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-xl);
box-shadow: var(--shadow-xl); padding: 30px 30px 26px; }
.tk-login__brand { display: flex; flex-direction: column; align-items: center; gap: 12px; margin-bottom: 22px; }
.tk-login__mark { width: 46px; height: 46px; }
.tk-login__t { font-family: var(--font-serif); font-size: 24px; font-weight: 600; letter-spacing: -0.02em; color: var(--ink-strong); }
.tk-login__sub { font-family: var(--font-mono); font-size: 11px; color: var(--ink-muted); letter-spacing: 0.04em; margin-top: -6px; }
.tk-login__fields { display: flex; flex-direction: column; gap: 13px; }
.tk-login__foot { margin-top: 16px; text-align: center; font-family: var(--font-sans); font-size: 11px; color: var(--ink-subtle); }
`;
const el = document.createElement('style');
el.id = 'tecto-kit-app-css';
el.textContent = css;
document.head.appendChild(el);
})();
function Login({ onAuth }) {
const I = window.TectoIcons;
const { Button, Input } = window.TectoDS;
return (
);
}
function EditorWorkspace({ status, onCompile, onDownload, engine, setEngine }) {
const { Editor } = window.TectoEditor;
const { PdfPreview } = window.TectoPreview;
const { LINES } = window.TectoSample;
return (
);
}
const CRUMBS = {
docs: ['Documentos'],
docview: ['Documentación'],
generator: ['Documentos', 'Nueva cotización'],
templates: ['Plantillas'],
assets: ['Assets'],
editor: ['Plantillas', 'cotizacion.cls'],
settings: ['Ajustes'],
};
function App() {
const [authed, setAuthed] = React.useState(true);
const [theme, setTheme] = React.useState('light');
const [view, setView] = React.useState('generator');
const [engine, setEngine] = React.useState('XeLaTeX');
const [status, setStatus] = React.useState('success');
const [data, setData] = React.useState(DEFAULT_DATA);
const [pdfUrl, setPdfUrl] = React.useState(null);
const [currentDocId, setCurrentDocId] = React.useState(null);
const [toasts, setToasts] = React.useState([]);
const timer = React.useRef(null);
React.useEffect(() => {
document.documentElement.classList.toggle('dark', theme === 'dark');
}, [theme]);
const toggleTheme = () => setTheme((t) => (t === 'dark' ? 'light' : 'dark'));
const pushToast = (t) => {
const id = Math.random().toString(36).slice(2);
setToasts((ts) => [...ts, { ...t, id }]);
setTimeout(() => setToasts((ts) => ts.filter((x) => x.id !== id)), 4200);
};
const compile = async () => {
if (status === 'running') return;
setStatus('running');
setPdfUrl(null);
try {
const tplRes = await fetch('/templates/tpl-cotizacion');
if (!tplRes.ok) throw new Error('No se pudo cargar la plantilla');
const tpl = await tplRes.json();
const tex = renderTex(tpl.tex_template, buildTemplateData(data));
const docId = 'doc-' + Math.random().toString(36).slice(2);
setCurrentDocId(docId);
const compRes = await fetch('/compile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: docId, tex, engine }),
});
const comp = await compRes.json();
if (comp.ok) {
setPdfUrl(comp.pdf_url + '?t=' + Date.now());
setStatus('success');
pushToast({ tone: 'success', title: 'PDF listo', msg: `Tectonic · ${comp.ms}ms` });
} else {
setStatus('error');
pushToast({ tone: 'danger', title: 'Error', msg: comp.log || 'Fallo en Tectonic' });
}
} catch (err) {
setStatus('error');
pushToast({ tone: 'danger', title: 'Error', msg: String(err) });
}
};
const newDoc = (template) => {
setView('generator');
setStatus('success');
if (template && template !== 'Cotización') {
pushToast({ tone: 'brand', title: `Plantilla: ${template}`, msg: 'Misma mecánica — demo con Cotización.' });
}
};
const openDoc = () => { setView('generator'); setStatus('success'); };
const goView = (v) => { setView(v); if (v === 'editor' || v === 'generator') setStatus('success'); };
const { TopBar, LeftRail, StatusBar } = window.TectoChrome;
const { Settings } = window.TectoScreens;
const { Docs } = window.TectoDocs;
const { Documentos, Plantillas, Assets } = window.TectoWorkspace;
const { Generator } = window.TectoGenerator;
const { TemplateWorkspace } = window.TectoTemplate;
const { Toast } = window.TectoDS;
if (!authed) return { setAuthed(true); setView('generator'); }} />;
return (
setAuthed(false)} />
{view === 'docs' &&
}
{view === 'docview' &&
}
{view === 'generator' && (
{ if (pdfUrl) window.open(pdfUrl, "_blank"); else pushToast({ tone: "warning", title: "Sin PDF", msg: "Compila primero." }); }} />
)}
{view === 'templates' && { setView('editor'); setStatus('success'); if (name === 'nueva') pushToast({ tone: 'brand', title: 'Nueva plantilla', msg: 'Carpeta /plantillas/nueva/ — edita su .cls y sus campos.' }); }} />}
{view === 'assets' && }
{view === 'editor' && (
pushToast({ tone: 'brand', title: 'Descargando…', msg: 'cotizacion-preview.pdf' })}
onToast={pushToast} />
)}
{view === 'settings' && }
{view === 'editor' && }
{toasts.map((t) => (
setToasts((ts) => ts.filter((x) => x.id !== t.id))}>{t.msg}
))}
);
}
window.TectoApp = { App, Login };
(function mount() {
if (!window.TectoApp) { setTimeout(mount, 30); return; }
ReactDOM.createRoot(document.getElementById('root')).render(
React.createElement(window.TectoApp.App)
);
})();
})();