naider 1.24.5 → 2.0.1

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.
package/src/gen.js CHANGED
@@ -105,6 +105,17 @@ const GAME_TYPES = {
105
105
  pong: ['pong', 'paddle', 'ポン', 'パドル', 'ピンポン'],
106
106
  };
107
107
 
108
+ const APP_TYPES = {
109
+ pomodoro: ['pomodoro', 'ポモドーロ'],
110
+ calculator: ['calculator', 'calc', '計算', '計算機', '電卓'],
111
+ drawing: ['drawing', 'draw', 'paint', 'sketch', 'お絵描き', 'ペイント', '落書き'],
112
+ timer: ['timer', 'stopwatch', 'タイマー', 'ストップウォッチ'],
113
+ counter: ['counter', 'カウンター', 'カウント'],
114
+ todo: ['todo', 'todos', 'タスク', 'やること', 'やることリスト'],
115
+ clock: ['clock', 'watch', '時計', 'クロック'],
116
+ notes: ['notes', 'note', 'memo', 'メモ', 'ノート'],
117
+ };
118
+
108
119
  const NOISE = new Set([
109
120
  'a', 'an', 'the', 'for', 'with', 'and', 'or', 'on', 'at', 'to', 'of', 'in', 'by',
110
121
  'that', 'which', 'this', 'it', 'its', 'my', 'is', 'are', 'has', 'have', 'do', 'does',
@@ -115,6 +126,8 @@ const NOISE = new Set([
115
126
  'tapping', 'tap', 'clicker', 'click', 'quiz', 'trivia', 'memory', 'matching', 'typing',
116
127
  'reaction', 'reflex', 'snake', 'breakout', 'brick', 'block', 'pong', 'puzzle',
117
128
  'dodge', 'avoid', 'escape', 'color', 'colour', 'stroop', 'paddle',
129
+ 'calculator', 'calc', 'timer', 'stopwatch', 'counter', 'pomodoro',
130
+ 'clock', 'watch', 'drawing', 'draw', 'paint', 'sketch', 'notes', 'note', 'memo', 'app',
118
131
  'の', 'を', 'に', 'で', 'は', 'が', 'と', 'も', 'から', 'まで', 'より', 'って',
119
132
  'な', 'ような', 'みたいな', 'っぽい', 'ような感じ',
120
133
  '作って', '作る', '作成', '生成', '追加', '実装', 'ください', 'して', 'つけて',
@@ -133,6 +146,16 @@ function detectGameType(words, raw) {
133
146
  return 'tapping';
134
147
  }
135
148
 
149
+ function detectAppType(words, raw) {
150
+ const lower = raw.toLowerCase();
151
+ for (const [type, kws] of Object.entries(APP_TYPES)) {
152
+ for (const kw of kws) {
153
+ if (lower.includes(kw)) return type;
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+
136
159
  // ── Field Normalization ─────────────────────────────────────
137
160
 
138
161
  const FIELD_SYNONYMS = {
@@ -528,6 +551,21 @@ function classify(words, raw) {
528
551
  mods.isGame = true;
529
552
  }
530
553
 
554
+ // App intent: standalone frontend apps
555
+ if (!mods.isGame) {
556
+ const appType = detectAppType(words, raw);
557
+ if (appType) {
558
+ const strongServerKw = ['server', 'api', 'rest', 'restful', 'endpoint', 'backend', 'service', 'microservice', 'http', 'web'];
559
+ const hasStrongServer = strongServerKw.some(k => lower.includes(k));
560
+ if (!hasStrongServer && !intents.has('crud') && !intents.has('database') && !intents.has('schema') && !intents.has('auth')) {
561
+ intents.delete('server');
562
+ mods.appType = appType;
563
+ mods.isApp = true;
564
+ intents.add('app');
565
+ }
566
+ }
567
+ }
568
+
531
569
  // Default fallback: if nothing detected, server + schema
532
570
  if (intents.size === 0) {
533
571
  intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('database');
@@ -548,6 +586,12 @@ function extractParams(words, raw, intents, mods) {
548
586
  return params;
549
587
  }
550
588
 
589
+ if (mods.isApp) {
590
+ params.appType = mods.appType;
591
+ params.appTitle = raw.replace(/\b(make|create|build|generate|simple|basic|new|a|an|the)\b/gi, '').trim();
592
+ return params;
593
+ }
594
+
551
595
  // Extract port number
552
596
  for (let i = 0; i < words.length; i++) {
553
597
  const n = parseInt(words[i]);
@@ -1785,22 +1829,30 @@ function migrationFile(entities, relationships, m2mRels) {
1785
1829
 
1786
1830
  // ── Code Composer ───────────────────────────────────────────
1787
1831
 
1788
- // ── Game Engine (compositional) ──────────────────────────
1789
-
1790
- const GAME_PALETTES = {
1791
- tapping: { bg:'#1a0a2e', sf:'#2d1a42', pr:'#FF6B35', sc:'#FF3864', ac:'#FFD700', tx:'#fff', mt:'#a89cc8' },
1792
- quiz: { bg:'#0d1b2a', sf:'#1b2838', pr:'#3A86FF', sc:'#8338EC', ac:'#06D6A0', tx:'#e8ecf1', mt:'#7b8ca3' },
1793
- memory: { bg:'#1a0f25', sf:'#2d1f3d', pr:'#C77DFF', sc:'#E05297', ac:'#FF6B9D', tx:'#f5f0ff', mt:'#9b8bb4' },
1794
- snake: { bg:'#071a0e', sf:'#0f2918', pr:'#4ADE80', sc:'#22D3EE', ac:'#FCD34D', tx:'#e8f5e9', mt:'#6b8a7a' },
1795
- typing: { bg:'#0a192f', sf:'#112240', pr:'#64FFDA', sc:'#00B4D8', ac:'#CCD6F6', tx:'#CCD6F6', mt:'#5c7a8a' },
1796
- reaction: { bg:'#18181b', sf:'#27272a', pr:'#F87171', sc:'#EF4444', ac:'#4ADE80', tx:'#fafafa', mt:'#71717a' },
1797
- breakout: { bg:'#0c0a1d', sf:'#1c1833', pr:'#A78BFA', sc:'#8B5CF6', ac:'#F59E0B', tx:'#f0f0ff', mt:'#8f85a3' },
1798
- dodge: { bg:'#1a0005', sf:'#2d0a12', pr:'#FF4757', sc:'#FF6B81', ac:'#2ED573', tx:'#fff', mt:'#a88a8f' },
1799
- color: { bg:'#0a1628', sf:'#132240', pr:'#FF6348', sc:'#FFA502', ac:'#7BED9F', tx:'#f5f6fa', mt:'#778ca3' },
1800
- pong: { bg:'#0a0a0a', sf:'#1a1a2e', pr:'#00FF87', sc:'#60EFFF', ac:'#FFE66D', tx:'#f0f0f0', mt:'#666680' },
1832
+ // ── Frontend Code Engine (module-based) ──────────────────
1833
+
1834
+ const THEMES = {
1835
+ tapping: { bg:'#1a0a2e', sf:'#2d1a42', pr:'#FF6B35', sc:'#FF3864', ac:'#FFD700', tx:'#fff', mt:'#a89cc8' },
1836
+ quiz: { bg:'#0d1b2a', sf:'#1b2838', pr:'#3A86FF', sc:'#8338EC', ac:'#06D6A0', tx:'#e8ecf1', mt:'#7b8ca3' },
1837
+ memory: { bg:'#1a0f25', sf:'#2d1f3d', pr:'#C77DFF', sc:'#E05297', ac:'#FF6B9D', tx:'#f5f0ff', mt:'#9b8bb4' },
1838
+ snake: { bg:'#071a0e', sf:'#0f2918', pr:'#4ADE80', sc:'#22D3EE', ac:'#FCD34D', tx:'#e8f5e9', mt:'#6b8a7a' },
1839
+ typing: { bg:'#0a192f', sf:'#112240', pr:'#64FFDA', sc:'#00B4D8', ac:'#CCD6F6', tx:'#CCD6F6', mt:'#5c7a8a' },
1840
+ reaction: { bg:'#18181b', sf:'#27272a', pr:'#F87171', sc:'#EF4444', ac:'#4ADE80', tx:'#fafafa', mt:'#71717a' },
1841
+ breakout: { bg:'#0c0a1d', sf:'#1c1833', pr:'#A78BFA', sc:'#8B5CF6', ac:'#F59E0B', tx:'#f0f0ff', mt:'#8f85a3' },
1842
+ dodge: { bg:'#1a0005', sf:'#2d0a12', pr:'#FF4757', sc:'#FF6B81', ac:'#2ED573', tx:'#fff', mt:'#a88a8f' },
1843
+ color: { bg:'#0a1628', sf:'#132240', pr:'#FF6348', sc:'#FFA502', ac:'#7BED9F', tx:'#f5f6fa', mt:'#778ca3' },
1844
+ pong: { bg:'#0a0a0a', sf:'#1a1a2e', pr:'#00FF87', sc:'#60EFFF', ac:'#FFE66D', tx:'#f0f0f0', mt:'#666680' },
1845
+ calculator: { bg:'#1a1a2e', sf:'#25254a', pr:'#667eea', sc:'#764ba2', ac:'#f093fb', tx:'#e8e8ff', mt:'#8888aa' },
1846
+ timer: { bg:'#0f172a', sf:'#1e293b', pr:'#38bdf8', sc:'#818cf8', ac:'#34d399', tx:'#f1f5f9', mt:'#64748b' },
1847
+ counter: { bg:'#18181b', sf:'#27272a', pr:'#a78bfa', sc:'#c084fc', ac:'#fbbf24', tx:'#fafafa', mt:'#71717a' },
1848
+ todo: { bg:'#fafaf9', sf:'#f5f5f4', pr:'#4f46e5', sc:'#7c3aed', ac:'#10b981', tx:'#1c1917', mt:'#78716c' },
1849
+ pomodoro: { bg:'#1a0000', sf:'#2d0a0a', pr:'#ef4444', sc:'#f97316', ac:'#22c55e', tx:'#fff', mt:'#a89c8c' },
1850
+ clock: { bg:'#020617', sf:'#0f172a', pr:'#e2e8f0', sc:'#94a3b8', ac:'#38bdf8', tx:'#f8fafc', mt:'#475569' },
1851
+ drawing: { bg:'#ffffff', sf:'#f3f4f6', pr:'#3b82f6', sc:'#8b5cf6', ac:'#ef4444', tx:'#111827', mt:'#9ca3af' },
1852
+ notes: { bg:'#fffbeb', sf:'#fef3c7', pr:'#f59e0b', sc:'#d97706', ac:'#059669', tx:'#1c1917', mt:'#92400e' },
1801
1853
  };
1802
1854
 
1803
- function extractGameOpts(raw) {
1855
+ function extractBuildOpts(raw) {
1804
1856
  const o = {};
1805
1857
  const d = raw.match(/(\d+)\s*(?:sec|seconds|秒|s\b)/i);
1806
1858
  if (d) o.duration = Math.min(120, Math.max(3, parseInt(d[1])));
@@ -1809,35 +1861,117 @@ function extractGameOpts(raw) {
1809
1861
  if (/fast|高速|はやい/i.test(raw)) o.speed = 'fast';
1810
1862
  if (/hard|難しい|ハード/i.test(raw)) o.difficulty = 'hard';
1811
1863
  if (/easy|簡単|イージー/i.test(raw)) o.difficulty = 'easy';
1864
+ if (/dark|ダーク|暗い/i.test(raw)) o.dark = true;
1865
+ if (/sound|sfx|音|サウンド/i.test(raw)) o.sfx = true;
1866
+ if (/history|履歴|ヒストリー/i.test(raw)) o.history = true;
1867
+ if (/category|カテゴリ|分類/i.test(raw)) o.categories = true;
1812
1868
  return o;
1813
1869
  }
1814
1870
 
1815
- function cssVars(p) {
1816
- return `:root{--bg:${p.bg};--sf:${p.sf};--pr:${p.pr};--sc:${p.sc};--ac:${p.ac};--tx:${p.tx};--mt:${p.mt}}`;
1817
- }
1871
+ // ── CSS Modules ──
1818
1872
 
1819
- function cssBase(p) {
1873
+ function cssBase(t) {
1820
1874
  return `*{margin:0;padding:0;box-sizing:border-box}
1821
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;background:${p.bg};color:${p.tx};min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;overflow:hidden;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent}
1875
+ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;background:${t.bg};color:${t.tx};min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;overflow-x:hidden;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent}
1822
1876
  .ct{width:min(420px,92vw);text-align:center;position:relative}
1823
- h1{font-size:1.8rem;margin-bottom:.5rem;background:linear-gradient(135deg,${p.pr},${p.sc});-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
1824
- .sc{font-size:3rem;font-weight:800;margin:.5rem 0;font-variant-numeric:tabular-nums}
1825
- .btn{display:inline-block;padding:.75rem 2rem;border:none;border-radius:12px;font-size:1.1rem;font-weight:600;cursor:pointer;transition:transform .1s,box-shadow .2s;color:#fff;background:linear-gradient(135deg,${p.pr},${p.sc})}
1826
- .btn:active{transform:scale(.95)}.btn:hover{box-shadow:0 0 24px ${p.pr}66}
1827
- .st{display:flex;gap:1.5rem;justify-content:center;margin:.75rem 0;font-size:.9rem;color:${p.mt}}
1828
- .st span{display:flex;flex-direction:column;align-items:center;gap:.2rem}.st b{color:${p.tx};font-size:1.1rem}
1829
- .hd{display:none!important}.hi{color:${p.ac}}
1830
- .cdwn{position:fixed;top:0;left:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:6rem;font-weight:900;color:${p.pr};z-index:99;pointer-events:none;animation:cdPop .6s ease-out}
1877
+ h1{font-size:1.8rem;margin-bottom:.5rem;background:linear-gradient(135deg,${t.pr},${t.sc});-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}`;
1878
+ }
1879
+
1880
+ function cssBtn(t) {
1881
+ return `.btn{display:inline-block;padding:.75rem 2rem;border:none;border-radius:12px;font-size:1.1rem;font-weight:600;cursor:pointer;transition:transform .1s,box-shadow .2s;color:#fff;background:linear-gradient(135deg,${t.pr},${t.sc})}
1882
+ .btn:active{transform:scale(.95)}.btn:hover{box-shadow:0 0 24px ${t.pr}66}`;
1883
+ }
1884
+
1885
+ function cssScr() {
1886
+ return `.scr{display:none}.scr.on{display:block}`;
1887
+ }
1888
+
1889
+ function cssDsp() {
1890
+ return `.dsp{font-size:3rem;font-weight:800;margin:.5rem 0;font-variant-numeric:tabular-nums}`;
1891
+ }
1892
+
1893
+ function cssStat(t) {
1894
+ return `.st{display:flex;gap:1.5rem;justify-content:center;margin:.75rem 0;font-size:.9rem;color:${t.mt}}
1895
+ .st span{display:flex;flex-direction:column;align-items:center;gap:.2rem}.st b{color:${t.tx};font-size:1.1rem}`;
1896
+ }
1897
+
1898
+ function cssBar(t) {
1899
+ return `.bar{width:100%;height:6px;background:${t.sf};border-radius:3px;margin:.5rem 0;overflow:hidden}
1900
+ .bar .fl{height:100%;background:linear-gradient(90deg,${t.ac},${t.pr});transition:width .1s linear;border-radius:3px}`;
1901
+ }
1902
+
1903
+ function cssInput(t) {
1904
+ return `.input{background:${t.sf};border:2px solid ${t.pr}44;border-radius:12px;padding:.75rem 1rem;font-size:1.1rem;color:${t.tx};width:100%;outline:none;font-family:inherit;transition:border-color .2s}
1905
+ .input:focus{border-color:${t.pr}}`;
1906
+ }
1907
+
1908
+ function cssList(t) {
1909
+ return `.list{display:flex;flex-direction:column;gap:8px;margin:1rem 0;text-align:left;max-height:60vh;overflow-y:auto}
1910
+ .list-item{padding:.75rem 1rem;background:${t.sf};border-radius:10px;display:flex;align-items:center;gap:.75rem;transition:opacity .3s,transform .2s;animation:fadeIn .2s ease-out}
1911
+ .list-item.done{opacity:.5;text-decoration:line-through}
1912
+ .list-item .del{margin-left:auto;cursor:pointer;opacity:.4;transition:opacity .2s;background:none;border:none;color:${t.tx};font-size:1.2rem}.list-item .del:hover{opacity:1}`;
1913
+ }
1914
+
1915
+ function cssFx(t) {
1916
+ return `.cdwn{position:fixed;top:0;left:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:6rem;font-weight:900;color:${t.pr};z-index:99;pointer-events:none;animation:cdPop .6s ease-out}
1831
1917
  @keyframes cdPop{0%{transform:scale(3);opacity:0}30%{opacity:1}100%{transform:scale(1);opacity:0}}
1832
1918
  @keyframes pop{0%{transform:scale(1)}50%{transform:scale(1.15)}100%{transform:scale(1)}}
1833
1919
  @keyframes fadeUp{0%{opacity:1;transform:translateY(0)}100%{opacity:0;transform:translateY(-60px)}}
1920
+ @keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
1834
1921
  @keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-4px)}75%{transform:translateX(4px)}}
1835
- .pcv{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:90}`;
1922
+ .pcv{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:90}
1923
+ .hi{color:${t.ac}}`;
1924
+ }
1925
+
1926
+ function cssCanvas(t) {
1927
+ return `canvas{border-radius:12px;background:${t.sf};display:block;margin:1rem auto;touch-action:none}`;
1928
+ }
1929
+
1930
+ function cssGrid(t, cols) {
1931
+ return `.grid{display:grid;grid-template-columns:repeat(${cols},1fr);gap:8px;margin:1rem auto;width:min(${cols*90}px,88vw)}`;
1836
1932
  }
1837
1933
 
1838
- function cssTimerBar(p) {
1839
- return `.bar{width:100%;height:6px;background:${p.sf};border-radius:3px;margin:.5rem 0;overflow:hidden}
1840
- .bar .fl{height:100%;background:linear-gradient(90deg,${p.ac},${p.pr});transition:width .1s linear;border-radius:3px}`;
1934
+ function cssCards(t, cols) {
1935
+ return `.card{aspect-ratio:1;border-radius:12px;cursor:pointer;perspective:600px;-webkit-perspective:600px}
1936
+ .card .inn{position:relative;width:100%;height:100%;transition:transform .4s;transform-style:preserve-3d;-webkit-transform-style:preserve-3d}
1937
+ .card.flip .inn{transform:rotateY(180deg)}
1938
+ .card .fr,.card .bk{position:absolute;width:100%;height:100%;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:${cols<=4?'2rem':'1.4rem'};backface-visibility:hidden;-webkit-backface-visibility:hidden}
1939
+ .card .fr{background:linear-gradient(135deg,${t.pr},${t.sc})}
1940
+ .card .bk{background:${t.sf};transform:rotateY(180deg);border:2px solid ${t.pr}}
1941
+ .card.done .fr{background:linear-gradient(135deg,${t.ac},#059669)}
1942
+ .card.done{pointer-events:none}`;
1943
+ }
1944
+
1945
+ // ── JS Modules ──
1946
+
1947
+ function jsHelper() {
1948
+ return `const $=id=>document.getElementById(id);`;
1949
+ }
1950
+
1951
+ function jsScreen() {
1952
+ return `function show(id){document.querySelectorAll('.scr').forEach(s=>s.classList.remove('on'));$(id).classList.add('on')}`;
1953
+ }
1954
+
1955
+ function jsStore(ns) {
1956
+ return `const _ns='${ns}';
1957
+ function sGet(k){try{return JSON.parse(localStorage.getItem(_ns+'_'+k))}catch(e){return null}}
1958
+ function sSet(k,v){localStorage.setItem(_ns+'_'+k,JSON.stringify(v))}
1959
+ function sDel(k){localStorage.removeItem(_ns+'_'+k)}`;
1960
+ }
1961
+
1962
+ function jsScore() {
1963
+ return `let _score=0,best=+(sGet('best')||0);
1964
+ if($('best'))$('best').textContent=best;
1965
+ function addPts(n){_score+=n;return _score}
1966
+ function getScore(){return _score}
1967
+ function resetScore(){_score=0}
1968
+ function saveBest(v){if(v>best){best=v;sSet('best',best);if($('best'))$('best').textContent=best;return true}return false}`;
1969
+ }
1970
+
1971
+ function jsTimer() {
1972
+ return `let _tmrId;
1973
+ function startTimer(ms,onTick,onEnd){const t0=Date.now();(function lp(){const e=Date.now()-t0;if(e>=ms){onEnd();return}onTick(e,ms);_tmrId=requestAnimationFrame(lp)})()}
1974
+ function stopTimer(){cancelAnimationFrame(_tmrId)}`;
1841
1975
  }
1842
1976
 
1843
1977
  function jsSfx() {
@@ -1861,19 +1995,43 @@ function burstAt(el,n){const r=el.getBoundingClientRect();emitParticles(r.left+r
1861
1995
  function shakeEl(el){el.style.animation='shake .2s';setTimeout(()=>el.style.animation='',200)}`;
1862
1996
  }
1863
1997
 
1864
- function jsCountdown(cb) {
1865
- return `function countdown(fn){let n=3;const t=()=>{if(n>0){const d=document.createElement('div');d.className='cdwn';d.textContent=n;document.body.appendChild(d);sfxTick();setTimeout(()=>d.remove(),600);n--;setTimeout(t,600)}else{const d=document.createElement('div');d.className='cdwn';d.textContent='GO!';d.style.color='var(--ac)';document.body.appendChild(d);sfxScore();setTimeout(()=>d.remove(),500);fn()}};t()}`;
1998
+ function jsCountdown(acColor) {
1999
+ return `function countdown(fn){let n=3;const t=()=>{if(n>0){const d=document.createElement('div');d.className='cdwn';d.textContent=n;document.body.appendChild(d);sfxTick();setTimeout(()=>d.remove(),600);n--;setTimeout(t,600)}else{const d=document.createElement('div');d.className='cdwn';d.textContent='GO!';d.style.color='${acColor}';document.body.appendChild(d);sfxScore();setTimeout(()=>d.remove(),500);fn()}};t()}`;
1866
2000
  }
1867
2001
 
1868
- function jsCore(storageKey) {
1869
- return `const $=id=>document.getElementById(id);
1870
- let best=+(localStorage.getItem('${storageKey}')||0);
1871
- if($('best'))$('best').textContent=best;
1872
- function show(s){document.querySelectorAll('[id^=s-]').forEach(e=>e.classList.add('hd'));$('s-'+s).classList.remove('hd')}
1873
- function saveBest(v){if(v>best){best=v;localStorage.setItem('${storageKey}',best);if($('best'))$('best').textContent=best;return true}return false}`;
2002
+ function jsShuffle() {
2003
+ return `function shuf(a){for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}`;
2004
+ }
2005
+
2006
+ // ── HTML Helpers ──
2007
+
2008
+ function htmlScr(id, content, active) {
2009
+ return `<div class="scr${active?' on':''}" id="${id}">${Array.isArray(content)?content.join('\n'):content}</div>`;
2010
+ }
2011
+
2012
+ function htmlBtn(text, onclick) {
2013
+ return `<button class="btn" onclick="${onclick}">${text}</button>`;
1874
2014
  }
1875
2015
 
1876
- function htmlWrap(title, css, sections, js) {
2016
+ function htmlStats(items) {
2017
+ return `<div class="st">${items.map(i=>`<span>${i.label}<b id="${i.id}">${i.value}</b></span>`).join('')}</div>`;
2018
+ }
2019
+
2020
+ function htmlDsp(id, val) {
2021
+ return `<div class="dsp" id="${id}">${val}</div>`;
2022
+ }
2023
+
2024
+ function htmlBar() {
2025
+ return `<div class="bar"><div class="fl" id="bar" style="width:100%"></div></div>`;
2026
+ }
2027
+
2028
+ function htmlP(text, style) {
2029
+ return `<p${style?' style="'+style+'"':''}>${text}</p>`;
2030
+ }
2031
+
2032
+ // ── Page Assembler ──
2033
+
2034
+ function assemblePage(title, css, body, js) {
1877
2035
  return `<!DOCTYPE html>
1878
2036
  <html lang="en">
1879
2037
  <head>
@@ -1887,7 +2045,7 @@ ${css}
1887
2045
  <body>
1888
2046
  <div class="ct">
1889
2047
  <h1>${title}</h1>
1890
- ${sections}
2048
+ ${body}
1891
2049
  </div>
1892
2050
  <script>
1893
2051
  ${js}
@@ -1896,85 +2054,88 @@ ${js}
1896
2054
  </html>`;
1897
2055
  }
1898
2056
 
1899
- // ── Tapping Game Builder ──
2057
+ // ── Game Builders ──
1900
2058
 
1901
- function buildTapping(title, opts) {
1902
- const p = GAME_PALETTES.tapping;
2059
+ function buildTapping(title, t, opts) {
1903
2060
  const dur = opts.duration || 10;
1904
2061
 
1905
- const css = [cssVars(p), cssBase(p), cssTimerBar(p),
1906
- `.tap{width:min(300px,75vw);height:min(300px,75vw);border-radius:50%;background:radial-gradient(circle at 35% 35%,${p.pr},${p.sc});display:flex;align-items:center;justify-content:center;margin:1.5rem auto;cursor:pointer;position:relative;font-size:3.5rem;font-weight:900;transition:box-shadow .15s;letter-spacing:-.05em;overflow:hidden}`,
1907
- `.tap:active{box-shadow:0 0 60px ${p.pr}88}`,
2062
+ const css = [cssBase(t), cssBtn(t), cssScr(), cssDsp(), cssStat(t), cssBar(t), cssFx(t),
2063
+ `.tap{width:min(300px,75vw);height:min(300px,75vw);border-radius:50%;background:radial-gradient(circle at 35% 35%,${t.pr},${t.sc});display:flex;align-items:center;justify-content:center;margin:1.5rem auto;cursor:pointer;position:relative;font-size:3.5rem;font-weight:900;transition:box-shadow .15s;letter-spacing:-.05em;overflow:hidden}`,
2064
+ `.tap:active{box-shadow:0 0 60px ${t.pr}88}`,
1908
2065
  `.tap.on{animation:pulse 1s infinite}`,
1909
- `@keyframes pulse{0%,100%{box-shadow:0 0 0 0 ${p.pr}66}50%{box-shadow:0 0 0 20px ${p.pr}00}}`,
2066
+ `@keyframes pulse{0%,100%{box-shadow:0 0 0 0 ${t.pr}66}50%{box-shadow:0 0 0 20px ${t.pr}00}}`,
1910
2067
  `.rp{position:absolute;border-radius:50%;background:rgba(255,255,255,.25);animation:rpAn .5s ease-out forwards;pointer-events:none}`,
1911
2068
  `@keyframes rpAn{0%{width:0;height:0;opacity:.6}100%{width:200px;height:200px;opacity:0}}`,
1912
- `.cb{position:fixed;font-weight:800;font-size:1.5rem;color:${p.ac};pointer-events:none;animation:fadeUp .6s ease-out forwards}`,
2069
+ `.cb{position:fixed;font-weight:800;font-size:1.5rem;color:${t.ac};pointer-events:none;animation:fadeUp .6s ease-out forwards}`,
1913
2070
  ].join('\n');
1914
2071
 
1915
- const html = `<div id="s-menu">
1916
- <p style="color:${p.mt};margin-bottom:1.5rem">Tap as fast as you can — ${dur} seconds!</p>
1917
- <div class="st"><span>Best<b id="best">0</b></span><span>Last<b id="last">-</b></span></div>
1918
- <button class="btn" onclick="go()">Start</button></div>
1919
- <div id="s-play" class="hd">
1920
- <div class="bar"><div class="fl" id="bar" style="width:100%"></div></div>
1921
- <div class="sc" id="cnt">0</div>
1922
- <div class="tap on" id="tap" ontouchstart="hit(event)" onmousedown="hit(event)">TAP</div>
1923
- <div class="st"><span>Combo<b id="cmb">x1</b></span><span>TPS<b id="tps">0</b></span></div></div>
1924
- <div id="s-end" class="hd">
1925
- <div class="sc" id="fin">0</div>
1926
- <div id="msg" style="font-size:1.2rem;line-height:2"></div>
1927
- <button class="btn" style="margin-top:1rem" onclick="go()">Again</button></div>`;
1928
-
1929
- const js = [jsCore('tap_best'), jsSfx(), jsParticles(p.pr, p.ac), jsCountdown(),
1930
- `const DUR=${dur*1000};let taps,t0,raf,combo,lh;`,
2072
+ const html = [
2073
+ htmlScr('menu', [
2074
+ htmlP(`Tap as fast as you can — ${dur} seconds!`, `color:${t.mt};margin-bottom:1.5rem`),
2075
+ htmlStats([{label:'Best',id:'best',value:'0'},{label:'Last',id:'last',value:'-'}]),
2076
+ htmlBtn('Start', 'go()'),
2077
+ ].join('\n'), true),
2078
+ htmlScr('play', [
2079
+ htmlBar(),
2080
+ htmlDsp('cnt', '0'),
2081
+ `<div class="tap on" id="tap" ontouchstart="hit(event)" onmousedown="hit(event)">TAP</div>`,
2082
+ htmlStats([{label:'Combo',id:'cmb',value:'x1'},{label:'TPS',id:'tps',value:'0'}]),
2083
+ ].join('\n')),
2084
+ htmlScr('end', [
2085
+ htmlDsp('fin', '0'),
2086
+ `<div id="msg" style="font-size:1.2rem;line-height:2"></div>`,
2087
+ htmlBtn('Again', 'go()'),
2088
+ ].join('\n')),
2089
+ ].join('\n');
2090
+
2091
+ const js = [jsHelper(), jsScreen(), jsStore('tap'), jsScore(), jsTimer(), jsSfx(), jsParticles(t.pr, t.ac), jsCountdown(t.ac),
2092
+ `const DUR=${dur*1000};let taps,t0,combo,lh;`,
1931
2093
  `function go(){show('play');countdown(startGame)}`,
1932
- `function startGame(){taps=0;combo=1;lh=0;t0=Date.now();$('cnt').textContent='0';$('cmb').textContent='x1';$('bar').style.width='100%';raf=requestAnimationFrame(tick)}`,
1933
- `function tick(){const e=Date.now()-t0,pc=Math.max(0,1-e/DUR)*100;$('bar').style.width=pc+'%';$('tps').textContent=(taps/Math.max(.1,e/1000)).toFixed(1);if(e>=DUR)return end();raf=requestAnimationFrame(tick)}`,
2094
+ `function startGame(){taps=0;combo=1;lh=0;t0=Date.now();$('cnt').textContent='0';$('cmb').textContent='x1';$('bar').style.width='100%';startTimer(DUR,tick,end)}`,
2095
+ `function tick(e,ms){const pc=Math.max(0,1-e/ms)*100;$('bar').style.width=pc+'%';$('tps').textContent=(taps/Math.max(.1,e/1000)).toFixed(1)}`,
1934
2096
  `function hit(ev){ev.preventDefault();if(!t0||Date.now()-t0>DUR)return;taps++;sfxTap();const now=Date.now(),gap=now-lh;combo=gap<200?Math.min(combo+1,99):gap>500?1:combo;lh=now;$('cnt').textContent=taps;$('cmb').textContent='x'+combo;$('cnt').style.animation='none';void $('cnt').offsetWidth;$('cnt').style.animation='pop .15s';const el=$('tap'),rc=el.getBoundingClientRect(),cx=(ev.touches?ev.touches[0].clientX:ev.clientX)-rc.left,cy=(ev.touches?ev.touches[0].clientY:ev.clientY)-rc.top;const r=document.createElement('div');r.className='rp';r.style.cssText='left:'+(cx-100)+'px;top:'+(cy-100)+'px';el.appendChild(r);setTimeout(()=>r.remove(),500);emitParticles((ev.touches?ev.touches[0].clientX:ev.clientX),(ev.touches?ev.touches[0].clientY:ev.clientY),combo>=3?10:4);if(combo>=3){sfxCombo();shakeEl($('cnt'));const c=document.createElement('div');c.className='cb';c.textContent='x'+combo;c.style.cssText='left:'+Math.random()*60+20+'%;top:'+Math.random()*30+20+'%';document.body.appendChild(c);setTimeout(()=>c.remove(),600)}}`,
1935
- `function end(){cancelAnimationFrame(raf);const tps=(taps/(DUR/1000)).toFixed(1);const nb=saveBest(taps);$('last').textContent=taps;$('fin').textContent=taps;if(nb&&taps>0){sfxWin();burstAt($('fin'),30)}else sfxFail();const r=taps>=100?'Insane!':taps>=70?'Amazing!':taps>=50?'Great!':taps>=30?'Nice!':'Keep trying!';$('msg').innerHTML=r+'<br>'+tps+' taps/sec'+(nb?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2097
+ `function end(){stopTimer();const tps=(taps/(DUR/1000)).toFixed(1);const nb=saveBest(taps);$('last').textContent=taps;$('fin').textContent=taps;if(nb&&taps>0){sfxWin();burstAt($('fin'),30)}else sfxFail();const r=taps>=100?'Insane!':taps>=70?'Amazing!':taps>=50?'Great!':taps>=30?'Nice!':'Keep trying!';$('msg').innerHTML=r+'<br>'+tps+' taps/sec'+(nb?' <span class=hi>★ New Best!</span>':'');show('end')}`,
1936
2098
  ].join('\n');
1937
2099
 
1938
- return htmlWrap(title, css, html, js);
2100
+ return assemblePage(title, css, html, js);
1939
2101
  }
1940
2102
 
1941
- // ── Quiz Game Builder ──
1942
-
1943
- function buildQuiz(title, opts) {
1944
- const p = GAME_PALETTES.quiz;
2103
+ function buildQuiz(title, t, opts) {
1945
2104
  const perQ = opts.duration || 15;
1946
2105
 
1947
- const css = [cssVars(p), cssBase(p),
2106
+ const css = [cssBase(t), cssBtn(t), cssScr(), cssDsp(), cssStat(t), cssFx(t),
1948
2107
  `.q{font-size:1.3rem;margin:1.5rem 0;min-height:3em;line-height:1.5}`,
1949
2108
  `.ch{display:flex;flex-direction:column;gap:.75rem;margin:1rem 0}`,
1950
- `.ch div{padding:1rem;border:2px solid ${p.sf};border-radius:12px;background:${p.sf};cursor:pointer;font-size:1rem;text-align:left;transition:border-color .2s,background .2s}`,
1951
- `.ch div:hover{border-color:${p.pr}}`,
1952
- `.ch .ok{border-color:${p.ac};background:${p.ac}22}`,
2109
+ `.ch div{padding:1rem;border:2px solid ${t.sf};border-radius:12px;background:${t.sf};cursor:pointer;font-size:1rem;text-align:left;transition:border-color .2s,background .2s}`,
2110
+ `.ch div:hover{border-color:${t.pr}}`,
2111
+ `.ch .ok{border-color:${t.ac};background:${t.ac}22}`,
1953
2112
  `.ch .no{border-color:#F87171;background:#F8717122}`,
1954
2113
  `.dots{display:flex;gap:4px;justify-content:center;margin-bottom:1rem}`,
1955
- `.dot{width:10px;height:10px;border-radius:50%;background:${p.sf}}`,
1956
- `.dot.pass{background:${p.ac}}.dot.fail{background:#F87171}.dot.now{background:${p.pr};box-shadow:0 0 8px ${p.pr}}`,
2114
+ `.dot{width:10px;height:10px;border-radius:50%;background:${t.sf}}`,
2115
+ `.dot.pass{background:${t.ac}}.dot.fail{background:#F87171}.dot.now{background:${t.pr};box-shadow:0 0 8px ${t.pr}}`,
1957
2116
  `.tm{font-size:2.5rem;font-weight:800;font-variant-numeric:tabular-nums}`,
1958
2117
  ].join('\n');
1959
2118
 
1960
- const html = `<div id="s-menu">
1961
- <p style="color:${p.mt};margin:1rem 0">10 questions, ${perQ} seconds each</p>
1962
- <div class="st"><span>Best<b id="best">0</b></span><span>Last<b id="last">-</b></span></div>
1963
- <button class="btn" onclick="go()">Start Quiz</button></div>
1964
- <div id="s-play" class="hd">
1965
- <div class="dots" id="dots"></div>
2119
+ const html = [
2120
+ htmlScr('menu', [
2121
+ htmlP(`10 questions, ${perQ} seconds each`, `color:${t.mt};margin:1rem 0`),
2122
+ htmlStats([{label:'Best',id:'best',value:'0'},{label:'Last',id:'last',value:'-'}]),
2123
+ htmlBtn('Start Quiz', 'go()'),
2124
+ ].join('\n'), true),
2125
+ htmlScr('play', `<div class="dots" id="dots"></div>
1966
2126
  <div class="tm" id="timer">${perQ}</div>
1967
2127
  <div class="q" id="qtext"></div>
1968
- <div class="ch" id="choices"></div></div>
1969
- <div id="s-end" class="hd">
1970
- <div class="sc" id="fin">0/10</div>
1971
- <div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>
1972
- <button class="btn" onclick="go()">Play Again</button></div>`;
2128
+ <div class="ch" id="choices"></div>`),
2129
+ htmlScr('end', [
2130
+ htmlDsp('fin', '0/10'),
2131
+ `<div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>`,
2132
+ htmlBtn('Play Again', 'go()'),
2133
+ ].join('\n')),
2134
+ ].join('\n');
1973
2135
 
1974
- const js = [jsCore('quiz_best'), jsSfx(), jsParticles(p.pr, p.ac),
2136
+ const js = [jsHelper(), jsScreen(), jsStore('quiz'), jsScore(), jsSfx(), jsParticles(t.pr, t.ac), jsShuffle(),
1975
2137
  `const QS=[{q:"What does HTML stand for?",a:["HyperText Markup Language","High Tech Modern Language","Home Tool Markup Language","Hyper Transfer Markup Language"],c:0},{q:"Which planet is closest to the Sun?",a:["Mercury","Venus","Mars","Earth"],c:0},{q:"What is the largest ocean on Earth?",a:["Pacific","Atlantic","Indian","Arctic"],c:0},{q:"In JavaScript, which keyword declares a constant?",a:["const","let","var","static"],c:0},{q:"What year did the World Wide Web go public?",a:["1991","1989","1995","2000"],c:0},{q:"What is the chemical symbol for gold?",a:["Au","Ag","Go","Gd"],c:0},{q:"How many bits are in a byte?",a:["8","4","16","32"],c:0},{q:"What does CSS stand for?",a:["Cascading Style Sheets","Computer Style Sheets","Creative Style System","Coded Style Sheets"],c:0},{q:"What is the speed of light (approx km/s)?",a:["300,000","150,000","500,000","1,000,000"],c:0},{q:"Which data structure uses FIFO?",a:["Queue","Stack","Tree","Graph"],c:0},{q:"What is the square root of 144?",a:["12","14","10","16"],c:0},{q:"Who painted the Mona Lisa?",a:["Da Vinci","Picasso","Van Gogh","Monet"],c:0},{q:"What gas do plants absorb?",a:["CO2","O2","N2","H2"],c:0},{q:"Which protocol is used for secure browsing?",a:["HTTPS","FTP","SMTP","SSH"],c:0},{q:"What is 2^10?",a:["1024","512","2048","4096"],c:0},{q:"What is the binary representation of 10?",a:["1010","1100","1001","0110"],c:0},{q:"Which HTTP status code means Not Found?",a:["404","500","403","301"],c:0},{q:"What is the capital of Japan?",a:["Tokyo","Osaka","Kyoto","Nagoya"],c:0},{q:"How many continents are there?",a:["7","6","5","8"],c:0},{q:"What does API stand for?",a:["Application Programming Interface","Advanced Program Integration","Application Process Interface","Automated Programming Interface"],c:0}];`,
1976
2138
  `const PER_Q=${perQ};let cur,score,pool,tmr,tl,streak=0;`,
1977
- `function shuf(a){for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}`,
1978
2139
  `function go(){pool=shuf([...QS]).slice(0,10);cur=0;score=0;streak=0;$('dots').innerHTML=pool.map((_,i)=>'<div class="dot" id="d'+i+'"></div>').join('');show('play');next()}`,
1979
2140
  `function next(){if(cur>=pool.length)return end();$('d'+cur).classList.add('now');const q=pool[cur],mx=q.a.map((t,i)=>({t,ok:i===q.c}));shuf(mx);$('qtext').textContent=q.q;const ch=$('choices');ch.innerHTML='';mx.forEach(m=>{const d=document.createElement('div');d.textContent=m.t;d.onclick=()=>pick(d,m.ok,ch);ch.appendChild(d)});tl=PER_Q;$('timer').textContent=tl;clearInterval(tmr);tmr=setInterval(()=>{tl--;$('timer').textContent=Math.max(0,tl);if(tl<=3&&tl>0)sfxTick();if(tl<=0){clearInterval(tmr);timeout()}},1000)}`,
1980
2141
  `function pick(el,ok,ch){clearInterval(tmr);ch.querySelectorAll('div').forEach(c=>{c.onclick=null;if(c===el)c.classList.add(ok?'ok':'no');else if(!ok){const t=pool[cur].a[pool[cur].c];if(c.textContent===t)c.classList.add('ok')}});$('d'+cur).classList.remove('now');$('d'+cur).classList.add(ok?'pass':'fail');if(ok){score++;streak++;sfxScore();burstAt(el,streak>=3?20:8)}else{streak=0;sfxFail();shakeEl($('qtext'))}cur++;setTimeout(next,800)}`,
@@ -1982,83 +2143,75 @@ function buildQuiz(title, opts) {
1982
2143
  `function end(){clearInterval(tmr);const nb=saveBest(score);$('last').textContent=score;$('fin').textContent=score+'/'+pool.length;if(nb&&score>0){sfxWin();burstAt($('fin'),30)}const pc=Math.round(score/pool.length*100);const r=pc===100?'Perfect!':pc>=80?'Excellent!':pc>=60?'Good!':pc>=40?'Not bad!':'Study more!';$('msg').innerHTML=r+'<br>'+pc+'% correct'+(nb?' <span class=hi>★ New Best!</span>':'');show('end')}`,
1983
2144
  ].join('\n');
1984
2145
 
1985
- return htmlWrap(title, css, html, js);
2146
+ return assemblePage(title, css, html, js);
1986
2147
  }
1987
2148
 
1988
- // ── Memory Game Builder ──
1989
-
1990
- function buildMemory(title, opts) {
1991
- const p = GAME_PALETTES.memory;
2149
+ function buildMemory(title, t, opts) {
1992
2150
  const cols = opts.cols || 4;
1993
2151
  const pairs = Math.floor((cols * (opts.rows || cols)) / 2);
1994
2152
 
1995
- const css = [cssVars(p), cssBase(p),
1996
- `.grid{display:grid;grid-template-columns:repeat(${cols},1fr);gap:8px;margin:1rem auto;width:min(${cols*90}px,88vw)}`,
1997
- `.card{aspect-ratio:1;border-radius:12px;cursor:pointer;perspective:600px;-webkit-perspective:600px}`,
1998
- `.card .inn{position:relative;width:100%;height:100%;transition:transform .4s;transform-style:preserve-3d;-webkit-transform-style:preserve-3d}`,
1999
- `.card.flip .inn{transform:rotateY(180deg)}`,
2000
- `.card .fr,.card .bk{position:absolute;width:100%;height:100%;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:${cols<=4?'2rem':'1.4rem'};backface-visibility:hidden;-webkit-backface-visibility:hidden}`,
2001
- `.card .fr{background:linear-gradient(135deg,${p.pr},${p.sc})}`,
2002
- `.card .bk{background:${p.sf};transform:rotateY(180deg);border:2px solid ${p.pr}}`,
2003
- `.card.done .fr{background:linear-gradient(135deg,${p.ac},#059669)}`,
2004
- `.card.done{pointer-events:none}`,
2153
+ const css = [cssBase(t), cssBtn(t), cssScr(), cssDsp(), cssStat(t), cssFx(t),
2154
+ cssGrid(t, cols), cssCards(t, cols),
2005
2155
  ].join('\n');
2006
2156
 
2007
- const html = `<div id="s-menu">
2008
- <p style="color:${p.mt};margin:1rem 0">Match all ${pairs} pairs with fewest moves</p>
2009
- <div class="st"><span>Best<b id="best">-</b></span></div>
2010
- <button class="btn" onclick="go()">Start</button></div>
2011
- <div id="s-play" class="hd">
2012
- <div class="st"><span>Moves<b id="mv">0</b></span><span>Pairs<b id="pr">0/${pairs}</b></span><span>Time<b id="tm">0s</b></span></div>
2013
- <div class="grid" id="grid"></div></div>
2014
- <div id="s-end" class="hd">
2015
- <div class="sc" id="fin">0</div>
2016
- <p style="font-size:1.2rem;margin:.5rem 0">moves</p>
2017
- <div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>
2018
- <button class="btn" onclick="go()">Play Again</button></div>`;
2019
-
2020
- const js = [jsCore('mem_best'), jsSfx(), jsParticles(p.pr, p.ac),
2157
+ const html = [
2158
+ htmlScr('menu', [
2159
+ htmlP(`Match all ${pairs} pairs with fewest moves`, `color:${t.mt};margin:1rem 0`),
2160
+ htmlStats([{label:'Best',id:'best',value:'-'}]),
2161
+ htmlBtn('Start', 'go()'),
2162
+ ].join('\n'), true),
2163
+ htmlScr('play', [
2164
+ htmlStats([{label:'Moves',id:'mv',value:'0'},{label:'Pairs',id:'pr',value:`0/${pairs}`},{label:'Time',id:'tm',value:'0s'}]),
2165
+ `<div class="grid" id="grid"></div>`,
2166
+ ].join('\n')),
2167
+ htmlScr('end', [
2168
+ htmlDsp('fin', '0'),
2169
+ htmlP('moves', 'font-size:1.2rem;margin:.5rem 0'),
2170
+ `<div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>`,
2171
+ htmlBtn('Play Again', 'go()'),
2172
+ ].join('\n')),
2173
+ ].join('\n');
2174
+
2175
+ const js = [jsHelper(), jsScreen(), jsStore('mem'), jsScore(), jsSfx(), jsParticles(t.pr, t.ac), jsShuffle(),
2021
2176
  `const EM=['🎮','🎯','🚀','💎','🔥','⚡','🌟','🎵','🎨','🏆','🍕','🌈','🦊','🐙','🎃','🍀','🎲','🌸'];`,
2022
2177
  `const PAIRS=${pairs};let cards,flp,done,moves,tmr,t0,cstrk=0;`,
2023
- `function shuf(a){for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}`,
2024
2178
  `function go(){const pick=shuf([...EM]).slice(0,PAIRS);cards=shuf([...pick,...pick]);flp=[];done=0;moves=0;cstrk=0;$('mv').textContent=0;$('pr').textContent='0/'+PAIRS;const g=$('grid');g.innerHTML='';cards.forEach((em,i)=>{const d=document.createElement('div');d.className='card';d.innerHTML='<div class="inn"><div class="fr">?</div><div class="bk">'+em+'</div></div>';d.onclick=()=>flip(d,i);g.appendChild(d)});t0=Date.now();tmr=setInterval(()=>{$('tm').textContent=Math.floor((Date.now()-t0)/1000)+'s'},200);show('play')}`,
2025
2179
  `function flip(el,i){if(flp.length>=2||el.classList.contains('flip')||el.classList.contains('done'))return;sfxTap();el.classList.add('flip');flp.push({el,i,v:cards[i]});if(flp.length===2){moves++;$('mv').textContent=moves;const[a,b]=flp;if(a.v===b.v){cstrk++;if(cstrk>=2)sfxCombo();else sfxScore();a.el.classList.add('done');b.el.classList.add('done');burstAt(a.el,cstrk>=2?16:8);burstAt(b.el,cstrk>=2?16:8);done++;$('pr').textContent=done+'/'+PAIRS;flp=[];if(done===PAIRS)end()}else{cstrk=0;sfxHit();setTimeout(()=>{a.el.classList.remove('flip');b.el.classList.remove('flip');flp=[]},700)}}}`,
2026
2180
  `function end(){clearInterval(tmr);const sec=Math.floor((Date.now()-t0)/1000);const nb=saveBest(moves);if(nb)sfxWin();$('fin').textContent=moves;burstAt($('fin'),30);const r=moves<=PAIRS+2?'Perfect memory!':moves<=PAIRS*2?'Great!':moves<=PAIRS*3?'Good!':'Keep practicing!';$('msg').innerHTML=r+'<br>Completed in '+sec+'s'+(nb?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2027
2181
  ].join('\n');
2028
2182
 
2029
- return htmlWrap(title, css, html, js);
2183
+ return assemblePage(title, css, html, js);
2030
2184
  }
2031
2185
 
2032
- // ── Snake Game Builder ──
2033
-
2034
- function buildSnake(title, opts) {
2035
- const p = GAME_PALETTES.snake;
2186
+ function buildSnake(title, t, opts) {
2036
2187
  const spd = opts.speed === 'fast' ? 80 : opts.difficulty === 'hard' ? 90 : 120;
2037
2188
  const W = 16, S = 20;
2038
2189
 
2039
- const css = [cssVars(p), cssBase(p),
2040
- `canvas{border-radius:12px;background:${p.sf};display:block;margin:1rem auto;image-rendering:pixelated;touch-action:none}`,
2190
+ const css = [cssBase(t), cssBtn(t), cssScr(), cssDsp(), cssStat(t), cssFx(t), cssCanvas(t),
2041
2191
  `.ctl{display:grid;grid-template-columns:repeat(3,56px);grid-template-rows:repeat(2,56px);gap:6px;justify-content:center;margin:1rem auto}`,
2042
- `.ctl button{border:none;border-radius:10px;background:${p.sf};color:${p.tx};font-size:1.4rem;cursor:pointer;display:flex;align-items:center;justify-content:center}`,
2043
- `.ctl button:active{background:${p.pr}}`,
2192
+ `.ctl button{border:none;border-radius:10px;background:${t.sf};color:${t.tx};font-size:1.4rem;cursor:pointer;display:flex;align-items:center;justify-content:center}`,
2193
+ `.ctl button:active{background:${t.pr}}`,
2044
2194
  ].join('\n');
2045
2195
 
2046
- const html = `<div id="s-menu">
2047
- <p style="color:${p.mt};margin:1rem 0">Arrow keys or swipe to control</p>
2048
- <div class="st"><span>Best<b id="best">0</b></span></div>
2049
- <button class="btn" onclick="go()">Play</button></div>
2050
- <div id="s-play" class="hd">
2051
- <div class="st"><span>Score<b id="score">0</b></span><span>Best<b id="hi">0</b></span></div>
2052
- <canvas id="cv" width="${W*S}" height="${W*S}"></canvas>
2053
- <div class="ctl">
2054
- <div></div><button ontouchstart="sd(0,-1)" onclick="sd(0,-1)">▲</button><div></div>
2055
- <button ontouchstart="sd(-1,0)" onclick="sd(-1,0)">◀</button><button ontouchstart="sd(0,1)" onclick="sd(0,1)">▼</button><button ontouchstart="sd(1,0)" onclick="sd(1,0)">▶</button></div></div>
2056
- <div id="s-end" class="hd">
2057
- <div class="sc" id="fin">0</div>
2058
- <div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>
2059
- <button class="btn" onclick="go()">Play Again</button></div>`;
2060
-
2061
- const js = [jsCore('snake_best'), jsSfx(), jsParticles(p.pr, p.ac), jsCountdown(),
2196
+ const html = [
2197
+ htmlScr('menu', [
2198
+ htmlP('Arrow keys or swipe to control', `color:${t.mt};margin:1rem 0`),
2199
+ htmlStats([{label:'Best',id:'best',value:'0'}]),
2200
+ htmlBtn('Play', 'go()'),
2201
+ ].join('\n'), true),
2202
+ htmlScr('play', [
2203
+ htmlStats([{label:'Score',id:'score',value:'0'},{label:'Best',id:'hi',value:'0'}]),
2204
+ `<canvas id="cv" width="${W*S}" height="${W*S}"></canvas>`,
2205
+ `<div class="ctl"><div></div><button ontouchstart="sd(0,-1)" onclick="sd(0,-1)">▲</button><div></div><button ontouchstart="sd(-1,0)" onclick="sd(-1,0)">◀</button><button ontouchstart="sd(0,1)" onclick="sd(0,1)">▼</button><button ontouchstart="sd(1,0)" onclick="sd(1,0)">▶</button></div>`,
2206
+ ].join('\n')),
2207
+ htmlScr('end', [
2208
+ htmlDsp('fin', '0'),
2209
+ `<div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>`,
2210
+ htmlBtn('Play Again', 'go()'),
2211
+ ].join('\n')),
2212
+ ].join('\n');
2213
+
2214
+ const js = [jsHelper(), jsScreen(), jsStore('snake'), jsScore(), jsSfx(), jsParticles(t.pr, t.ac), jsCountdown(t.ac),
2062
2215
  `const GS=${S},GW=${W},cv=$('cv'),ctx=cv.getContext('2d'),BASE_SPD=${spd};`,
2063
2216
  `let sn,dir,nd,food,sc,lp,spd;$('hi').textContent=best;`,
2064
2217
  `function rnd(){let p;do{p={x:Math.floor(Math.random()*GW),y:Math.floor(Math.random()*GW)}}while(sn.some(s=>s.x===p.x&&s.y===p.y));return p}`,
@@ -2066,94 +2219,96 @@ function buildSnake(title, opts) {
2066
2219
  `function startGame(){sn=[{x:8,y:8},{x:7,y:8},{x:6,y:8}];dir={x:1,y:0};nd={x:1,y:0};food=rnd();sc=0;spd=BASE_SPD;$('score').textContent=0;clearInterval(lp);lp=setInterval(tick,spd)}`,
2067
2220
  `function sd(x,y){if(x===-dir.x&&y===-dir.y)return;nd={x,y}}`,
2068
2221
  `function tick(){dir=nd;const h={x:sn[0].x+dir.x,y:sn[0].y+dir.y};if(h.x<0||h.x>=GW||h.y<0||h.y>=GW||sn.some(s=>s.x===h.x&&s.y===h.y))return end();sn.unshift(h);if(h.x===food.x&&h.y===food.y){sc++;sfxScore();$('score').textContent=sc;const rc=cv.getBoundingClientRect();emitParticles(rc.left+food.x*GS*(rc.width/(GW*GS))+GS/2,rc.top+food.y*GS*(rc.height/(GW*GS))+GS/2,10);food=rnd();if(sc%5===0&&spd>50){spd-=8;clearInterval(lp);lp=setInterval(tick,spd)}}else sn.pop();draw()}`,
2069
- `function draw(){ctx.fillStyle='${p.sf}';ctx.fillRect(0,0,GW*GS,GW*GS);for(let x=0;x<GW;x++)for(let y=0;y<GW;y++){ctx.fillStyle='rgba(255,255,255,.015)';if((x+y)%2===0)ctx.fillRect(x*GS,y*GS,GS,GS)}ctx.fillStyle='${p.ac}';ctx.beginPath();ctx.arc(food.x*GS+GS/2,food.y*GS+GS/2,GS/2-2+Math.sin(Date.now()/200)*2,0,Math.PI*2);ctx.fill();ctx.shadowColor='${p.ac}';ctx.shadowBlur=8;ctx.fill();ctx.shadowBlur=0;sn.forEach((s,i)=>{const t=i/sn.length;ctx.fillStyle=i===0?'${p.pr}':'hsl('+(140+t*40)+',70%,'+(55-t*20)+'%)';ctx.beginPath();ctx.roundRect(s.x*GS+1,s.y*GS+1,GS-2,GS-2,4);ctx.fill();if(i===0){ctx.fillStyle='${p.bg}';const ex=dir.x===1?GS-6:dir.x===-1?4:dir.y===0?GS/2:dir.y===1?GS-6:4;const ey=dir.y===1?GS-6:dir.y===-1?4:dir.x===0?GS/2:dir.x===1?GS/2-2:GS/2-2;ctx.beginPath();ctx.arc(s.x*GS+ex,s.y*GS+ey,2,0,Math.PI*2);ctx.fill()}})}`,
2222
+ `function draw(){ctx.fillStyle='${t.sf}';ctx.fillRect(0,0,GW*GS,GW*GS);for(let x=0;x<GW;x++)for(let y=0;y<GW;y++){ctx.fillStyle='rgba(255,255,255,.015)';if((x+y)%2===0)ctx.fillRect(x*GS,y*GS,GS,GS)}ctx.fillStyle='${t.ac}';ctx.beginPath();ctx.arc(food.x*GS+GS/2,food.y*GS+GS/2,GS/2-2+Math.sin(Date.now()/200)*2,0,Math.PI*2);ctx.fill();ctx.shadowColor='${t.ac}';ctx.shadowBlur=8;ctx.fill();ctx.shadowBlur=0;sn.forEach((s,i)=>{const t=i/sn.length;ctx.fillStyle=i===0?'${t.pr}':'hsl('+(140+t*40)+',70%,'+(55-t*20)+'%)';ctx.beginPath();ctx.roundRect(s.x*GS+1,s.y*GS+1,GS-2,GS-2,4);ctx.fill();if(i===0){ctx.fillStyle='${t.bg}';const ex=dir.x===1?GS-6:dir.x===-1?4:dir.y===0?GS/2:dir.y===1?GS-6:4;const ey=dir.y===1?GS-6:dir.y===-1?4:dir.x===0?GS/2:dir.x===1?GS/2-2:GS/2-2;ctx.beginPath();ctx.arc(s.x*GS+ex,s.y*GS+ey,2,0,Math.PI*2);ctx.fill()}})}`,
2070
2223
  `function end(){clearInterval(lp);sfxFail();const nb=saveBest(sc);$('hi').textContent=best;$('fin').textContent=sc;if(nb&&sc>0){sfxWin();burstAt($('fin'),30)}const r=sc>=30?'Legend!':sc>=20?'Amazing!':sc>=10?'Great!':sc>=5?'Good!':'Try again!';$('msg').innerHTML=r+(nb&&sc>0?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2071
2224
  `document.addEventListener('keydown',e=>{if(e.key==='ArrowUp'){e.preventDefault();sd(0,-1)}if(e.key==='ArrowDown'){e.preventDefault();sd(0,1)}if(e.key==='ArrowLeft')sd(-1,0);if(e.key==='ArrowRight')sd(1,0)});`,
2072
2225
  `let tx,ty;cv.addEventListener('touchstart',e=>{tx=e.touches[0].clientX;ty=e.touches[0].clientY},{passive:true});cv.addEventListener('touchend',e=>{const dx=e.changedTouches[0].clientX-tx,dy=e.changedTouches[0].clientY-ty;if(Math.abs(dx)>Math.abs(dy))sd(dx>0?1:-1,0);else sd(0,dy>0?1:-1)},{passive:true});`,
2073
2226
  ].join('\n');
2074
2227
 
2075
- return htmlWrap(title, css, html, js);
2228
+ return assemblePage(title, css, html, js);
2076
2229
  }
2077
2230
 
2078
- // ── Reaction Game Builder ──
2079
-
2080
- function buildReaction(title, opts) {
2081
- const p = GAME_PALETTES.reaction;
2231
+ function buildReaction(title, t, opts) {
2082
2232
  const rounds = opts.difficulty === 'hard' ? 10 : 5;
2083
2233
 
2084
- const css = [cssVars(p), cssBase(p),
2234
+ const css = [cssBase(t), cssBtn(t), cssScr(), cssDsp(), cssStat(t), cssFx(t),
2085
2235
  `.zone{width:min(340px,85vw);height:min(340px,85vw);border-radius:24px;display:flex;align-items:center;justify-content:center;margin:1.5rem auto;cursor:pointer;font-size:1.2rem;font-weight:600;transition:background .2s}`,
2086
- `.zone.wait{background:${p.sf}}`,
2087
- `.zone.ready{background:${p.pr};color:#fff}`,
2088
- `.zone.go{background:${p.ac};color:${p.bg}}`,
2236
+ `.zone.wait{background:${t.sf}}`,
2237
+ `.zone.ready{background:${t.pr};color:#fff}`,
2238
+ `.zone.go{background:${t.ac};color:${t.bg}}`,
2089
2239
  `.zone.early{background:#DC2626}`,
2090
2240
  `.times{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin:1rem 0}`,
2091
- `.times span{background:${p.sf};padding:.3rem .6rem;border-radius:6px;font-size:.85rem;font-variant-numeric:tabular-nums}`,
2241
+ `.times span{background:${t.sf};padding:.3rem .6rem;border-radius:6px;font-size:.85rem;font-variant-numeric:tabular-nums}`,
2092
2242
  `.ms{font-size:3.5rem;font-weight:900;font-variant-numeric:tabular-nums}`,
2093
2243
  ].join('\n');
2094
2244
 
2095
- const html = `<div id="s-menu">
2096
- <p style="color:${p.mt};margin:1rem 0">Tap when it turns green — ${rounds} rounds</p>
2097
- <div class="st"><span>Best Avg<b id="best">-</b></span></div>
2098
- <button class="btn" onclick="go()">Start</button></div>
2099
- <div id="s-play" class="hd">
2100
- <div class="st"><span>Round<b id="rnd">1/${rounds}</b></span><span>Avg<b id="avg">-</b></span></div>
2101
- <div class="zone wait" id="zone" onmousedown="tap()" ontouchstart="tap()"><span id="ztxt">Wait...</span></div>
2102
- <div class="times" id="times"></div></div>
2103
- <div id="s-end" class="hd">
2104
- <div class="ms" id="fin">0</div>
2105
- <p style="font-size:1.2rem;margin:.5rem 0">ms average</p>
2106
- <div id="msg" style="margin:1rem 0"></div>
2107
- <div class="times" id="ftimes"></div>
2108
- <button class="btn" style="margin-top:1rem" onclick="go()">Try Again</button></div>`;
2109
-
2110
- const js = [jsCore('react_best'), jsSfx(), jsParticles(p.pr, p.ac),
2245
+ const html = [
2246
+ htmlScr('menu', [
2247
+ htmlP(`Tap when it turns green — ${rounds} rounds`, `color:${t.mt};margin:1rem 0`),
2248
+ htmlStats([{label:'Best Avg',id:'best',value:'-'}]),
2249
+ htmlBtn('Start', 'go()'),
2250
+ ].join('\n'), true),
2251
+ htmlScr('play', [
2252
+ htmlStats([{label:'Round',id:'rnd',value:`1/${rounds}`},{label:'Avg',id:'avg',value:'-'}]),
2253
+ `<div class="zone wait" id="zone" onmousedown="tap()" ontouchstart="tap()"><span id="ztxt">Wait...</span></div>`,
2254
+ `<div class="times" id="times"></div>`,
2255
+ ].join('\n')),
2256
+ htmlScr('end', [
2257
+ `<div class="ms" id="fin">0</div>`,
2258
+ htmlP('ms average', 'font-size:1.2rem;margin:.5rem 0'),
2259
+ `<div id="msg" style="margin:1rem 0"></div>`,
2260
+ `<div class="times" id="ftimes"></div>`,
2261
+ htmlBtn('Try Again', 'go()'),
2262
+ ].join('\n')),
2263
+ ].join('\n');
2264
+
2265
+ const js = [jsHelper(), jsScreen(), jsStore('react'), jsScore(), jsSfx(), jsParticles(t.pr, t.ac),
2111
2266
  `if(best)$('best').textContent=best+'ms';`,
2112
2267
  `const RDS=${rounds};let rd,res,phase,gAt,tmr;`,
2113
2268
  `function go(){rd=0;res=[];$('times').innerHTML='';show('play');nr()}`,
2114
2269
  `function nr(){if(rd>=RDS)return end();$('rnd').textContent=(rd+1)+'/'+RDS;const z=$('zone');z.className='zone ready';$('ztxt').textContent='Wait for green...';phase='wait';clearTimeout(tmr);tmr=setTimeout(()=>{z.className='zone go';$('ztxt').textContent='TAP NOW!';sfxTick();gAt=Date.now();phase='go'},1500+Math.random()*3000)}`,
2115
- `function tap(){if(phase==='wait'){clearTimeout(tmr);sfxFail();shakeEl($('zone'));$('zone').className='zone early';$('ztxt').textContent='Too early! Tap to retry';phase='early'}else if(phase==='early'){nr()}else if(phase==='go'){const ms=Date.now()-gAt;res.push(ms);sfxScore();burstAt($('zone'),ms<=250?20:8);const t=document.createElement('span');t.textContent=ms+'ms';if(ms<=200)t.style.color='${p.ac}';$('times').appendChild(t);$('avg').textContent=Math.round(res.reduce((a,b)=>a+b,0)/res.length)+'ms';rd++;phase='done';setTimeout(nr,600)}}`,
2270
+ `function tap(){if(phase==='wait'){clearTimeout(tmr);sfxFail();shakeEl($('zone'));$('zone').className='zone early';$('ztxt').textContent='Too early! Tap to retry';phase='early'}else if(phase==='early'){nr()}else if(phase==='go'){const ms=Date.now()-gAt;res.push(ms);sfxScore();burstAt($('zone'),ms<=250?20:8);const t=document.createElement('span');t.textContent=ms+'ms';if(ms<=200)t.style.color='${t.ac}';$('times').appendChild(t);$('avg').textContent=Math.round(res.reduce((a,b)=>a+b,0)/res.length)+'ms';rd++;phase='done';setTimeout(nr,600)}}`,
2116
2271
  `function end(){const avg=Math.round(res.reduce((a,b)=>a+b,0)/res.length);const nb=saveBest(avg);if(nb){$('best').textContent=avg+'ms';sfxWin();burstAt($('fin'),30)}$('fin').textContent=avg;$('ftimes').innerHTML=res.map((r,i)=>'<span>R'+(i+1)+': '+r+'ms</span>').join('');const r=avg<=200?'Superhuman!':avg<=250?'Lightning!':avg<=300?'Fast!':avg<=400?'Average':'Keep practicing!';$('msg').innerHTML='<span style="font-size:1.2rem">'+r+'</span>'+(nb?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2117
2272
  ].join('\n');
2118
2273
 
2119
- return htmlWrap(title, css, html, js);
2274
+ return assemblePage(title, css, html, js);
2120
2275
  }
2121
2276
 
2122
- // ── Typing Game Builder ──
2123
-
2124
- function buildTyping(title, opts) {
2125
- const p = GAME_PALETTES.typing;
2277
+ function buildTyping(title, t, opts) {
2126
2278
  const dur = opts.duration || 30;
2127
2279
 
2128
- const css = [cssVars(p), cssBase(p), cssTimerBar(p),
2280
+ const css = [cssBase(t), cssBtn(t), cssScr(), cssDsp(), cssStat(t), cssBar(t), cssFx(t), cssInput(t),
2129
2281
  `.wd{font-size:2rem;font-weight:700;letter-spacing:.1em;margin:1.5rem 0;min-height:3rem;display:flex;justify-content:center;gap:2px;flex-wrap:wrap}`,
2130
- `.wd .c{transition:color .1s}.wd .y{color:${p.ac}}.wd .n{color:#F87171}`,
2131
- `.wd .cur{border-bottom:3px solid ${p.pr};animation:blink 1s step-end infinite}`,
2282
+ `.wd .c{transition:color .1s}.wd .y{color:${t.ac}}.wd .n{color:#F87171}`,
2283
+ `.wd .cur{border-bottom:3px solid ${t.pr};animation:blink 1s step-end infinite}`,
2132
2284
  `@keyframes blink{50%{border-color:transparent}}`,
2133
- `.ti{background:${p.sf};border:2px solid ${p.pr};border-radius:12px;padding:.75rem 1rem;font-size:1.2rem;color:${p.tx};width:100%;text-align:center;outline:none;caret-color:${p.pr};font-family:inherit}`,
2134
- `.wq{color:${p.mt};font-size:1rem;margin:.5rem 0;height:2em;overflow:hidden;display:flex;gap:1rem;justify-content:center;flex-wrap:wrap}`,
2285
+ `.wq{color:${t.mt};font-size:1rem;margin:.5rem 0;height:2em;overflow:hidden;display:flex;gap:1rem;justify-content:center;flex-wrap:wrap}`,
2135
2286
  ].join('\n');
2136
2287
 
2137
- const html = `<div id="s-menu">
2138
- <p style="color:${p.mt};margin:1rem 0">Type as many words as you can in ${dur} seconds</p>
2139
- <div class="st"><span>Best WPM<b id="best">0</b></span></div>
2140
- <button class="btn" onclick="go()">Start</button></div>
2141
- <div id="s-play" class="hd">
2142
- <div class="st"><span>Time<b id="timer">${dur}</b></span><span>Words<b id="wc">0</b></span><span>WPM<b id="wpm">0</b></span></div>
2143
- <div class="bar"><div class="fl" id="bar" style="width:100%"></div></div>
2144
- <div class="wd" id="disp"></div>
2145
- <div class="wq" id="queue"></div>
2146
- <input class="ti" id="inp" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" placeholder="Type here..."></div>
2147
- <div id="s-end" class="hd">
2148
- <div class="sc" id="fin">0</div>
2149
- <p style="font-size:1.2rem;margin:.5rem 0">WPM</p>
2150
- <div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>
2151
- <button class="btn" onclick="go()">Play Again</button></div>`;
2152
-
2153
- const js = [jsCore('type_best'), jsSfx(), jsParticles(p.pr, p.ac), jsCountdown(),
2288
+ const html = [
2289
+ htmlScr('menu', [
2290
+ htmlP(`Type as many words as you can in ${dur} seconds`, `color:${t.mt};margin:1rem 0`),
2291
+ htmlStats([{label:'Best WPM',id:'best',value:'0'}]),
2292
+ htmlBtn('Start', 'go()'),
2293
+ ].join('\n'), true),
2294
+ htmlScr('play', [
2295
+ htmlStats([{label:'Time',id:'timer',value:dur},{label:'Words',id:'wc',value:'0'},{label:'WPM',id:'wpm',value:'0'}]),
2296
+ htmlBar(),
2297
+ `<div class="wd" id="disp"></div>`,
2298
+ `<div class="wq" id="queue"></div>`,
2299
+ `<input class="input" id="inp" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" placeholder="Type here...">`,
2300
+ ].join('\n')),
2301
+ htmlScr('end', [
2302
+ htmlDsp('fin', '0'),
2303
+ htmlP('WPM', 'font-size:1.2rem;margin:.5rem 0'),
2304
+ `<div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>`,
2305
+ htmlBtn('Play Again', 'go()'),
2306
+ ].join('\n')),
2307
+ ].join('\n');
2308
+
2309
+ const js = [jsHelper(), jsScreen(), jsStore('type'), jsScore(), jsTimer(), jsSfx(), jsParticles(t.pr, t.ac), jsCountdown(t.ac), jsShuffle(),
2154
2310
  `const WDS=["the","be","to","and","in","that","have","for","not","with","you","this","but","from","they","say","she","or","will","my","one","all","would","there","what","so","up","out","if","about","who","get","which","go","when","make","can","like","time","just","know","take","people","into","year","good","some","could","them","see","other","than","then","now","look","only","come","its","over","think","also","back","after","use","two","how","work","first","well","even","new","want","give","most","find","here","thing","code","fast","type","game","play","next","open","read","test","data","file","line","word","text","long","name","part","best","help","made","move","hand","high","keep","last","same","tell","set","each","much","turn","real","show","full","form","left","start","might","run","need","home","life","old","big","end","point","still","call","live","away","right","hard","plan","team","late","mind","wait","stop","must","land","close","draw","press","mark","step","pick","rock","blue","deep","dark","hold","four","week","room","free","fall","base","rest","less","note","hear"];`,
2155
2311
  `const DUR=${dur*1000};let pool,ci,pos,done,t0,tmr,tried=0,streak=0;`,
2156
- `function shuf(a){for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}`,
2157
2312
  `function go(){show('play');countdown(()=>{pool=shuf([...WDS]);ci=0;pos=0;done=0;tried=0;streak=0;t0=Date.now();$('wc').textContent=0;$('wpm').textContent=0;$('inp').value='';$('inp').focus();render();tmr=setInterval(tick,100)})}`,
2158
2313
  `function render(){const w=pool[ci%pool.length];const v=$('inp').value;$('disp').innerHTML=w.split('').map((c,i)=>{let cl=i<pos?(v[i]===c?'y':'n'):'';if(i===pos)cl+=' cur';return'<span class="c '+cl+'">'+c+'</span>'}).join('');$('queue').innerHTML='';for(let i=1;i<=5;i++)$('queue').innerHTML+='<span>'+pool[(ci+i)%pool.length]+'</span>'}`,
2159
2314
  `function tick(){const e=Date.now()-t0,pc=Math.max(0,1-e/DUR)*100;$('bar').style.width=pc+'%';$('timer').textContent=Math.max(0,Math.ceil((DUR-e)/1000));$('wpm').textContent=Math.round(done/Math.max(.01,(e/60000)));if(e>=DUR)end()}`,
@@ -2161,33 +2316,33 @@ function buildTyping(title, opts) {
2161
2316
  `function end(){clearInterval(tmr);$('inp').blur();const wpm=Math.round(done/(DUR/60000));const acc=tried>0?Math.round(done/tried*100):0;const nb=saveBest(wpm);if(nb&&wpm>0){sfxWin();burstAt($('fin'),30)}else sfxFail();$('fin').textContent=wpm;const r=wpm>=80?'Blazing!':wpm>=60?'Excellent!':wpm>=40?'Good!':wpm>=20?'Decent!':'Keep practicing!';$('msg').innerHTML=r+'<br>'+done+' words in ${dur}s — '+acc+'% accuracy'+(nb&&wpm>0?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2162
2317
  ].join('\n');
2163
2318
 
2164
- return htmlWrap(title, css, html, js);
2319
+ return assemblePage(title, css, html, js);
2165
2320
  }
2166
2321
 
2167
- // ── Breakout Game Builder ──
2168
-
2169
- function buildBreakout(title, opts) {
2170
- const p = GAME_PALETTES.breakout;
2322
+ function buildBreakout(title, t, opts) {
2171
2323
  const COLS = 8, ROWS = opts.difficulty === 'hard' ? 7 : 5;
2172
2324
 
2173
- const css = [cssVars(p), cssBase(p),
2174
- `canvas{border-radius:12px;background:${p.sf};display:block;margin:1rem auto;touch-action:none}`,
2325
+ const css = [cssBase(t), cssBtn(t), cssScr(), cssDsp(), cssStat(t), cssFx(t), cssCanvas(t)].join('\n');
2326
+
2327
+ const html = [
2328
+ htmlScr('menu', [
2329
+ htmlP(`Move the paddle to break all ${COLS * ROWS} blocks`, `color:${t.mt};margin:1rem 0`),
2330
+ htmlStats([{label:'Best',id:'best',value:'0'}]),
2331
+ htmlBtn('Play', 'go()'),
2332
+ ].join('\n'), true),
2333
+ htmlScr('play', [
2334
+ htmlStats([{label:'Score',id:'score',value:'0'},{label:'Lives',id:'lives',value:'3'}]),
2335
+ `<canvas id="cv" width="360" height="480"></canvas>`,
2336
+ ].join('\n')),
2337
+ htmlScr('end', [
2338
+ htmlDsp('fin', '0'),
2339
+ `<div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>`,
2340
+ htmlBtn('Play Again', 'go()'),
2341
+ ].join('\n')),
2175
2342
  ].join('\n');
2176
2343
 
2177
- const html = `<div id="s-menu">
2178
- <p style="color:${p.mt};margin:1rem 0">Move the paddle to break all ${COLS * ROWS} blocks</p>
2179
- <div class="st"><span>Best<b id="best">0</b></span></div>
2180
- <button class="btn" onclick="go()">Play</button></div>
2181
- <div id="s-play" class="hd">
2182
- <div class="st"><span>Score<b id="score">0</b></span><span>Lives<b id="lives">3</b></span></div>
2183
- <canvas id="cv" width="360" height="480"></canvas></div>
2184
- <div id="s-end" class="hd">
2185
- <div class="sc" id="fin">0</div>
2186
- <div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>
2187
- <button class="btn" onclick="go()">Play Again</button></div>`;
2188
-
2189
- const brickColors = `['${p.pr}','${p.sc}','#A78BFA','${p.ac}','#EC4899']`;
2190
- const js = [jsCore('brk_best'), jsSfx(), jsParticles(p.pr, p.ac),
2344
+ const brickColors = `['${t.pr}','${t.sc}','#A78BFA','${t.ac}','#EC4899']`;
2345
+ const js = [jsHelper(), jsScreen(), jsStore('brk'), jsScore(), jsSfx(), jsParticles(t.pr, t.ac),
2191
2346
  `const cv=$('cv'),ctx=cv.getContext('2d'),CW=360,CH=480,COLS=${COLS},ROWS=${ROWS},BW=CW/COLS-4,BH=18,PW=70,PH=12,BR=6;`,
2192
2347
  `const BC=${brickColors};`,
2193
2348
  `let px,ball,dx,dy,bricks,sc,lives,raf,cmb=0;`,
@@ -2195,46 +2350,46 @@ function buildBreakout(title, opts) {
2195
2350
  `function go(){px=CW/2;ball={x:CW/2,y:CH-60};dx=3*(Math.random()>.5?1:-1);dy=-3.5;sc=0;lives=3;cmb=0;mkBricks();$('score').textContent=0;$('lives').textContent=3;show('play');cancelAnimationFrame(raf);tick()}`,
2196
2351
  `function tick(){update();draw();if(lives>0&&bricks.some(b=>b.alive))raf=requestAnimationFrame(tick);else end()}`,
2197
2352
  `function update(){ball.x+=dx;ball.y+=dy;if(ball.x<=BR||ball.x>=CW-BR)dx=-dx;if(ball.y<=BR)dy=-dy;if(ball.y>=CH-PH-BR-8&&ball.y<=CH-8&&ball.x>=px-PW/2&&ball.x<=px+PW/2){dy=-Math.abs(dy);dx+=((ball.x-px)/(PW/2))*2;sfxTap();cmb=0}if(ball.y>CH){lives--;sfxFail();$('lives').textContent=lives;cmb=0;if(lives>0){ball={x:CW/2,y:CH-60};dx=3*(Math.random()>.5?1:-1);dy=-3.5}}let hitAny=false;for(const b of bricks){if(!b.alive)continue;if(ball.x+BR>b.x&&ball.x-BR<b.x+b.w&&ball.y+BR>b.y&&ball.y-BR<b.y+b.h){b.alive=false;dy=-dy;cmb++;const pts=10*(cmb>=5?3:cmb>=3?2:1);sc+=pts;hitAny=true;sfxHit();const rc=cv.getBoundingClientRect();emitParticles(rc.left+b.x*(rc.width/CW)+BW/2,rc.top+b.y*(rc.height/CH)+BH/2,cmb>=3?12:6,b.color);$('score').textContent=sc;if(cmb>=5){const sp=Math.sqrt(dx*dx+dy*dy);if(sp<7){dx*=1.05;dy*=1.05}}}}}`,
2198
- `function draw(){ctx.clearRect(0,0,CW,CH);for(const b of bricks){if(!b.alive)continue;ctx.fillStyle=b.color;ctx.beginPath();ctx.roundRect(b.x,b.y,b.w,b.h,4);ctx.fill();ctx.fillStyle='rgba(255,255,255,.15)';ctx.fillRect(b.x+2,b.y+2,b.w-4,BH/3)}ctx.fillStyle='${p.pr}';ctx.shadowColor='${p.pr}';ctx.shadowBlur=10;ctx.beginPath();ctx.roundRect(px-PW/2,CH-PH-8,PW,PH,6);ctx.fill();ctx.shadowBlur=0;ctx.fillStyle='${p.ac}';ctx.shadowColor='${p.ac}';ctx.shadowBlur=6;ctx.beginPath();ctx.arc(ball.x,ball.y,BR,0,Math.PI*2);ctx.fill();ctx.shadowBlur=0}`,
2353
+ `function draw(){ctx.clearRect(0,0,CW,CH);for(const b of bricks){if(!b.alive)continue;ctx.fillStyle=b.color;ctx.beginPath();ctx.roundRect(b.x,b.y,b.w,b.h,4);ctx.fill();ctx.fillStyle='rgba(255,255,255,.15)';ctx.fillRect(b.x+2,b.y+2,b.w-4,BH/3)}ctx.fillStyle='${t.pr}';ctx.shadowColor='${t.pr}';ctx.shadowBlur=10;ctx.beginPath();ctx.roundRect(px-PW/2,CH-PH-8,PW,PH,6);ctx.fill();ctx.shadowBlur=0;ctx.fillStyle='${t.ac}';ctx.shadowColor='${t.ac}';ctx.shadowBlur=6;ctx.beginPath();ctx.arc(ball.x,ball.y,BR,0,Math.PI*2);ctx.fill();ctx.shadowBlur=0}`,
2199
2354
  `function end(){cancelAnimationFrame(raf);const won=!bricks.some(b=>b.alive);const nb=saveBest(sc);if(won||nb)sfxWin();$('fin').textContent=sc;if(nb&&sc>0)burstAt($('fin'),30);$('msg').innerHTML=(won?'You cleared it!':'Game Over')+(nb&&sc>0?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2200
2355
  `cv.addEventListener('mousemove',e=>{const r=cv.getBoundingClientRect();px=Math.max(PW/2,Math.min(CW-PW/2,(e.clientX-r.left)*(CW/r.width)))});`,
2201
2356
  `cv.addEventListener('touchmove',e=>{e.preventDefault();const r=cv.getBoundingClientRect();px=Math.max(PW/2,Math.min(CW-PW/2,(e.touches[0].clientX-r.left)*(CW/r.width)))},{passive:false});`,
2202
2357
  `document.addEventListener('keydown',e=>{if(e.key==='ArrowLeft')px=Math.max(PW/2,px-20);if(e.key==='ArrowRight')px=Math.min(CW-PW/2,px+20)});`,
2203
2358
  ].join('\n');
2204
2359
 
2205
- return htmlWrap(title, css, html, js);
2360
+ return assemblePage(title, css, html, js);
2206
2361
  }
2207
2362
 
2208
- // ── Dodge Game Builder ──
2209
-
2210
- function buildDodge(title, opts) {
2211
- const p = GAME_PALETTES.dodge;
2363
+ function buildDodge(title, t, opts) {
2212
2364
  const spd = opts.difficulty === 'hard' ? 1.4 : opts.difficulty === 'easy' ? 0.7 : 1.0;
2213
2365
 
2214
- const css = [cssVars(p), cssBase(p),
2215
- `canvas{border-radius:12px;background:${p.sf};display:block;margin:1rem auto;touch-action:none}`,
2366
+ const css = [cssBase(t), cssBtn(t), cssScr(), cssDsp(), cssStat(t), cssFx(t), cssCanvas(t)].join('\n');
2367
+
2368
+ const html = [
2369
+ htmlScr('menu', [
2370
+ htmlP('Move to dodge the falling obstacles!', `color:${t.mt};margin:1rem 0`),
2371
+ htmlStats([{label:'Best',id:'best',value:'0'}]),
2372
+ htmlBtn('Play', 'go()'),
2373
+ ].join('\n'), true),
2374
+ htmlScr('play', [
2375
+ htmlStats([{label:'Score',id:'score',value:'0'},{label:'Best',id:'hi',value:'0'}]),
2376
+ `<canvas id="cv" width="360" height="520"></canvas>`,
2377
+ ].join('\n')),
2378
+ htmlScr('end', [
2379
+ htmlDsp('fin', '0'),
2380
+ `<div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>`,
2381
+ htmlBtn('Play Again', 'go()'),
2382
+ ].join('\n')),
2216
2383
  ].join('\n');
2217
2384
 
2218
- const html = `<div id="s-menu">
2219
- <p style="color:${p.mt};margin:1rem 0">Move to dodge the falling obstacles!</p>
2220
- <div class="st"><span>Best<b id="best">0</b></span></div>
2221
- <button class="btn" onclick="go()">Play</button></div>
2222
- <div id="s-play" class="hd">
2223
- <div class="st"><span>Score<b id="score">0</b></span><span>Best<b id="hi">0</b></span></div>
2224
- <canvas id="cv" width="360" height="520"></canvas></div>
2225
- <div id="s-end" class="hd">
2226
- <div class="sc" id="fin">0</div>
2227
- <div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>
2228
- <button class="btn" onclick="go()">Play Again</button></div>`;
2229
-
2230
- const js = [jsCore('dodge_best'), jsSfx(), jsParticles(p.pr, p.ac), jsCountdown(),
2385
+ const js = [jsHelper(), jsScreen(), jsStore('dodge'), jsScore(), jsSfx(), jsParticles(t.pr, t.ac), jsCountdown(t.ac),
2231
2386
  `const cv=$('cv'),ctx=cv.getContext('2d'),CW=360,CH=520,PW=30,PH=30,SPD_MUL=${spd};`,
2232
2387
  `let px,py,obs,sc,raf,t0,spd,spawnT,alive;$('hi').textContent=best;`,
2233
2388
  `function go(){show('play');countdown(startGame)}`,
2234
2389
  `function startGame(){px=CW/2;py=CH-60;obs=[];sc=0;spd=2*SPD_MUL;spawnT=0;alive=true;t0=Date.now();$('score').textContent=0;cancelAnimationFrame(raf);tick()}`,
2235
2390
  `function tick(){if(!alive)return;update();draw();raf=requestAnimationFrame(tick)}`,
2236
- `function update(){spawnT++;const diff=Math.min(30,Math.floor((Date.now()-t0)/5000));if(spawnT%(Math.max(8,30-diff*2))===0){const w=20+Math.random()*30;obs.push({x:Math.random()*(CW-w),y:-30,w,h:20+Math.random()*10,s:spd+Math.random()*2+diff*0.3,c:['${p.pr}','${p.sc}','#FF4757','#FF6B81','#FFA502'][Math.floor(Math.random()*5)]})}obs.forEach(o=>o.y+=o.s);obs=obs.filter(o=>{if(o.y>CH){sc++;$('score').textContent=sc;if(sc%10===0)sfxScore();return false}return true});for(const o of obs){if(px+PW/2>o.x&&px-PW/2<o.x+o.w&&py+PH/2>o.y&&py-PH/2<o.y+o.h){alive=false;sfxFail();const rc=cv.getBoundingClientRect();emitParticles(rc.left+px*(rc.width/CW),rc.top+py*(rc.height/CH),25);end();return}}spd=2*SPD_MUL+Math.floor((Date.now()-t0)/10000)*0.5}`,
2237
- `function draw(){ctx.fillStyle='${p.sf}';ctx.fillRect(0,0,CW,CH);for(let i=0;i<CW;i+=40)for(let j=0;j<CH;j+=40){ctx.fillStyle='rgba(255,255,255,.01)';if((Math.floor(i/40)+Math.floor(j/40))%2===0)ctx.fillRect(i,j,40,40)}obs.forEach(o=>{ctx.fillStyle=o.c;ctx.beginPath();ctx.roundRect(o.x,o.y,o.w,o.h,4);ctx.fill()});ctx.fillStyle='${p.ac}';ctx.shadowColor='${p.ac}';ctx.shadowBlur=12;ctx.beginPath();ctx.arc(px,py,PW/2,0,Math.PI*2);ctx.fill();ctx.shadowBlur=0;ctx.fillStyle='${p.bg}';ctx.beginPath();ctx.arc(px-5,py-4,3,0,Math.PI*2);ctx.arc(px+5,py-4,3,0,Math.PI*2);ctx.fill();ctx.strokeStyle='${p.bg}';ctx.lineWidth=2;ctx.beginPath();ctx.arc(px,py+2,6,0,Math.PI);ctx.stroke()}`,
2391
+ `function update(){spawnT++;const diff=Math.min(30,Math.floor((Date.now()-t0)/5000));if(spawnT%(Math.max(8,30-diff*2))===0){const w=20+Math.random()*30;obs.push({x:Math.random()*(CW-w),y:-30,w,h:20+Math.random()*10,s:spd+Math.random()*2+diff*0.3,c:['${t.pr}','${t.sc}','#FF4757','#FF6B81','#FFA502'][Math.floor(Math.random()*5)]})}obs.forEach(o=>o.y+=o.s);obs=obs.filter(o=>{if(o.y>CH){sc++;$('score').textContent=sc;if(sc%10===0)sfxScore();return false}return true});for(const o of obs){if(px+PW/2>o.x&&px-PW/2<o.x+o.w&&py+PH/2>o.y&&py-PH/2<o.y+o.h){alive=false;sfxFail();const rc=cv.getBoundingClientRect();emitParticles(rc.left+px*(rc.width/CW),rc.top+py*(rc.height/CH),25);end();return}}spd=2*SPD_MUL+Math.floor((Date.now()-t0)/10000)*0.5}`,
2392
+ `function draw(){ctx.fillStyle='${t.sf}';ctx.fillRect(0,0,CW,CH);for(let i=0;i<CW;i+=40)for(let j=0;j<CH;j+=40){ctx.fillStyle='rgba(255,255,255,.01)';if((Math.floor(i/40)+Math.floor(j/40))%2===0)ctx.fillRect(i,j,40,40)}obs.forEach(o=>{ctx.fillStyle=o.c;ctx.beginPath();ctx.roundRect(o.x,o.y,o.w,o.h,4);ctx.fill()});ctx.fillStyle='${t.ac}';ctx.shadowColor='${t.ac}';ctx.shadowBlur=12;ctx.beginPath();ctx.arc(px,py,PW/2,0,Math.PI*2);ctx.fill();ctx.shadowBlur=0;ctx.fillStyle='${t.bg}';ctx.beginPath();ctx.arc(px-5,py-4,3,0,Math.PI*2);ctx.arc(px+5,py-4,3,0,Math.PI*2);ctx.fill();ctx.strokeStyle='${t.bg}';ctx.lineWidth=2;ctx.beginPath();ctx.arc(px,py+2,6,0,Math.PI);ctx.stroke()}`,
2238
2393
  `function end(){cancelAnimationFrame(raf);const nb=saveBest(sc);$('hi').textContent=best;$('fin').textContent=sc;if(nb&&sc>0){sfxWin();burstAt($('fin'),30)}const r=sc>=100?'Untouchable!':sc>=60?'Amazing!':sc>=30?'Great!':sc>=15?'Good!':'Keep dodging!';$('msg').innerHTML=r+(nb&&sc>0?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2239
2394
  `let keys={};document.addEventListener('keydown',e=>{keys[e.key]=true;e.preventDefault()});document.addEventListener('keyup',e=>keys[e.key]=false);`,
2240
2395
  `setInterval(()=>{if(!alive)return;const s=5;if(keys.ArrowLeft||keys.a)px=Math.max(PW/2,px-s);if(keys.ArrowRight||keys.d)px=Math.min(CW-PW/2,px+s);if(keys.ArrowUp||keys.w)py=Math.max(PH/2,py-s);if(keys.ArrowDown||keys.s)py=Math.min(CH-PH/2,py+s)},16);`,
@@ -2244,13 +2399,10 @@ function buildDodge(title, opts) {
2244
2399
  `cv.addEventListener('mousemove',e=>{if(!alive)return;const r=cv.getBoundingClientRect();px=Math.max(PW/2,Math.min(CW-PW/2,(e.clientX-r.left)*(CW/r.width)));py=Math.max(PH/2,Math.min(CH-PH/2,(e.clientY-r.top)*(CH/r.height)))});`,
2245
2400
  ].join('\n');
2246
2401
 
2247
- return htmlWrap(title, css, html, js);
2402
+ return assemblePage(title, css, html, js);
2248
2403
  }
2249
2404
 
2250
- // ── Color Match Game Builder ──
2251
-
2252
- function buildColor(title, opts) {
2253
- const p = GAME_PALETTES.color;
2405
+ function buildColor(title, t, opts) {
2254
2406
  const dur = opts.duration || 30;
2255
2407
  const COLORS = [
2256
2408
  {name:'Red',hex:'#FF4757'},{name:'Blue',hex:'#3742FA'},{name:'Green',hex:'#2ED573'},
@@ -2258,117 +2410,482 @@ function buildColor(title, opts) {
2258
2410
  {name:'Pink',hex:'#FF6B81'},{name:'Cyan',hex:'#18DCFF'},
2259
2411
  ];
2260
2412
 
2261
- const css = [cssVars(p), cssBase(p), cssTimerBar(p),
2413
+ const css = [cssBase(t), cssBtn(t), cssScr(), cssDsp(), cssStat(t), cssBar(t), cssFx(t),
2262
2414
  `.clr-name{font-size:2.8rem;font-weight:900;margin:1.5rem 0;letter-spacing:.05em;transition:transform .1s}`,
2263
2415
  `.opts{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:1rem 0}`,
2264
2416
  `.opts div{padding:1.2rem;border-radius:14px;cursor:pointer;font-size:1.1rem;font-weight:700;text-align:center;transition:transform .1s,box-shadow .2s;color:#fff;text-shadow:0 1px 3px rgba(0,0,0,.4)}`,
2265
2417
  `.opts div:active{transform:scale(.93)}`,
2266
- `.strk{font-size:.95rem;color:${p.ac};min-height:1.5em;font-weight:700}`,
2418
+ `.strk{font-size:.95rem;color:${t.ac};min-height:1.5em;font-weight:700}`,
2267
2419
  ].join('\n');
2268
2420
 
2269
- const html = `<div id="s-menu">
2270
- <p style="color:${p.mt};margin:1rem 0">Tap the color shown, not the word! ${dur}s</p>
2271
- <div class="st"><span>Best<b id="best">0</b></span><span>Last<b id="last">-</b></span></div>
2272
- <button class="btn" onclick="go()">Start</button></div>
2273
- <div id="s-play" class="hd">
2274
- <div class="st"><span>Score<b id="sc">0</b></span><span>Time<b id="timer">${dur}</b></span><span>Streak<b id="strk">0</b></span></div>
2275
- <div class="bar"><div class="fl" id="bar" style="width:100%"></div></div>
2276
- <div class="clr-name" id="word">-</div>
2277
- <div class="strk" id="stxt"></div>
2278
- <div class="opts" id="opts"></div></div>
2279
- <div id="s-end" class="hd">
2280
- <div class="sc" id="fin">0</div>
2281
- <div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>
2282
- <button class="btn" onclick="go()">Play Again</button></div>`;
2421
+ const html = [
2422
+ htmlScr('menu', [
2423
+ htmlP(`Tap the color shown, not the word! ${dur}s`, `color:${t.mt};margin:1rem 0`),
2424
+ htmlStats([{label:'Best',id:'best',value:'0'},{label:'Last',id:'last',value:'-'}]),
2425
+ htmlBtn('Start', 'go()'),
2426
+ ].join('\n'), true),
2427
+ htmlScr('play', [
2428
+ htmlStats([{label:'Score',id:'sc',value:'0'},{label:'Time',id:'timer',value:dur},{label:'Streak',id:'strk',value:'0'}]),
2429
+ htmlBar(),
2430
+ `<div class="clr-name" id="word">-</div>`,
2431
+ `<div class="strk" id="stxt"></div>`,
2432
+ `<div class="opts" id="opts"></div>`,
2433
+ ].join('\n')),
2434
+ htmlScr('end', [
2435
+ htmlDsp('fin', '0'),
2436
+ `<div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>`,
2437
+ htmlBtn('Play Again', 'go()'),
2438
+ ].join('\n')),
2439
+ ].join('\n');
2283
2440
 
2284
2441
  const colorsJS = JSON.stringify(COLORS);
2285
- const js = [jsCore('color_best'), jsSfx(), jsParticles(p.pr, p.ac), jsCountdown(),
2442
+ const js = [jsHelper(), jsScreen(), jsStore('color'), jsScore(), jsTimer(), jsSfx(), jsParticles(t.pr, t.ac), jsCountdown(t.ac), jsShuffle(),
2286
2443
  `const CS=${colorsJS};`,
2287
- `const DUR=${dur*1000};let sc,t0,tmr,streak,correct;`,
2288
- `function shuf(a){for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}`,
2444
+ `const DUR=${dur*1000};let sc,t0,tmrI,streak,correct;`,
2289
2445
  `function go(){show('play');countdown(startGame)}`,
2290
- `function startGame(){sc=0;streak=0;t0=Date.now();$('sc').textContent=0;$('strk').textContent=0;$('stxt').textContent='';tmr=setInterval(tick,100);nextRound()}`,
2291
- `function tick(){const e=Date.now()-t0,pc=Math.max(0,1-e/DUR)*100;$('bar').style.width=pc+'%';$('timer').textContent=Math.max(0,Math.ceil((DUR-e)/1000));if(e>=DUR)end()}`,
2446
+ `function startGame(){sc=0;streak=0;t0=Date.now();$('sc').textContent=0;$('strk').textContent=0;$('stxt').textContent='';tmrI=setInterval(tickI,100);nextRound()}`,
2447
+ `function tickI(){const e=Date.now()-t0,pc=Math.max(0,1-e/DUR)*100;$('bar').style.width=pc+'%';$('timer').textContent=Math.max(0,Math.ceil((DUR-e)/1000));if(e>=DUR)end()}`,
2292
2448
  `function nextRound(){const pool=shuf([...CS]);const ans=pool[0];const fakeText=pool[1+Math.floor(Math.random()*(pool.length-1))];correct=ans.name;$('word').textContent=fakeText.name;$('word').style.color=ans.hex;const choices=shuf([ans,...shuf(pool.filter(c=>c.name!==ans.name)).slice(0,3)]);const o=$('opts');o.innerHTML='';choices.forEach(c=>{const d=document.createElement('div');d.textContent=c.name;d.style.background=c.hex;d.onclick=()=>pick(c.name===correct,d);o.appendChild(d)})}`,
2293
2449
  `function pick(ok,el){if(Date.now()-t0>DUR)return;$('opts').querySelectorAll('div').forEach(d=>d.onclick=null);if(ok){sc++;streak++;sfxScore();$('sc').textContent=sc;$('strk').textContent=streak;burstAt(el,streak>=5?20:8);if(streak>=3){sfxCombo();$('stxt').textContent=streak>=10?'ON FIRE!':streak>=5?'AMAZING!':'STREAK!'}}else{streak=0;sfxFail();shakeEl($('word'));$('strk').textContent=0;$('stxt').textContent=''}setTimeout(nextRound,ok?300:600)}`,
2294
- `function end(){clearInterval(tmr);const nb=saveBest(sc);$('last').textContent=sc;$('fin').textContent=sc;if(nb&&sc>0){sfxWin();burstAt($('fin'),30)}const r=sc>=50?'Color Master!':sc>=35?'Amazing!':sc>=20?'Great!':sc>=10?'Good!':'Keep trying!';$('msg').innerHTML=r+'<br>'+sc+' in ${dur}s'+(nb?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2450
+ `function end(){clearInterval(tmrI);const nb=saveBest(sc);$('last').textContent=sc;$('fin').textContent=sc;if(nb&&sc>0){sfxWin();burstAt($('fin'),30)}const r=sc>=50?'Color Master!':sc>=35?'Amazing!':sc>=20?'Great!':sc>=10?'Good!':'Keep trying!';$('msg').innerHTML=r+'<br>'+sc+' in ${dur}s'+(nb?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2295
2451
  ].join('\n');
2296
2452
 
2297
- return htmlWrap(title, css, html, js);
2453
+ return assemblePage(title, css, html, js);
2298
2454
  }
2299
2455
 
2300
- // ── Pong Game Builder ──
2301
-
2302
- function buildPong(title, opts) {
2303
- const p = GAME_PALETTES.pong;
2456
+ function buildPong(title, t, opts) {
2304
2457
  const winScore = opts.difficulty === 'hard' ? 11 : 7;
2305
2458
 
2306
- const css = [cssVars(p), cssBase(p),
2307
- `canvas{border-radius:12px;background:${p.bg};display:block;margin:1rem auto;touch-action:none;border:2px solid ${p.sf}}`,
2459
+ const css = [cssBase(t), cssBtn(t), cssScr(), cssDsp(), cssStat(t), cssFx(t),
2460
+ `canvas{border-radius:12px;background:${t.bg};display:block;margin:1rem auto;touch-action:none;border:2px solid ${t.sf}}`,
2308
2461
  `.vs{display:flex;justify-content:center;gap:3rem;font-size:2.5rem;font-weight:900;font-variant-numeric:tabular-nums;margin:.5rem 0}`,
2309
- `.vs span:first-child{color:${p.pr}}.vs span:last-child{color:${p.sc}}`,
2462
+ `.vs span:first-child{color:${t.pr}}.vs span:last-child{color:${t.sc}}`,
2463
+ ].join('\n');
2464
+
2465
+ const html = [
2466
+ htmlScr('menu', [
2467
+ htmlP(`First to ${winScore} wins — you vs AI`, `color:${t.mt};margin:1rem 0`),
2468
+ htmlStats([{label:'Wins',id:'best',value:'0'}]),
2469
+ htmlBtn('Play', 'go()'),
2470
+ ].join('\n'), true),
2471
+ htmlScr('play', [
2472
+ `<div class="vs"><span id="p1">0</span><span id="p2">0</span></div>`,
2473
+ `<canvas id="cv" width="360" height="420"></canvas>`,
2474
+ htmlP('Move mouse/touch or arrow keys', `color:${t.mt};font-size:.8rem;margin:.5rem`),
2475
+ ].join('\n')),
2476
+ htmlScr('end', [
2477
+ htmlDsp('fin', 'WIN'),
2478
+ `<div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>`,
2479
+ htmlBtn('Rematch', 'go()'),
2480
+ ].join('\n')),
2310
2481
  ].join('\n');
2311
2482
 
2312
- const html = `<div id="s-menu">
2313
- <p style="color:${p.mt};margin:1rem 0">First to ${winScore} wins — you vs AI</p>
2314
- <div class="st"><span>Wins<b id="best">0</b></span></div>
2315
- <button class="btn" onclick="go()">Play</button></div>
2316
- <div id="s-play" class="hd">
2317
- <div class="vs"><span id="p1">0</span><span id="p2">0</span></div>
2318
- <canvas id="cv" width="360" height="420"></canvas>
2319
- <p style="color:${p.mt};font-size:.8rem;margin:.5rem">Move mouse/touch or arrow keys</p></div>
2320
- <div id="s-end" class="hd">
2321
- <div class="sc" id="fin">WIN</div>
2322
- <div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>
2323
- <button class="btn" onclick="go()">Rematch</button></div>`;
2324
-
2325
- const js = [jsCore('pong_best'), jsSfx(), jsParticles(p.pr, p.ac),
2483
+ const js = [jsHelper(), jsScreen(), jsStore('pong'), jsScore(), jsSfx(), jsParticles(t.pr, t.ac),
2326
2484
  `const cv=$('cv'),ctx=cv.getContext('2d'),CW=360,CH=420,PW=70,PH=10,BR=7,WIN=${winScore};`,
2327
2485
  `let py,ay,bx,by,bdx,bdy,s1,s2,raf,aiSpd;`,
2328
2486
  `function go(){py=CW/2;ay=CW/2;bx=CW/2;by=CH/2;bdx=3*(Math.random()>.5?1:-1);bdy=3*(Math.random()>.5?1:-1);s1=0;s2=0;aiSpd=3;$('p1').textContent=0;$('p2').textContent=0;show('play');cancelAnimationFrame(raf);tick()}`,
2329
2487
  `function tick(){update();draw();if(s1<WIN&&s2<WIN)raf=requestAnimationFrame(tick);else end()}`,
2330
- `function update(){bx+=bdx;by+=bdy;if(bx<=BR||bx>=CW-BR)bdx=-bdx;if(by<=PH+BR+4&&bx>=ay-PW/2&&bx<=ay+PW/2){bdy=Math.abs(bdy);bdx+=((bx-ay)/(PW/2))*1.5;sfxTap();const rc=cv.getBoundingClientRect();emitParticles(rc.left+bx*(rc.width/CW),rc.top+by*(rc.height/CH),4,'${p.sc}')}if(by>=CH-PH-BR-4&&bx>=py-PW/2&&bx<=py+PW/2){bdy=-Math.abs(bdy);bdx+=((bx-py)/(PW/2))*1.5;sfxHit();const rc=cv.getBoundingClientRect();emitParticles(rc.left+bx*(rc.width/CW),rc.top+by*(rc.height/CH),4,'${p.pr}')}if(by<0){s2++;sfxScore();$('p1').textContent=s2;reset()}if(by>CH){s1++;sfxFail();$('p2').textContent=s1;reset()}const diff=bx-ay;ay+=Math.min(aiSpd,Math.abs(diff))*(diff>0?1:-1)*(0.7+Math.random()*0.3);ay=Math.max(PW/2,Math.min(CW-PW/2,ay));aiSpd=3+Math.max(s1,s2)*0.3}`,
2488
+ `function update(){bx+=bdx;by+=bdy;if(bx<=BR||bx>=CW-BR)bdx=-bdx;if(by<=PH+BR+4&&bx>=ay-PW/2&&bx<=ay+PW/2){bdy=Math.abs(bdy);bdx+=((bx-ay)/(PW/2))*1.5;sfxTap();const rc=cv.getBoundingClientRect();emitParticles(rc.left+bx*(rc.width/CW),rc.top+by*(rc.height/CH),4,'${t.sc}')}if(by>=CH-PH-BR-4&&bx>=py-PW/2&&bx<=py+PW/2){bdy=-Math.abs(bdy);bdx+=((bx-py)/(PW/2))*1.5;sfxHit();const rc=cv.getBoundingClientRect();emitParticles(rc.left+bx*(rc.width/CW),rc.top+by*(rc.height/CH),4,'${t.pr}')}if(by<0){s2++;sfxScore();$('p1').textContent=s2;reset()}if(by>CH){s1++;sfxFail();$('p2').textContent=s1;reset()}const diff=bx-ay;ay+=Math.min(aiSpd,Math.abs(diff))*(diff>0?1:-1)*(0.7+Math.random()*0.3);ay=Math.max(PW/2,Math.min(CW-PW/2,ay));aiSpd=3+Math.max(s1,s2)*0.3}`,
2331
2489
  `function reset(){bx=CW/2;by=CH/2;bdx=3*(Math.random()>.5?1:-1);bdy=3*(Math.random()>.5?1:-1)}`,
2332
- `function draw(){ctx.fillStyle='${p.bg}';ctx.fillRect(0,0,CW,CH);ctx.setLineDash([6,6]);ctx.strokeStyle='${p.sf}';ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(0,CH/2);ctx.lineTo(CW,CH/2);ctx.stroke();ctx.setLineDash([]);ctx.fillStyle='${p.sc}';ctx.shadowColor='${p.sc}';ctx.shadowBlur=8;ctx.beginPath();ctx.roundRect(ay-PW/2,4,PW,PH,4);ctx.fill();ctx.fillStyle='${p.pr}';ctx.shadowColor='${p.pr}';ctx.beginPath();ctx.roundRect(py-PW/2,CH-PH-4,PW,PH,4);ctx.fill();ctx.shadowBlur=0;ctx.fillStyle='${p.ac}';ctx.shadowColor='${p.ac}';ctx.shadowBlur=10;ctx.beginPath();ctx.arc(bx,by,BR,0,Math.PI*2);ctx.fill();ctx.shadowBlur=0}`,
2333
- `function end(){cancelAnimationFrame(raf);const won=s1<s2;if(won){sfxWin();saveBest(best+1);$('fin').textContent='YOU WIN!';$('fin').style.color='${p.ac}';burstAt($('fin'),30)}else{sfxFail();$('fin').textContent='AI WINS';$('fin').style.color='${p.pr}'}$('msg').innerHTML=s2+' - '+s1+(won?' — Great match!':' — Try again!');show('end')}`,
2490
+ `function draw(){ctx.fillStyle='${t.bg}';ctx.fillRect(0,0,CW,CH);ctx.setLineDash([6,6]);ctx.strokeStyle='${t.sf}';ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(0,CH/2);ctx.lineTo(CW,CH/2);ctx.stroke();ctx.setLineDash([]);ctx.fillStyle='${t.sc}';ctx.shadowColor='${t.sc}';ctx.shadowBlur=8;ctx.beginPath();ctx.roundRect(ay-PW/2,4,PW,PH,4);ctx.fill();ctx.fillStyle='${t.pr}';ctx.shadowColor='${t.pr}';ctx.beginPath();ctx.roundRect(py-PW/2,CH-PH-4,PW,PH,4);ctx.fill();ctx.shadowBlur=0;ctx.fillStyle='${t.ac}';ctx.shadowColor='${t.ac}';ctx.shadowBlur=10;ctx.beginPath();ctx.arc(bx,by,BR,0,Math.PI*2);ctx.fill();ctx.shadowBlur=0}`,
2491
+ `function end(){cancelAnimationFrame(raf);const won=s1<s2;if(won){sfxWin();saveBest(best+1);$('fin').textContent='YOU WIN!';$('fin').style.color='${t.ac}';burstAt($('fin'),30)}else{sfxFail();$('fin').textContent='AI WINS';$('fin').style.color='${t.pr}'}$('msg').innerHTML=s2+' - '+s1+(won?' — Great match!':' — Try again!');show('end')}`,
2334
2492
  `cv.addEventListener('mousemove',e=>{const r=cv.getBoundingClientRect();py=Math.max(PW/2,Math.min(CW-PW/2,(e.clientX-r.left)*(CW/r.width)))});`,
2335
2493
  `cv.addEventListener('touchmove',e=>{e.preventDefault();const r=cv.getBoundingClientRect();py=Math.max(PW/2,Math.min(CW-PW/2,(e.touches[0].clientX-r.left)*(CW/r.width)))},{passive:false});`,
2336
2494
  `document.addEventListener('keydown',e=>{if(e.key==='ArrowLeft')py=Math.max(PW/2,py-20);if(e.key==='ArrowRight')py=Math.min(CW-PW/2,py+20)});`,
2337
2495
  ].join('\n');
2338
2496
 
2339
- return htmlWrap(title, css, html, js);
2497
+ return assemblePage(title, css, html, js);
2498
+ }
2499
+
2500
+ // ── App Builders ──
2501
+
2502
+ function buildCalculator(title, t, opts) {
2503
+ const css = [cssBase(t), cssBtn(t), cssFx(t),
2504
+ `.calc-dsp{background:${t.sf};border-radius:16px;padding:1.2rem;margin:1rem 0;text-align:right;min-height:4.5rem;display:flex;flex-direction:column;justify-content:center}`,
2505
+ `.calc-expr{font-size:.9rem;color:${t.mt};min-height:1.2em;word-break:break-all}`,
2506
+ `.calc-val{font-size:2.4rem;font-weight:800;font-variant-numeric:tabular-nums;word-break:break-all}`,
2507
+ `.calc-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin:.5rem 0}`,
2508
+ `.calc-grid button{padding:1rem;border:none;border-radius:12px;font-size:1.2rem;font-weight:600;cursor:pointer;transition:transform .1s}`,
2509
+ `.calc-grid button:active{transform:scale(.93)}`,
2510
+ `.calc-num{background:${t.sf};color:${t.tx}}`,
2511
+ `.calc-op{background:${t.pr};color:#fff}`,
2512
+ `.calc-fn{background:${t.sf}88;color:${t.mt}}`,
2513
+ `.calc-eq{background:linear-gradient(135deg,${t.pr},${t.sc});color:#fff}`,
2514
+ opts.history ? `.calc-hist{max-height:30vh;overflow-y:auto;margin-top:.5rem}.calc-hist div{font-size:.85rem;color:${t.mt};padding:.3rem 0;border-bottom:1px solid ${t.sf};text-align:right}` : '',
2515
+ ].join('\n');
2516
+
2517
+ const histHTML = opts.history ? `<div class="calc-hist" id="hist"></div>` : '';
2518
+ const html = `<div class="calc-dsp">
2519
+ <div class="calc-expr" id="expr"></div>
2520
+ <div class="calc-val" id="val">0</div></div>
2521
+ <div class="calc-grid">
2522
+ <button class="calc-fn" onclick="clr()">C</button>
2523
+ <button class="calc-fn" onclick="pct()">%</button>
2524
+ <button class="calc-fn" onclick="neg()">±</button>
2525
+ <button class="calc-op" onclick="op('/')">÷</button>
2526
+ <button class="calc-num" onclick="num('7')">7</button>
2527
+ <button class="calc-num" onclick="num('8')">8</button>
2528
+ <button class="calc-num" onclick="num('9')">9</button>
2529
+ <button class="calc-op" onclick="op('*')">×</button>
2530
+ <button class="calc-num" onclick="num('4')">4</button>
2531
+ <button class="calc-num" onclick="num('5')">5</button>
2532
+ <button class="calc-num" onclick="num('6')">6</button>
2533
+ <button class="calc-op" onclick="op('-')">−</button>
2534
+ <button class="calc-num" onclick="num('1')">1</button>
2535
+ <button class="calc-num" onclick="num('2')">2</button>
2536
+ <button class="calc-num" onclick="num('3')">3</button>
2537
+ <button class="calc-op" onclick="op('+')">+</button>
2538
+ <button class="calc-num" style="grid-column:span 2" onclick="num('0')">0</button>
2539
+ <button class="calc-num" onclick="dot()">.</button>
2540
+ <button class="calc-eq" onclick="eq()">=</button></div>${histHTML}`;
2541
+
2542
+ const histJS = opts.history ? `function addHist(expr,res){const d=document.createElement('div');d.textContent=expr+' = '+res;$('hist').prepend(d)}` : `function addHist(){}`;
2543
+ const js = [jsHelper(),
2544
+ `let cur='0',prev='',oper='',fresh=true;`,
2545
+ `function upd(){$('val').textContent=cur;$('expr').textContent=prev+(oper?{'+':' + ','-':' − ','*':' × ','/':' ÷ '}[oper]:'')}`,
2546
+ `function num(n){if(fresh){cur='';fresh=false}if(cur==='0'&&n!=='.')cur=n;else if(cur.length<15)cur+=n;upd()}`,
2547
+ `function dot(){if(!cur.includes('.')){if(fresh){cur='0';fresh=false}cur+='.'}upd()}`,
2548
+ `function op(o){if(prev&&oper&&!fresh)eq();prev=cur;oper=o;fresh=true;upd()}`,
2549
+ `function eq(){if(!oper||!prev)return;const a=parseFloat(prev),b=parseFloat(cur);let r;if(oper==='+')r=a+b;else if(oper==='-')r=a-b;else if(oper==='*')r=a*b;else r=b!==0?a/b:'Error';const expr=prev+{'+':'+','-':'-','*':'×','/':'÷'}[oper]+cur;addHist(expr,typeof r==='number'?+r.toPrecision(12):r);prev='';oper='';cur=typeof r==='number'?String(+r.toPrecision(12)):String(r);fresh=true;upd()}`,
2550
+ `function clr(){cur='0';prev='';oper='';fresh=true;upd()}`,
2551
+ `function pct(){cur=String(parseFloat(cur)/100);upd()}`,
2552
+ `function neg(){cur=String(-parseFloat(cur));upd()}`,
2553
+ histJS,
2554
+ `document.addEventListener('keydown',e=>{if(e.key>='0'&&e.key<='9')num(e.key);else if(e.key==='.')dot();else if(e.key==='+')op('+');else if(e.key==='-')op('-');else if(e.key==='*')op('*');else if(e.key==='/')op('/');else if(e.key==='Enter'||e.key==='=')eq();else if(e.key==='Escape'||e.key==='c'||e.key==='C')clr();e.preventDefault()});`,
2555
+ ].join('\n');
2556
+
2557
+ return assemblePage(title, css, html, js);
2558
+ }
2559
+
2560
+ function buildTimerApp(title, t, opts) {
2561
+ const css = [cssBase(t), cssBtn(t), cssDsp(), cssFx(t),
2562
+ `.tmr-dsp{font-size:4rem;font-weight:200;font-variant-numeric:tabular-nums;letter-spacing:.05em;margin:2rem 0;font-family:'Courier New',monospace}`,
2563
+ `.tmr-btns{display:flex;gap:12px;justify-content:center;margin:1rem 0}`,
2564
+ `.tmr-btns .btn{padding:.6rem 1.5rem;font-size:1rem}`,
2565
+ `.lap{max-height:40vh;overflow-y:auto;margin-top:1rem}`,
2566
+ `.lap div{display:flex;justify-content:space-between;padding:.4rem .5rem;font-size:.9rem;color:${t.mt};border-bottom:1px solid ${t.sf}22}`,
2567
+ `.lap div b{color:${t.tx}}`,
2568
+ ].join('\n');
2569
+
2570
+ const html = `<div class="tmr-dsp" id="dsp">00:00.00</div>
2571
+ <div class="tmr-btns">
2572
+ <button class="btn" id="main" onclick="toggle()">Start</button>
2573
+ <button class="btn" onclick="lap()" style="background:${t.sf};color:${t.tx}">Lap</button>
2574
+ <button class="btn" onclick="rst()" style="background:${t.sf};color:${t.tx}">Reset</button></div>
2575
+ <div class="lap" id="laps"></div>`;
2576
+
2577
+ const js = [jsHelper(), jsSfx(),
2578
+ `let t0=0,elapsed=0,running=false,raf,lapN=0;`,
2579
+ `function fmt(ms){const m=Math.floor(ms/60000),s=Math.floor(ms%60000/1000),cs=Math.floor(ms%1000/10);return String(m).padStart(2,'0')+':'+String(s).padStart(2,'0')+'.'+String(cs).padStart(2,'0')}`,
2580
+ `function upd(){$('dsp').textContent=fmt(elapsed+(running?Date.now()-t0:0))}`,
2581
+ `function loop(){upd();if(running)raf=requestAnimationFrame(loop)}`,
2582
+ `function toggle(){if(running){elapsed+=Date.now()-t0;running=false;cancelAnimationFrame(raf);$('main').textContent='Resume';sfxHit()}else{t0=Date.now();running=true;$('main').textContent='Stop';loop();sfxTap()}}`,
2583
+ `function lap(){if(!running&&elapsed===0)return;lapN++;const ms=elapsed+(running?Date.now()-t0:0);const d=document.createElement('div');d.innerHTML='<span>Lap '+lapN+'</span><b>'+fmt(ms)+'</b>';$('laps').prepend(d);sfxScore()}`,
2584
+ `function rst(){running=false;elapsed=0;lapN=0;cancelAnimationFrame(raf);$('dsp').textContent='00:00.00';$('laps').innerHTML='';$('main').textContent='Start'}`,
2585
+ `document.addEventListener('keydown',e=>{if(e.code==='Space'){e.preventDefault();toggle()}else if(e.key==='l')lap();else if(e.key==='r')rst()});`,
2586
+ ].join('\n');
2587
+
2588
+ return assemblePage(title, css, html, js);
2589
+ }
2590
+
2591
+ function buildCounter(title, t, opts) {
2592
+ const css = [cssBase(t), cssBtn(t), cssDsp(), cssFx(t), cssStat(t),
2593
+ `.cnt-val{font-size:6rem;font-weight:900;margin:1.5rem 0;font-variant-numeric:tabular-nums;transition:transform .1s}`,
2594
+ `.cnt-btns{display:flex;gap:16px;justify-content:center;margin:1rem 0}`,
2595
+ `.cnt-btns .btn{width:70px;height:70px;border-radius:50%;font-size:2rem;padding:0;display:flex;align-items:center;justify-content:center}`,
2596
+ `.cnt-rst{margin-top:1.5rem;background:${t.sf}!important;color:${t.mt}!important;padding:.5rem 1.2rem!important;font-size:.9rem!important}`,
2597
+ ].join('\n');
2598
+
2599
+ const html = [
2600
+ htmlStats([{label:'Total taps',id:'total',value:'0'}]),
2601
+ `<div class="cnt-val" id="val">0</div>`,
2602
+ `<div class="cnt-btns">`,
2603
+ `<button class="btn" onclick="dec()" style="background:${t.sf};color:${t.tx}">−</button>`,
2604
+ `<button class="btn" onclick="inc()">+</button>`,
2605
+ `</div>`,
2606
+ `<button class="btn cnt-rst" onclick="rst()">Reset</button>`,
2607
+ ].join('\n');
2608
+
2609
+ const js = [jsHelper(), jsStore('cnt'), jsSfx(), jsParticles(t.pr, t.ac),
2610
+ `let val=+(sGet('val')||0),total=+(sGet('total')||0);`,
2611
+ `$('val').textContent=val;$('total').textContent=total;`,
2612
+ `function upd(){$('val').textContent=val;sSet('val',val);$('val').style.animation='none';void $('val').offsetWidth;$('val').style.animation='pop .15s'}`,
2613
+ `function inc(){val++;total++;$('total').textContent=total;sSet('total',total);sfxTap();burstAt($('val'),4);upd()}`,
2614
+ `function dec(){val--;sfxHit();upd()}`,
2615
+ `function rst(){val=0;sfxFail();upd()}`,
2616
+ `document.addEventListener('keydown',e=>{if(e.key==='ArrowUp'||e.key==='+')inc();else if(e.key==='ArrowDown'||e.key==='-')dec();else if(e.key==='r')rst()});`,
2617
+ ].join('\n');
2618
+
2619
+ return assemblePage(title, css, html, js);
2620
+ }
2621
+
2622
+ function buildTodo(title, t, opts) {
2623
+ const css = [cssBase(t), cssBtn(t), cssInput(t), cssList(t), cssFx(t), cssStat(t),
2624
+ `.todo-add{display:flex;gap:8px;margin:1rem 0}`,
2625
+ `.todo-add .input{flex:1}`,
2626
+ `.todo-add .btn{padding:.75rem 1.2rem}`,
2627
+ `.list-item .chk{width:22px;height:22px;border-radius:6px;border:2px solid ${t.pr};background:none;cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;color:${t.ac};font-size:14px;transition:background .2s}`,
2628
+ `.list-item.done .chk{background:${t.ac};border-color:${t.ac};color:#fff}`,
2629
+ `.list-item .text{flex:1;word-break:break-word}`,
2630
+ `.filters{display:flex;gap:8px;justify-content:center;margin:.5rem 0}`,
2631
+ `.filters button{background:none;border:none;color:${t.mt};cursor:pointer;font-size:.85rem;padding:.3rem .6rem;border-radius:6px}`,
2632
+ `.filters button.on{background:${t.pr};color:#fff}`,
2633
+ opts.categories ? `.cat{font-size:.7rem;padding:.15rem .4rem;border-radius:4px;background:${t.pr}33;color:${t.pr};margin-left:.3rem}` : '',
2634
+ ].join('\n');
2635
+
2636
+ const html = [
2637
+ htmlStats([{label:'Done',id:'done',value:'0'},{label:'Total',id:'total',value:'0'}]),
2638
+ `<div class="todo-add">`,
2639
+ `<input class="input" id="inp" placeholder="Add a task..." autocomplete="off">`,
2640
+ `<button class="btn" onclick="add()">+</button></div>`,
2641
+ `<div class="filters">`,
2642
+ `<button class="on" id="fAll" onclick="filt('all')">All</button>`,
2643
+ `<button id="fAct" onclick="filt('active')">Active</button>`,
2644
+ `<button id="fDon" onclick="filt('done')">Done</button></div>`,
2645
+ `<div class="list" id="list"></div>`,
2646
+ ].join('\n');
2647
+
2648
+ const js = [jsHelper(), jsStore('todo'), jsSfx(), jsParticles(t.pr, t.ac),
2649
+ `let items=sGet('items')||[],filter='all';`,
2650
+ `function render(){const f=filter==='done'?items.filter(i=>i.done):filter==='active'?items.filter(i=>!i.done):items;$('list').innerHTML='';f.forEach((item,idx)=>{const realIdx=items.indexOf(item);const d=document.createElement('div');d.className='list-item'+(item.done?' done':'');d.innerHTML='<button class="chk" onclick="tog('+realIdx+')">'+(item.done?'✓':'')+'</button><span class="text">'+item.text+'</span><button class="del" onclick="del('+realIdx+')">×</button>';$('list').appendChild(d)});$('done').textContent=items.filter(i=>i.done).length;$('total').textContent=items.length;['fAll','fAct','fDon'].forEach(id=>$(id).classList.remove('on'));$({all:'fAll',active:'fAct',done:'fDon'}[filter]).classList.add('on')}`,
2651
+ `function save(){sSet('items',items)}`,
2652
+ `function add(){const v=$('inp').value.trim();if(!v)return;items.unshift({text:v,done:false,id:Date.now()});save();render();$('inp').value='';sfxTap();burstAt($('inp'),6)}`,
2653
+ `function tog(i){items[i].done=!items[i].done;save();render();if(items[i].done){sfxScore();burstAt($('list').children[0],8)}else sfxHit()}`,
2654
+ `function del(i){items.splice(i,1);save();render();sfxFail()}`,
2655
+ `function filt(f){filter=f;render()}`,
2656
+ `$('inp').addEventListener('keydown',e=>{if(e.key==='Enter')add()});`,
2657
+ `render();`,
2658
+ ].join('\n');
2659
+
2660
+ return assemblePage(title, css, html, js);
2661
+ }
2662
+
2663
+ function buildPomodoro(title, t, opts) {
2664
+ const workMin = 25, breakMin = 5;
2665
+
2666
+ const css = [cssBase(t), cssBtn(t), cssDsp(), cssFx(t), cssStat(t),
2667
+ `.pom-ring{width:min(280px,70vw);height:min(280px,70vw);border-radius:50%;border:6px solid ${t.sf};display:flex;align-items:center;justify-content:center;flex-direction:column;margin:1.5rem auto;position:relative}`,
2668
+ `.pom-time{font-size:3.5rem;font-weight:200;font-variant-numeric:tabular-nums;font-family:monospace}`,
2669
+ `.pom-label{font-size:.9rem;color:${t.mt};margin-top:.3rem;text-transform:uppercase;letter-spacing:.1em}`,
2670
+ `.pom-ring svg{position:absolute;top:-6px;left:-6px;transform:rotate(-90deg)}`,
2671
+ `.pom-btns{display:flex;gap:12px;justify-content:center;margin:1rem 0}`,
2672
+ `.pom-btns .btn{padding:.6rem 1.5rem;font-size:1rem}`,
2673
+ ].join('\n');
2674
+
2675
+ const ringSize = 280;
2676
+ const r = (ringSize - 12) / 2;
2677
+ const circ = Math.round(2 * Math.PI * r);
2678
+
2679
+ const html = [
2680
+ htmlStats([{label:'Sessions',id:'sessions',value:'0'},{label:'Total',id:'totalMin',value:'0 min'}]),
2681
+ `<div class="pom-ring">`,
2682
+ `<svg width="${ringSize}" height="${ringSize}"><circle cx="${ringSize/2}" cy="${ringSize/2}" r="${r}" fill="none" stroke="${t.pr}" stroke-width="6" stroke-dasharray="${circ}" stroke-dashoffset="0" stroke-linecap="round" id="ring"/></svg>`,
2683
+ `<div class="pom-time" id="time">${workMin}:00</div>`,
2684
+ `<div class="pom-label" id="phase">Focus</div></div>`,
2685
+ `<div class="pom-btns">`,
2686
+ `<button class="btn" id="main" onclick="toggle()">Start</button>`,
2687
+ `<button class="btn" onclick="skip()" style="background:${t.sf};color:${t.tx}">Skip</button>`,
2688
+ `<button class="btn" onclick="rst()" style="background:${t.sf};color:${t.tx}">Reset</button></div>`,
2689
+ ].join('\n');
2690
+
2691
+ const js = [jsHelper(), jsStore('pom'), jsSfx(), jsParticles(t.pr, t.ac),
2692
+ `const WORK=${workMin*60},BRK=${breakMin*60},CIRC=${circ};`,
2693
+ `let sessions=+(sGet('sessions')||0),totalSec=+(sGet('totalSec')||0),remain=WORK,phase='work',running=false,raf,lastT;`,
2694
+ `$('sessions').textContent=sessions;$('totalMin').textContent=Math.floor(totalSec/60)+' min';`,
2695
+ `function fmt(s){return String(Math.floor(s/60)).padStart(2,'0')+':'+String(s%60).padStart(2,'0')}`,
2696
+ `function upd(){const dur=phase==='work'?WORK:BRK;const pct=remain/dur;$('time').textContent=fmt(remain);$('ring').setAttribute('stroke-dashoffset',CIRC*(1-pct));$('ring').setAttribute('stroke',phase==='work'?'${t.pr}':'${t.ac}');$('phase').textContent=phase==='work'?'Focus':'Break'}`,
2697
+ `function tick(){if(!running)return;const now=Date.now();const dt=Math.floor((now-lastT)/1000);if(dt>=1){lastT=now;remain=Math.max(0,remain-dt);if(phase==='work')totalSec+=dt;upd();if(remain<=0){done()}}raf=requestAnimationFrame(tick)}`,
2698
+ `function toggle(){if(running){running=false;cancelAnimationFrame(raf);$('main').textContent='Resume';sfxHit()}else{running=true;lastT=Date.now();$('main').textContent='Pause';tick();sfxTap()}}`,
2699
+ `function done(){running=false;cancelAnimationFrame(raf);if(phase==='work'){sessions++;sSet('sessions',sessions);sSet('totalSec',totalSec);$('sessions').textContent=sessions;$('totalMin').textContent=Math.floor(totalSec/60)+' min';sfxWin();burstAt($('time'),20)}else{sfxScore();burstAt($('time'),10)}phase=phase==='work'?'break':'work';remain=phase==='work'?WORK:BRK;$('main').textContent='Start';upd()}`,
2700
+ `function skip(){running=false;cancelAnimationFrame(raf);phase=phase==='work'?'break':'work';remain=phase==='work'?WORK:BRK;$('main').textContent='Start';upd();sfxFail()}`,
2701
+ `function rst(){running=false;cancelAnimationFrame(raf);phase='work';remain=WORK;$('main').textContent='Start';upd()}`,
2702
+ `upd();`,
2703
+ ].join('\n');
2704
+
2705
+ return assemblePage(title, css, html, js);
2706
+ }
2707
+
2708
+ function buildClock(title, t, opts) {
2709
+ const css = [cssBase(t), cssDsp(), cssFx(t),
2710
+ `.clock-face{width:min(300px,75vw);height:min(300px,75vw);border-radius:50%;border:4px solid ${t.pr}44;margin:1.5rem auto;position:relative;background:${t.sf}}`,
2711
+ `.clock-face .mark{position:absolute;width:2px;height:10px;background:${t.mt};top:8px;left:50%;transform-origin:0 142px}`,
2712
+ `.clock-face .mark.hr{height:16px;width:3px;background:${t.tx}}`,
2713
+ `.hand{position:absolute;bottom:50%;left:50%;transform-origin:50% 100%;border-radius:2px}`,
2714
+ `.hand-h{width:4px;height:60px;background:${t.tx}}`,
2715
+ `.hand-m{width:3px;height:80px;background:${t.pr}}`,
2716
+ `.hand-s{width:1.5px;height:90px;background:${t.ac}}`,
2717
+ `.clock-dot{position:absolute;width:8px;height:8px;border-radius:50%;background:${t.ac};top:50%;left:50%;transform:translate(-50%,-50%);z-index:5}`,
2718
+ `.digital{font-size:2rem;font-weight:300;font-variant-numeric:tabular-nums;font-family:monospace;margin:1rem 0;letter-spacing:.1em}`,
2719
+ `.date-str{color:${t.mt};font-size:.9rem}`,
2720
+ ].join('\n');
2721
+
2722
+ const html = `<div class="clock-face" id="face"></div>
2723
+ <div class="digital" id="digital">00:00:00</div>
2724
+ <div class="date-str" id="date"></div>`;
2725
+
2726
+ const js = [jsHelper(),
2727
+ `const face=$('face');`,
2728
+ `for(let i=0;i<60;i++){const m=document.createElement('div');m.className='mark'+(i%5===0?' hr':'');m.style.transform='translateX(-50%) rotate('+i*6+'deg)';face.appendChild(m)}`,
2729
+ `['hand-h','hand-m','hand-s'].forEach(c=>{const h=document.createElement('div');h.className='hand '+c;h.id=c;face.appendChild(h)});`,
2730
+ `const dot=document.createElement('div');dot.className='clock-dot';face.appendChild(dot);`,
2731
+ `function tick(){const d=new Date();const h=d.getHours()%12,m=d.getMinutes(),s=d.getSeconds(),ms=d.getMilliseconds();const hDeg=h*30+m*.5;const mDeg=m*6+s*.1;const sDeg=s*6+ms*.006;$('hand-h').style.transform='translateX(-50%) rotate('+hDeg+'deg)';$('hand-m').style.transform='translateX(-50%) rotate('+mDeg+'deg)';$('hand-s').style.transform='translateX(-50%) rotate('+sDeg+'deg)';$('digital').textContent=String(d.getHours()).padStart(2,'0')+':'+String(m).padStart(2,'0')+':'+String(s).padStart(2,'0');$('date').textContent=d.toLocaleDateString(undefined,{weekday:'long',year:'numeric',month:'long',day:'numeric'});requestAnimationFrame(tick)}`,
2732
+ `tick();`,
2733
+ ].join('\n');
2734
+
2735
+ return assemblePage(title, css, html, js);
2736
+ }
2737
+
2738
+ function buildDrawing(title, t, opts) {
2739
+ const css = [cssBase(t), cssBtn(t), cssFx(t),
2740
+ `body{overflow:hidden}`,
2741
+ `.toolbar{display:flex;gap:6px;justify-content:center;align-items:center;flex-wrap:wrap;margin:.5rem 0}`,
2742
+ `.toolbar button{border:none;border-radius:8px;padding:.5rem .8rem;font-size:.85rem;cursor:pointer;background:${t.sf};color:${t.tx};transition:transform .1s}`,
2743
+ `.toolbar button:active{transform:scale(.93)}`,
2744
+ `.toolbar button.on{background:${t.pr};color:#fff}`,
2745
+ `.colors{display:flex;gap:4px;justify-content:center;margin:.3rem 0}`,
2746
+ `.colors div{width:28px;height:28px;border-radius:50%;cursor:pointer;border:3px solid transparent;transition:border-color .2s}`,
2747
+ `.colors div.on{border-color:${t.tx}}`,
2748
+ `.sizes{display:flex;gap:8px;align-items:center;justify-content:center;margin:.3rem 0}`,
2749
+ `.sizes div{border-radius:50%;background:${t.tx};cursor:pointer;opacity:.4;transition:opacity .2s}`,
2750
+ `.sizes div.on{opacity:1}`,
2751
+ `canvas{cursor:crosshair;border:2px solid ${t.sf};background:#fff}`,
2752
+ ].join('\n');
2753
+
2754
+ const palette = ['#111827','#ef4444','#f97316','#eab308','#22c55e','#3b82f6','#8b5cf6','#ec4899','#ffffff'];
2755
+ const sizes = [2, 5, 10, 20];
2756
+
2757
+ const html = [
2758
+ `<div class="toolbar">`,
2759
+ `<button class="on" id="tb-pen" onclick="setTool('pen')">Pen</button>`,
2760
+ `<button id="tb-eraser" onclick="setTool('eraser')">Eraser</button>`,
2761
+ `<button onclick="undo()">Undo</button>`,
2762
+ `<button onclick="clearAll()">Clear</button>`,
2763
+ `<button onclick="save()">Save</button></div>`,
2764
+ `<div class="colors" id="colors">`,
2765
+ palette.map((c,i) => `<div style="background:${c}" onclick="setColor('${c}',this)"${i===0?' class="on"':''}></div>`).join(''),
2766
+ `</div>`,
2767
+ `<div class="sizes" id="sizes">`,
2768
+ sizes.map((s,i) => `<div style="width:${s+8}px;height:${s+8}px" onclick="setSize(${s},this)"${i===1?' class="on"':''}></div>`).join(''),
2769
+ `</div>`,
2770
+ `<canvas id="cv"></canvas>`,
2771
+ ].join('\n');
2772
+
2773
+ const js = [jsHelper(), jsSfx(),
2774
+ `const cv=$('cv'),ctx=cv.getContext('2d');`,
2775
+ `let tool='pen',color='#111827',size=5,drawing=false,history=[],histIdx=-1;`,
2776
+ `function resize(){const w=Math.min(420,window.innerWidth*.92);cv.width=w;cv.height=w;ctx.fillStyle='#fff';ctx.fillRect(0,0,w,w);redraw()}window.addEventListener('resize',resize);resize();`,
2777
+ `function snapshot(){histIdx++;history=history.slice(0,histIdx);history.push(cv.toDataURL());if(history.length>50)history.shift()}`,
2778
+ `function redraw(){if(histIdx>=0){const img=new Image();img.onload=()=>{ctx.clearRect(0,0,cv.width,cv.height);ctx.drawImage(img,0,0)};img.src=history[histIdx]}}`,
2779
+ `function getPos(e){const r=cv.getBoundingClientRect();const t=e.touches?e.touches[0]:e;return{x:t.clientX-r.left,y:t.clientY-r.top}}`,
2780
+ `function startDraw(e){e.preventDefault();drawing=true;const p=getPos(e);ctx.beginPath();ctx.moveTo(p.x,p.y)}`,
2781
+ `function draw(e){if(!drawing)return;e.preventDefault();const p=getPos(e);ctx.lineTo(p.x,p.y);ctx.strokeStyle=tool==='eraser'?'#fff':color;ctx.lineWidth=tool==='eraser'?size*3:size;ctx.lineCap='round';ctx.lineJoin='round';ctx.stroke()}`,
2782
+ `function endDraw(){if(drawing){drawing=false;snapshot()}}`,
2783
+ `cv.addEventListener('mousedown',startDraw);cv.addEventListener('mousemove',draw);cv.addEventListener('mouseup',endDraw);cv.addEventListener('mouseleave',endDraw);`,
2784
+ `cv.addEventListener('touchstart',startDraw,{passive:false});cv.addEventListener('touchmove',draw,{passive:false});cv.addEventListener('touchend',endDraw);`,
2785
+ `function setTool(t){tool=t;document.querySelectorAll('.toolbar button').forEach(b=>b.classList.remove('on'));$('tb-'+t).classList.add('on');sfxTap()}`,
2786
+ `function setColor(c,el){color=c;document.querySelectorAll('.colors div').forEach(d=>d.classList.remove('on'));el.classList.add('on')}`,
2787
+ `function setSize(s,el){size=s;document.querySelectorAll('.sizes div').forEach(d=>d.classList.remove('on'));el.classList.add('on')}`,
2788
+ `function undo(){if(histIdx>0){histIdx--;redraw();sfxHit()}else if(histIdx===0){histIdx=-1;ctx.fillStyle='#fff';ctx.fillRect(0,0,cv.width,cv.height);sfxHit()}}`,
2789
+ `function clearAll(){ctx.fillStyle='#fff';ctx.fillRect(0,0,cv.width,cv.height);snapshot();sfxFail()}`,
2790
+ `function save(){const a=document.createElement('a');a.download='drawing.png';a.href=cv.toDataURL();a.click();sfxWin()}`,
2791
+ `snapshot();`,
2792
+ ].join('\n');
2793
+
2794
+ return assemblePage(title, css, html, js);
2795
+ }
2796
+
2797
+ function buildNotes(title, t, opts) {
2798
+ const css = [cssBase(t), cssBtn(t), cssInput(t), cssList(t), cssFx(t),
2799
+ `body{justify-content:flex-start;padding-top:2rem}`,
2800
+ `.note-card{background:${t.sf};border-radius:12px;padding:1rem;margin:.5rem 0;text-align:left;cursor:pointer;transition:transform .2s,box-shadow .2s;animation:fadeIn .2s ease-out}`,
2801
+ `.note-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.1)}`,
2802
+ `.note-card h3{font-size:1rem;margin-bottom:.3rem;color:${t.tx}}`,
2803
+ `.note-card p{font-size:.85rem;color:${t.mt};display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}`,
2804
+ `.note-card .time{font-size:.7rem;color:${t.mt};margin-top:.4rem}`,
2805
+ `.editor{display:none;flex-direction:column;gap:.5rem;margin:1rem 0}`,
2806
+ `.editor.on{display:flex}`,
2807
+ `.editor textarea{background:${t.sf};border:2px solid ${t.pr}44;border-radius:12px;padding:.75rem;font-size:1rem;color:${t.tx};min-height:200px;resize:vertical;outline:none;font-family:inherit;transition:border-color .2s}`,
2808
+ `.editor textarea:focus{border-color:${t.pr}}`,
2809
+ `.editor-btns{display:flex;gap:8px}`,
2810
+ `.notes-list.hide{display:none}`,
2811
+ ].join('\n');
2812
+
2813
+ const html = [
2814
+ `<div class="notes-list" id="notesList">`,
2815
+ `<div style="display:flex;gap:8px;margin:1rem 0">`,
2816
+ `<input class="input" id="search" placeholder="Search notes..." oninput="render()">`,
2817
+ `<button class="btn" onclick="newNote()" style="padding:.75rem 1rem">+</button></div>`,
2818
+ `<div id="list"></div></div>`,
2819
+ `<div class="editor" id="editor">`,
2820
+ `<input class="input" id="etitle" placeholder="Title">`,
2821
+ `<textarea id="ebody" placeholder="Write your note..."></textarea>`,
2822
+ `<div class="editor-btns">`,
2823
+ `<button class="btn" onclick="saveNote()">Save</button>`,
2824
+ `<button class="btn" onclick="cancelEdit()" style="background:${t.sf};color:${t.tx}">Cancel</button>`,
2825
+ `<button class="btn" onclick="delNote()" style="background:#ef4444;color:#fff;margin-left:auto">Delete</button></div></div>`,
2826
+ ].join('\n');
2827
+
2828
+ const js = [jsHelper(), jsStore('notes'), jsSfx(),
2829
+ `let notes=sGet('list')||[],editId=null;`,
2830
+ `function render(){const q=$('search').value.toLowerCase();const f=q?notes.filter(n=>n.title.toLowerCase().includes(q)||n.body.toLowerCase().includes(q)):notes;$('list').innerHTML='';f.forEach(n=>{const d=document.createElement('div');d.className='note-card';d.innerHTML='<h3>'+esc(n.title||'Untitled')+'</h3><p>'+esc(n.body||'')+'</p><div class="time">'+new Date(n.updated).toLocaleString()+'</div>';d.onclick=()=>openNote(n.id);$('list').appendChild(d)})}`,
2831
+ `function esc(s){return s.replace(/</g,'&lt;').replace(/>/g,'&gt;')}`,
2832
+ `function save(){sSet('list',notes)}`,
2833
+ `function newNote(){editId=null;$('etitle').value='';$('ebody').value='';$('editor').classList.add('on');$('notesList').classList.add('hide');$('etitle').focus();sfxTap()}`,
2834
+ `function openNote(id){const n=notes.find(x=>x.id===id);if(!n)return;editId=id;$('etitle').value=n.title;$('ebody').value=n.body;$('editor').classList.add('on');$('notesList').classList.add('hide')}`,
2835
+ `function saveNote(){const title=$('etitle').value.trim()||'Untitled';const body=$('ebody').value;const now=Date.now();if(editId){const n=notes.find(x=>x.id===editId);if(n){n.title=title;n.body=body;n.updated=now}}else{notes.unshift({id:now,title,body,created:now,updated:now})}save();cancelEdit();sfxScore()}`,
2836
+ `function cancelEdit(){$('editor').classList.remove('on');$('notesList').classList.remove('hide');render()}`,
2837
+ `function delNote(){if(editId){notes=notes.filter(n=>n.id!==editId);save();sfxFail()}cancelEdit()}`,
2838
+ `render();`,
2839
+ ].join('\n');
2840
+
2841
+ return assemblePage(title, css, html, js);
2340
2842
  }
2341
2843
 
2342
- // ── Game Router ──
2844
+ // ── Builder Registry ──
2343
2845
 
2344
- const GAME_BUILDERS = {
2846
+ const BUILDERS = {
2345
2847
  tapping: buildTapping, quiz: buildQuiz, memory: buildMemory,
2346
- snake: buildSnake, reaction: buildReaction, typing: buildTyping, breakout: buildBreakout,
2347
- dodge: buildDodge, color: buildColor, pong: buildPong,
2848
+ snake: buildSnake, reaction: buildReaction, typing: buildTyping,
2849
+ breakout: buildBreakout, dodge: buildDodge, color: buildColor, pong: buildPong,
2850
+ calculator: buildCalculator, timer: buildTimerApp, counter: buildCounter,
2851
+ todo: buildTodo, pomodoro: buildPomodoro, clock: buildClock,
2852
+ drawing: buildDrawing, notes: buildNotes,
2348
2853
  };
2349
2854
 
2855
+ function frontendHTML(type, title, rawInstruction) {
2856
+ const t = THEMES[type] || THEMES.tapping;
2857
+ const opts = extractBuildOpts(rawInstruction || '');
2858
+ const builder = BUILDERS[type];
2859
+ if (!builder) return buildTapping(title || type, t, opts);
2860
+ return builder(title || type, t, opts);
2861
+ }
2862
+
2350
2863
  function gameHTML(type, title, rawInstruction) {
2351
- const opts = extractGameOpts(rawInstruction || '');
2352
- const builder = GAME_BUILDERS[type] || buildTapping;
2353
- return builder(title || type, opts);
2864
+ return frontendHTML(type, title, rawInstruction);
2354
2865
  }
2355
2866
 
2356
- function gameNaideCode(title, port) {
2357
- return `server game port ${port}:
2867
+ function appNaideCode(title, port, type) {
2868
+ const label = GAME_TYPES[type] ? 'game' : 'app';
2869
+ return `server ${label} port ${port}:
2358
2870
  cors "*"
2359
2871
 
2360
2872
  get "/" (req, res):
2361
2873
  res.sendFile("index.html", {root: "."})
2362
2874
 
2363
2875
  get "/api/health" (req, res):
2364
- ret {status: "ok", game: "${title || 'game'}"}
2876
+ ret {status: "ok", ${label}: "${title || type || 'app'}"}
2365
2877
  `;
2366
2878
  }
2367
2879
 
2880
+
2368
2881
  function compose(intents, params) {
2369
- // Game intent: generate minimal server code (game HTML is generated separately in project mode)
2882
+ // Game/App intent: generate minimal server code (HTML is generated separately in project mode)
2370
2883
  if (params.gameType && intents.has('game') && !intents.has('crud')) {
2371
- return gameNaideCode(params.gameTitle || params.gameType, params.port);
2884
+ return appNaideCode(params.gameTitle || params.gameType, params.port, params.gameType);
2885
+ }
2886
+
2887
+ if (params.appType && intents.has('app') && !intents.has('crud')) {
2888
+ return appNaideCode(params.appTitle || params.appType, params.port, params.appType);
2372
2889
  }
2373
2890
 
2374
2891
  const sections = [];
@@ -2700,7 +3217,8 @@ function heal(code, err) {
2700
3217
  const KEYWORD_CHEATSHEET = {
2701
3218
  'Intent': 'server, api, rest, bot, cli, page, test, database, ai, crud, auth, websocket, mail, graphql, game',
2702
3219
  'Composite': 'todo, blog, chat, shop, fullstack, board, crm, inventory, booking, sns',
2703
- 'Game': 'tapping, quiz, memory, snake, typing, reaction, breakout',
3220
+ 'Game': 'tapping, quiz, memory, snake, typing, reaction, breakout, dodge, color, pong',
3221
+ 'App': 'calculator, timer, counter, todo, pomodoro, clock, drawing, notes',
2704
3222
  'Platform': 'discord, slack, telegram, line',
2705
3223
  'DB': 'sqlite, postgres',
2706
3224
  'Japanese': 'サーバー, 認証, ログイン, 会員, CRUD, 管理, データベース, ボット, テスト, ページ',
@@ -2723,12 +3241,14 @@ export function generateProject(instruction, options = {}) {
2723
3241
  const files = [];
2724
3242
  const intents = new Set(result.intents);
2725
3243
 
2726
- // Game project: generate standalone HTML game
2727
- if (intents.has('game') && result.analysis && result.analysis.gameType) {
2728
- const gt = result.analysis.gameType;
2729
- const title = result.analysis.gameTitle || gt;
2730
- const projectName = safeName(gt) + '-game';
2731
- const html = gameHTML(gt, title, instruction);
3244
+ // Game/App project: generate standalone HTML frontend
3245
+ const frontendType = (intents.has('game') && result.analysis && result.analysis.gameType) ? result.analysis.gameType
3246
+ : (intents.has('app') && result.analysis && result.analysis.appType) ? result.analysis.appType : null;
3247
+ if (frontendType) {
3248
+ const isGame = !!result.analysis.gameType;
3249
+ const title = result.analysis.gameTitle || result.analysis.appTitle || frontendType;
3250
+ const projectName = safeName(frontendType) + (isGame ? '-game' : '-app');
3251
+ const html = frontendHTML(frontendType, title, instruction);
2732
3252
 
2733
3253
  files.push({ path: 'index.html', content: html });
2734
3254
  files.push({ path: 'app.naide', content: result.code });
@@ -2738,12 +3258,17 @@ export function generateProject(instruction, options = {}) {
2738
3258
  dependencies: { express: '^4.21.0', naider: '^1.21.0' },
2739
3259
  }, null, 2) + '\n' });
2740
3260
  files.push({ path: '.gitignore', content: 'node_modules/\n' });
2741
- files.push({ path: 'README.md', content: `# ${title}\n\nGenerated by NAIDE Agent Coder (~~)\n\n## Play\n\nOpen \`index.html\` directly in a browser, or:\n\n\`\`\`bash\nnpm install && npm run dev\n\`\`\`\n\nThen visit http://localhost:${result.port}\n` });
3261
+ const verb = isGame ? 'Play' : 'Use';
3262
+ files.push({ path: 'README.md', content: `# ${title}\n\nGenerated by NAIDE Agent Coder (~~)\n\n## ${verb}\n\nOpen \`index.html\` directly in a browser, or:\n\n\`\`\`bash\nnpm install && npm run dev\n\`\`\`\n\nThen visit http://localhost:${result.port}\n` });
3263
+
3264
+ const plan = [`${isGame ? 'Game' : 'App'}: ${frontendType}`, 'Standalone HTML + CSS + JS'];
3265
+ if (isGame) plan.push('High scores saved in localStorage');
3266
+ plan.push('Touch & keyboard support', `Server on port ${result.port}`);
2742
3267
 
2743
3268
  return {
2744
3269
  ...result,
2745
3270
  files,
2746
- analysis: { ...result.analysis, plan: [`Game: ${gt}`, `Standalone HTML + CSS + JS`, `High scores saved in localStorage`, `Touch & keyboard support`, `Server on port ${result.port}`] },
3271
+ analysis: { ...result.analysis, plan },
2747
3272
  };
2748
3273
  }
2749
3274
 
@@ -3231,10 +3756,12 @@ export function expandDirectives(source) {
3231
3756
  function buildAnalysis(instruction, intents, params) {
3232
3757
  const plan = [];
3233
3758
 
3234
- if (params.gameType) {
3235
- plan.push(`Game: ${params.gameType}`);
3759
+ if (params.gameType || params.appType) {
3760
+ const type = params.gameType || params.appType;
3761
+ const isGame = !!params.gameType;
3762
+ plan.push(`${isGame ? 'Game' : 'App'}: ${type}`);
3236
3763
  plan.push('Standalone HTML + CSS + JS');
3237
- plan.push('High scores saved in localStorage');
3764
+ if (isGame) plan.push('High scores saved in localStorage');
3238
3765
  plan.push('Touch & keyboard support');
3239
3766
  return {
3240
3767
  instruction,
@@ -3244,6 +3771,8 @@ function buildAnalysis(instruction, intents, params) {
3244
3771
  plan,
3245
3772
  gameType: params.gameType,
3246
3773
  gameTitle: params.gameTitle,
3774
+ appType: params.appType,
3775
+ appTitle: params.appTitle,
3247
3776
  };
3248
3777
  }
3249
3778