feat(credits): solde IA unique (option M), packs Stripe et polish billing
Un pot de crédits global (BASIC 100 / PRO 1 000 / BUSINESS 4 000) remplace les seaux par fonction côté débit. Packs one-shot S/M/L, admin sans seaux legacy, usage multi-features, hints de coût, anti-flash thème et hydratation admin. Inclut aussi le pipeline slides renforcé et la page publique polishée.
This commit is contained in:
@@ -72,17 +72,19 @@ function renderTitle(slide: SlideTitle, r: Recipe, idx: number): string {
|
||||
}
|
||||
|
||||
function renderBullets(slide: SlideBullets, r: Recipe, idx: number): string {
|
||||
const items = slide.items.map((item, i) => `
|
||||
<div class="reveal" style="display:flex;align-items:flex-start;gap:16px;padding:8px 0;">
|
||||
<span style="width:8px;height:8px;border-radius:50%;background:${r.accent1};margin-top:10px;flex-shrink:0;"></span>
|
||||
<span style="font-size:1.1rem;line-height:1.65;color:${r.text};">${esc(item)}</span>
|
||||
const list = (slide.items || []).filter(Boolean)
|
||||
// Dense layout: 2-col grid when 4+ bullets (Hermes/OpenClaw: avoid sparse empty slides)
|
||||
const twoCol = list.length >= 4
|
||||
const items = list.map((item) => `
|
||||
<div class="reveal" style="display:flex;align-items:flex-start;gap:14px;padding:12px 14px;background:${r.glassBg};border:1px solid ${r.glassBorder};border-radius:12px;">
|
||||
<span style="width:8px;height:8px;border-radius:50%;background:${r.accent1};margin-top:8px;flex-shrink:0;"></span>
|
||||
<span style="font-size:clamp(0.95rem,1.6vw,1.15rem);line-height:1.55;color:${r.text};">${esc(item)}</span>
|
||||
</div>`).join('')
|
||||
return `<div class="slide" data-slide="${idx}">
|
||||
${mesh(r)}
|
||||
<div class="content">
|
||||
<h2 class="reveal" style="font-family:${r.fontDisplay},serif;font-size:clamp(1.8rem,3.5vw,2.6rem);font-weight:800;letter-spacing:-0.03em;margin:0 0 8px;">${esc(slide.title)}</h2>
|
||||
<div class="reveal" style="width:48px;height:4px;background:${r.accent1};border-radius:2px;margin-bottom:32px;"></div>
|
||||
<div style="display:flex;flex-direction:column;gap:6px;">${items}</div>
|
||||
<h2 class="reveal" style="font-family:${r.fontDisplay},serif;font-size:clamp(1.6rem,3.2vw,2.4rem);font-weight:800;letter-spacing:-0.03em;margin:0 0 20px;">${esc(slide.title)}</h2>
|
||||
<div style="display:grid;grid-template-columns:${twoCol ? '1fr 1fr' : '1fr'};gap:12px;">${items}</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
@@ -109,8 +111,11 @@ function renderChart(slide: SlideChart, r: Recipe, idx: number): string {
|
||||
}
|
||||
|
||||
function renderStats(slide: SlideStats, r: Recipe, idx: number): string {
|
||||
const cols = Math.min(slide.stats.length, 4)
|
||||
const items = slide.stats.map(s => `
|
||||
const stats = (slide.stats || []).filter((s) => s && (s.value || s.label))
|
||||
// Never force empty columns: 1 stat = full width, 2 = half, 3–4 = equal
|
||||
const n = Math.max(1, stats.length)
|
||||
const cols = n === 1 ? 1 : n === 2 ? 2 : n === 3 ? 3 : 4
|
||||
const items = stats.map(s => `
|
||||
<div class="reveal" style="text-align:center;padding:28px 16px;background:${r.glassBg};border:1px solid ${r.glassBorder};border-radius:16px;">
|
||||
<div style="font-size:clamp(2.2rem,4vw,3.5rem);font-weight:900;line-height:1;background:linear-gradient(135deg,${r.accent1},${r.accent2});-webkit-background-clip:text;-webkit-text-fill-color:transparent;" data-count="${extractNum(s.value)}" data-suffix="${extractSuffix(s.value)}">${esc(s.value)}</div>
|
||||
<div style="font-size:0.8rem;color:${r.textMuted};margin-top:10px;letter-spacing:0.12em;text-transform:uppercase;font-weight:600;">${esc(s.label)}</div>
|
||||
@@ -118,9 +123,8 @@ function renderStats(slide: SlideStats, r: Recipe, idx: number): string {
|
||||
return `<div class="slide" data-slide="${idx}">
|
||||
${mesh(r)}
|
||||
<div class="content">
|
||||
<h2 class="reveal" style="font-family:${r.fontDisplay},serif;font-size:clamp(1.8rem,3.5vw,2.6rem);font-weight:800;letter-spacing:-0.03em;margin:0 0 8px;">${esc(slide.title)}</h2>
|
||||
<div class="reveal" style="width:48px;height:4px;background:${r.accent1};border-radius:2px;margin-bottom:32px;"></div>
|
||||
<div style="display:grid;grid-template-columns:repeat(${cols},1fr);gap:20px;">${items}</div>
|
||||
<h2 class="reveal" style="font-family:${r.fontDisplay},serif;font-size:clamp(1.6rem,3.2vw,2.4rem);font-weight:800;letter-spacing:-0.03em;margin:0 0 24px;">${esc(slide.title)}</h2>
|
||||
<div style="display:grid;grid-template-columns:repeat(${cols},minmax(0,1fr));gap:16px;">${items}</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
@@ -144,20 +148,22 @@ function renderTable(slide: SlideTable, r: Recipe, idx: number): string {
|
||||
}
|
||||
|
||||
function renderCards(slide: SlideCards, r: Recipe, idx: number): string {
|
||||
const cols = slide.cards.length <= 2 ? 2 : slide.cards.length === 4 ? 2 : 3
|
||||
const items = slide.cards.map((c, i) => `
|
||||
<div class="reveal" style="padding:24px;background:${r.glassBg};border:1px solid ${r.glassBorder};border-radius:14px;position:relative;overflow:hidden;">
|
||||
const cards = (slide.cards || []).filter((c) => c && (c.title || c.description))
|
||||
// 1 card → full width (no empty second column). 2 → 2 cols. 3 → 3. 4+ → 2x2
|
||||
const n = Math.max(1, cards.length)
|
||||
const cols = n === 1 ? 1 : n === 2 ? 2 : n === 3 ? 3 : 2
|
||||
const items = cards.map((c, i) => `
|
||||
<div class="reveal" style="padding:24px;background:${r.glassBg};border:1px solid ${r.glassBorder};border-radius:14px;position:relative;overflow:hidden;min-height:120px;">
|
||||
<span style="position:absolute;top:10px;right:14px;font-size:1.8rem;font-weight:900;color:${r.accent1};opacity:0.12;">${String(i + 1).padStart(2, '0')}</span>
|
||||
<div style="width:24px;height:3px;background:${r.accent1};border-radius:2px;margin-bottom:12px;"></div>
|
||||
<div style="font-size:1rem;font-weight:700;color:${r.text};margin-bottom:8px;">${esc(c.title)}</div>
|
||||
<div style="font-size:0.88rem;color:${r.textSecondary};line-height:1.6;">${esc(c.description)}</div>
|
||||
<div style="font-size:1.05rem;font-weight:700;color:${r.text};margin-bottom:8px;">${esc(c.title)}</div>
|
||||
<div style="font-size:0.9rem;color:${r.textSecondary};line-height:1.6;">${esc(c.description)}</div>
|
||||
</div>`).join('')
|
||||
return `<div class="slide" data-slide="${idx}">
|
||||
${mesh(r)}
|
||||
<div class="content">
|
||||
<h2 class="reveal" style="font-family:${r.fontDisplay},serif;font-size:clamp(1.8rem,3.5vw,2.6rem);font-weight:800;letter-spacing:-0.03em;margin:0 0 8px;">${esc(slide.title)}</h2>
|
||||
<div class="reveal" style="width:48px;height:4px;background:${r.accent1};border-radius:2px;margin-bottom:32px;"></div>
|
||||
<div style="display:grid;grid-template-columns:repeat(${cols},1fr);gap:16px;">${items}</div>
|
||||
<h2 class="reveal" style="font-family:${r.fontDisplay},serif;font-size:clamp(1.6rem,3.2vw,2.4rem);font-weight:800;letter-spacing:-0.03em;margin:0 0 24px;">${esc(slide.title)}</h2>
|
||||
<div style="display:grid;grid-template-columns:repeat(${cols},minmax(0,1fr));gap:16px;">${items}</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
@@ -214,18 +220,22 @@ function renderComparison(slide: SlideComparison, r: Recipe, idx: number): strin
|
||||
}
|
||||
|
||||
function renderEquation(slide: SlideEquation, r: Recipe, idx: number): string {
|
||||
const eqs = slide.equations.map(eq => `
|
||||
<div class="reveal" style="text-align:center;padding:24px;background:${r.glassBg};border:1px solid ${r.glassBorder};border-radius:12px;margin-bottom:12px;">
|
||||
<div style="font-size:clamp(1.4rem,3vw,2.2rem);font-family:'KaTeX_Main',serif;color:${r.text};letter-spacing:0.02em;">${esc(eq.latex)}</div>
|
||||
${eq.label ? `<div style="font-size:0.8rem;color:${r.textMuted};margin-top:8px;">${esc(eq.label)}</div>` : ''}
|
||||
</div>`).join('')
|
||||
const eqs = (slide.equations || []).map((eq) => {
|
||||
// data-latex for KaTeX client render; also show fallback monospaced text
|
||||
const latex = eq.latex || ''
|
||||
return `
|
||||
<div class="reveal" style="text-align:center;padding:22px 24px;background:${r.glassBg};border:1px solid ${r.glassBorder};border-radius:14px;margin-bottom:14px;">
|
||||
<div class="eq-latex" data-latex="${esc(latex)}" style="font-size:clamp(1.25rem,2.6vw,1.85rem);color:${r.text};overflow-x:auto;padding:8px 0;"></div>
|
||||
<noscript><code style="font-size:1.1rem;">${esc(latex)}</code></noscript>
|
||||
${eq.label ? `<div style="font-size:0.85rem;color:${r.textMuted};margin-top:10px;font-weight:600;">${esc(eq.label)}</div>` : ''}
|
||||
</div>`
|
||||
}).join('')
|
||||
return `<div class="slide" data-slide="${idx}">
|
||||
${mesh(r)}
|
||||
<div class="content">
|
||||
<h2 class="reveal" style="font-family:${r.fontDisplay},serif;font-size:clamp(1.8rem,3.5vw,2.4rem);font-weight:800;letter-spacing:-0.03em;margin:0 0 8px;">${esc(slide.title)}</h2>
|
||||
<div class="reveal" style="width:48px;height:4px;background:${r.accent1};border-radius:2px;margin-bottom:28px;"></div>
|
||||
${eqs}
|
||||
${slide.explanation ? `<p class="reveal" style="font-size:0.95rem;color:${r.textSecondary};line-height:1.7;margin-top:16px;text-align:center;max-width:700px;margin-inline:auto;">${esc(slide.explanation)}</p>` : ''}
|
||||
<h2 class="reveal" style="font-family:${r.fontDisplay},serif;font-size:clamp(1.6rem,3.2vw,2.4rem);font-weight:800;letter-spacing:-0.03em;margin:0 0 20px;">${esc(slide.title)}</h2>
|
||||
${eqs || `<div class="reveal" style="color:${r.textMuted};">Aucune équation</div>`}
|
||||
${slide.explanation ? `<p class="reveal" style="font-size:1rem;color:${r.textSecondary};line-height:1.7;margin-top:18px;max-width:780px;">${esc(slide.explanation)}</p>` : ''}
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
@@ -429,14 +439,19 @@ export function buildPresentationHTML(input: PresentationInput): string {
|
||||
<title>${esc(input.title)}</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="${r.fontUrl}" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||
<style>
|
||||
:root{--color-bg:${r.bg};--color-text:${r.text};--color-text-secondary:${r.textSecondary};--color-text-muted:${r.textMuted};--color-accent-1:${r.accent1};--color-accent-2:${r.accent2};--color-glass-bg:${r.glassBg};--color-glass-border:${r.glassBorder};--svg-grid-line:${r.svgGrid};--font-display:${r.fontDisplay},serif;--font-body:${r.fontBody},system-ui,sans-serif;}
|
||||
*,*::before,*::after{box-sizing:border-box;}
|
||||
html,body{margin:0;padding:0;overflow:hidden;background:var(--color-bg);color:var(--color-text);font-family:var(--font-body);-webkit-font-smoothing:antialiased;}
|
||||
.deck{width:100vw;height:100vh;position:relative;}
|
||||
.slide{position:absolute;top:0;left:0;width:100%;height:100%;background:var(--color-bg);opacity:0;transform:scale(0.96);transition:opacity 0.6s ease,transform 0.6s ease;pointer-events:none;overflow:hidden;display:flex;align-items:center;justify-content:center;}
|
||||
html,body{margin:0;padding:0;width:100%;height:100%;overflow:hidden;background:var(--color-bg);color:var(--color-text);font-family:var(--font-body);-webkit-font-smoothing:antialiased;}
|
||||
/* Fill iframe/container — use % not 100vw (vw breaks lab preview letterboxing) */
|
||||
.deck{width:100%;height:100%;position:relative;overflow:hidden;}
|
||||
.slide{position:absolute;top:0;left:0;width:100%;height:100%;background:var(--color-bg);opacity:0;transform:scale(0.98);transition:opacity 0.45s ease,transform 0.45s ease;pointer-events:none;overflow:hidden;display:flex;align-items:center;justify-content:center;}
|
||||
.slide.active{opacity:1;transform:scale(1);pointer-events:all;}
|
||||
.slide>.content{position:relative;z-index:2;width:100%;max-width:1100px;padding:clamp(1.5rem,4vw,4rem);}
|
||||
.slide>.content{position:relative;z-index:2;width:100%;max-width:min(1100px,94%);height:100%;max-height:100%;padding:clamp(1.25rem,3.5vmin,3.5rem);display:flex;flex-direction:column;justify-content:center;box-sizing:border-box;}
|
||||
.eq-latex{max-width:100%;}
|
||||
.eq-latex .katex-display{margin:0.4em 0;overflow-x:auto;overflow-y:hidden;}
|
||||
.gradient-mesh{position:absolute;inset:0;overflow:hidden;pointer-events:none;z-index:0;}
|
||||
.blob{position:absolute;border-radius:50%;filter:blur(80px);animation:float-slow var(--dur,18s) ease-in-out infinite;}
|
||||
@keyframes float-slow{0%{transform:translate(0,0) scale(1);}50%{transform:translate(-40px,40px) scale(0.92);}100%{transform:translate(0,0) scale(1);}}
|
||||
@@ -464,13 +479,14 @@ ${slidesHtml}
|
||||
<script>
|
||||
var current=1,slides=document.querySelectorAll('.slide'),total=slides.length;
|
||||
(function(){var d=document.getElementById('nav-dots');for(var i=1;i<=total;i++){var b=document.createElement('button');b.className='nav-dot'+(i===1?' active':'');(function(n){b.addEventListener('click',function(){goToSlide(n)});})(i);d.appendChild(b);}updateNav();})();
|
||||
function goToSlide(n){if(n<1||n>total||n===current)return;var p=document.querySelector('.slide.active'),nx=document.querySelector('.slide[data-slide="'+n+'"]');if(!nx)return;if(p)p.classList.remove('active');nx.classList.add('active');current=n;updateNav();animateSlide(nx);}
|
||||
function renderKatex(){if(typeof katex==='undefined')return;document.querySelectorAll('.eq-latex[data-latex]').forEach(function(el){var tex=el.getAttribute('data-latex')||'';try{katex.render(tex,el,{throwOnError:false,displayMode:true});}catch(e){el.textContent=tex;}});}
|
||||
function goToSlide(n){if(n<1||n>total||n===current)return;var p=document.querySelector('.slide.active'),nx=document.querySelector('.slide[data-slide="'+n+'"]');if(!nx)return;if(p)p.classList.remove('active');nx.classList.add('active');current=n;updateNav();animateSlide(nx);setTimeout(renderKatex,40);}
|
||||
function changeSlide(dir){var n=current+dir;if(n<1)n=total;if(n>total)n=1;goToSlide(n);}
|
||||
function updateNav(){document.querySelectorAll('.nav-dot').forEach(function(d,i){d.classList.toggle('active',(i+1)===current);});var c=document.getElementById('slide-counter');if(c)c.textContent=current+' / '+total;try{parent.postMessage({type:'slideChange',current:current,total:total},'*');}catch(e){}}
|
||||
document.addEventListener('keydown',function(e){if(e.key==='ArrowRight'||e.key===' ')changeSlide(1);if(e.key==='ArrowLeft')changeSlide(-1);});
|
||||
var tx=0;document.addEventListener('touchstart',function(e){tx=e.touches[0].clientX;},{passive:true});document.addEventListener('touchend',function(e){var dx=tx-e.changedTouches[0].clientX;if(Math.abs(dx)>50)changeSlide(dx>0?1:-1);},{passive:true});
|
||||
function animateSlide(s){s.querySelectorAll('.reveal').forEach(function(el,i){el.style.transition='none';el.style.opacity='0';el.style.transform='translateY(18px)';el.offsetHeight;el.style.transition='opacity 0.35s ease '+(i*0.07)+'s, transform 0.35s ease '+(i*0.07)+'s';el.style.opacity='1';el.style.transform='translateY(0)';});s.querySelectorAll('.bar[data-height]').forEach(function(b){b.style.height='0%';setTimeout(function(){b.style.height=b.dataset.height+'%';},100);});s.querySelectorAll('.bar-fill[data-width]').forEach(function(b){b.style.width='0%';setTimeout(function(){b.style.width=b.dataset.width+'%';},100);});s.querySelectorAll('.line-path').forEach(function(p){var l=p.getTotalLength?p.getTotalLength():2000;p.style.strokeDasharray=l;p.style.strokeDashoffset=l;setTimeout(function(){p.style.strokeDashoffset='0';},100);});s.querySelectorAll('[data-count]').forEach(function(el){var t=parseFloat(el.dataset.count),sf=el.dataset.suffix||'',st=30,inc=t/st,i=0,v=0;var iv=setInterval(function(){v+=inc;i++;el.textContent=(i>=st?t:Math.round(v))+sf;if(i>=st)clearInterval(iv);},30);});}
|
||||
var first=document.querySelector('.slide[data-slide="1"]');if(first){first.classList.add('active');setTimeout(function(){animateSlide(first);},300);}
|
||||
var first=document.querySelector('.slide[data-slide="1"]');if(first){first.classList.add('active');setTimeout(function(){animateSlide(first);renderKatex();},300);}else{setTimeout(renderKatex,400);}
|
||||
// Particles
|
||||
document.querySelectorAll('canvas[id^="particles-"]').forEach(function(c){c.width=window.innerWidth;c.height=window.innerHeight;var ctx=c.getContext('2d'),pts=[];for(var i=0;i<50;i++)pts.push({x:Math.random()*c.width,y:Math.random()*c.height,vx:(Math.random()-0.5)*0.3,vy:(Math.random()-0.5)*0.3,r:Math.random()*2+0.5});function draw(){ctx.clearRect(0,0,c.width,c.height);pts.forEach(function(p){p.x+=p.vx;p.y+=p.vy;if(p.x<0)p.x=c.width;if(p.x>c.width)p.x=0;if(p.y<0)p.y=c.height;if(p.y>c.height)p.y=0;ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,Math.PI*2);ctx.fillStyle='${r.accent1}80';ctx.fill();});requestAnimationFrame(draw);}draw();});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user