create-rakta-app 0.1.5 → 0.1.7

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/dist/index.js CHANGED
@@ -886,16 +886,34 @@ var T$1 = class T extends V {
886
886
  }
887
887
  }
888
888
  };
889
+
890
+ class r extends V {
891
+ get cursor() {
892
+ return this.value ? 0 : 1;
893
+ }
894
+ get _value() {
895
+ return this.cursor === 0;
896
+ }
897
+ constructor(t2) {
898
+ super(t2, false), this.value = !!t2.initialValue, this.on("userInput", () => {
899
+ this.value = this._value;
900
+ }), this.on("confirm", (i) => {
901
+ this.output.write(import_sisteransi.cursor.move(0, -1)), this.value = i, this.state = "submit", this.close();
902
+ }), this.on("cursor", () => {
903
+ this.value = !this.value;
904
+ });
905
+ }
906
+ }
889
907
  var _ = {
890
908
  Y: { type: "year", len: 4 },
891
909
  M: { type: "month", len: 2 },
892
910
  D: { type: "day", len: 2 }
893
911
  };
894
- function M(r) {
895
- return [...r].map((t2) => _[t2]);
912
+ function M(r2) {
913
+ return [...r2].map((t2) => _[t2]);
896
914
  }
897
- function P(r) {
898
- const i = new Intl.DateTimeFormat(r, {
915
+ function P(r2) {
916
+ const i = new Intl.DateTimeFormat(r2, {
899
917
  year: "numeric",
900
918
  month: "2-digit",
901
919
  day: "2-digit"
@@ -905,32 +923,32 @@ function P(r) {
905
923
  e.type === "literal" ? n = e.value.trim() || e.value : (e.type === "year" || e.type === "month" || e.type === "day") && s.push({ type: e.type, len: e.type === "year" ? 4 : 2 });
906
924
  return { segments: s, separator: n };
907
925
  }
908
- function p(r) {
909
- return Number.parseInt((r || "0").replace(/_/g, "0"), 10) || 0;
926
+ function p(r2) {
927
+ return Number.parseInt((r2 || "0").replace(/_/g, "0"), 10) || 0;
910
928
  }
911
- function f(r) {
929
+ function f(r2) {
912
930
  return {
913
- year: p(r.year),
914
- month: p(r.month),
915
- day: p(r.day)
931
+ year: p(r2.year),
932
+ month: p(r2.month),
933
+ day: p(r2.day)
916
934
  };
917
935
  }
918
- function c(r, t2) {
919
- return new Date(r || 2001, t2 || 1, 0).getDate();
936
+ function c(r2, t2) {
937
+ return new Date(r2 || 2001, t2 || 1, 0).getDate();
920
938
  }
921
- function b(r) {
922
- const { year: t2, month: i, day: s } = f(r);
939
+ function b(r2) {
940
+ const { year: t2, month: i, day: s } = f(r2);
923
941
  if (!t2 || t2 < 0 || t2 > 9999 || !i || i < 1 || i > 12 || !s || s < 1)
924
942
  return;
925
943
  const n = new Date(Date.UTC(t2, i - 1, s));
926
944
  if (!(n.getUTCFullYear() !== t2 || n.getUTCMonth() !== i - 1 || n.getUTCDate() !== s))
927
945
  return { year: t2, month: i, day: s };
928
946
  }
929
- function C(r) {
930
- const t2 = b(r);
947
+ function C(r2) {
948
+ const t2 = b(r2);
931
949
  return t2 ? new Date(Date.UTC(t2.year, t2.month - 1, t2.day)) : undefined;
932
950
  }
933
- function T2(r, t2, i, s) {
951
+ function T2(r2, t2, i, s) {
934
952
  const n = i ? {
935
953
  year: i.getUTCFullYear(),
936
954
  month: i.getUTCMonth() + 1,
@@ -940,7 +958,7 @@ function T2(r, t2, i, s) {
940
958
  month: s.getUTCMonth() + 1,
941
959
  day: s.getUTCDate()
942
960
  } : null;
943
- return r === "year" ? { min: n?.year ?? 1, max: e?.year ?? 9999 } : r === "month" ? {
961
+ return r2 === "year" ? { min: n?.year ?? 1, max: e?.year ?? 9999 } : r2 === "month" ? {
944
962
  min: n && t2.year === n.year ? n.month : 1,
945
963
  max: e && t2.year === e.year ? e.month : 12
946
964
  } : {
@@ -1113,26 +1131,26 @@ var u$1 = class u extends V {
1113
1131
  cursor = 0;
1114
1132
  #t;
1115
1133
  getGroupItems(t2) {
1116
- return this.options.filter((r) => r.group === t2);
1134
+ return this.options.filter((r2) => r2.group === t2);
1117
1135
  }
1118
1136
  isGroupSelected(t2) {
1119
- const r = this.getGroupItems(t2), e = this.value;
1120
- return e === undefined ? false : r.every((s) => e.includes(s.value));
1137
+ const r2 = this.getGroupItems(t2), e = this.value;
1138
+ return e === undefined ? false : r2.every((s) => e.includes(s.value));
1121
1139
  }
1122
1140
  toggleValue() {
1123
1141
  const t2 = this.options[this.cursor];
1124
1142
  if (this.value === undefined && (this.value = []), t2.group === true) {
1125
- const r = t2.value, e = this.getGroupItems(r);
1126
- this.isGroupSelected(r) ? this.value = this.value.filter((s) => e.findIndex((i) => i.value === s) === -1) : this.value = [...this.value, ...e.map((s) => s.value)], this.value = Array.from(new Set(this.value));
1143
+ const r2 = t2.value, e = this.getGroupItems(r2);
1144
+ this.isGroupSelected(r2) ? this.value = this.value.filter((s) => e.findIndex((i) => i.value === s) === -1) : this.value = [...this.value, ...e.map((s) => s.value)], this.value = Array.from(new Set(this.value));
1127
1145
  } else {
1128
- const r = this.value.includes(t2.value);
1129
- this.value = r ? this.value.filter((e) => e !== t2.value) : [...this.value, t2.value];
1146
+ const r2 = this.value.includes(t2.value);
1147
+ this.value = r2 ? this.value.filter((e) => e !== t2.value) : [...this.value, t2.value];
1130
1148
  }
1131
1149
  }
1132
1150
  constructor(t2) {
1133
1151
  super(t2, false);
1134
- const { options: r } = t2;
1135
- this.#t = t2.selectableGroups !== false, this.options = Object.entries(r).flatMap(([e, s]) => [
1152
+ const { options: r2 } = t2;
1153
+ this.#t = t2.selectableGroups !== false, this.options = Object.entries(r2).flatMap(([e, s]) => [
1136
1154
  { value: e, group: true, label: e },
1137
1155
  ...s.map((i) => ({ ...i, group: e }))
1138
1156
  ]), this.value = [...t2.initialValues ?? []], this.cursor = Math.max(this.options.findIndex(({ value: e }) => e === t2.cursorAt), this.#t ? 0 : 1), this.on("cursor", (e) => {
@@ -1170,10 +1188,10 @@ class h extends V {
1170
1188
  const t2 = this.userInput;
1171
1189
  if (this.cursor >= t2.length)
1172
1190
  return `${t2}\u2588`;
1173
- const s = t2.slice(0, this.cursor), r = t2[this.cursor], i = t2.slice(this.cursor + 1);
1174
- return r === `
1191
+ const s = t2.slice(0, this.cursor), r2 = t2[this.cursor], i = t2.slice(this.cursor + 1);
1192
+ return r2 === `
1175
1193
  ` ? `${s}\u2588
1176
- ${i}` : `${s}${styleText("inverse", r)}${i}`;
1194
+ ${i}` : `${s}${styleText("inverse", r2)}${i}`;
1177
1195
  }
1178
1196
  get cursor() {
1179
1197
  return this._cursor;
@@ -1206,8 +1224,8 @@ ${i}` : `${s}${styleText("inverse", r)}${i}`;
1206
1224
  if (this.#s)
1207
1225
  return this.focused === "submit" ? true : (this.#r(`
1208
1226
  `), this._cursor++, false);
1209
- const r = this.#t;
1210
- return this.#t = true, r && this.cursor === this.userInput.length ? (this.userInput[this.cursor - 1] === `
1227
+ const r2 = this.#t;
1228
+ return this.#t = true, r2 && this.cursor === this.userInput.length ? (this.userInput[this.cursor - 1] === `
1211
1229
  ` && (this._setUserInput(this.userInput.slice(0, this.cursor - 1) + this.userInput.slice(this.cursor)), this._cursor--), true) : (this.#r(`
1212
1230
  `), this._cursor++, false);
1213
1231
  }
@@ -1216,12 +1234,12 @@ ${i}` : `${s}${styleText("inverse", r)}${i}`;
1216
1234
  super({
1217
1235
  ...t2,
1218
1236
  initialUserInput: s
1219
- }, false), s !== undefined && (this._cursor = s.length), this.#s = t2.showSubmit ?? false, this.on("key", (r, i) => {
1237
+ }, false), s !== undefined && (this._cursor = s.length), this.#s = t2.showSubmit ?? false, this.on("key", (r2, i) => {
1220
1238
  if (i?.name && o$1.has(i.name)) {
1221
1239
  this.#t = false, this.#i(i.name);
1222
1240
  return;
1223
1241
  }
1224
- if (r === "\t" && this.#s) {
1242
+ if (r2 === "\t" && this.#s) {
1225
1243
  this.focused = this.focused === "editor" ? "submit" : "editor";
1226
1244
  return;
1227
1245
  }
@@ -1234,10 +1252,10 @@ ${i}` : `${s}${styleText("inverse", r)}${i}`;
1234
1252
  this._setUserInput(this.userInput.slice(0, this.cursor) + this.userInput.slice(this.cursor + 1));
1235
1253
  return;
1236
1254
  }
1237
- r && (this.#s && this.focused === "submit" && (this.focused = "editor"), this.#r(r ?? ""), this._cursor++);
1255
+ r2 && (this.#s && this.focused === "submit" && (this.focused = "editor"), this.#r(r2 ?? ""), this._cursor++);
1238
1256
  }
1239
- }), this.on("userInput", (r) => {
1240
- this._setValue(r);
1257
+ }), this.on("userInput", (r2) => {
1258
+ this._setValue(r2);
1241
1259
  }), this.on("finalize", () => {
1242
1260
  this.value || (this.value = t2.defaultValue), this.value === undefined && (this.value = "");
1243
1261
  });
@@ -1277,8 +1295,8 @@ class n extends V {
1277
1295
  const t2 = this.userInput;
1278
1296
  if (this.cursor >= t2.length)
1279
1297
  return `${this.userInput}\u2588`;
1280
- const e = t2.slice(0, this.cursor), [s, ...r] = t2.slice(this.cursor);
1281
- return `${e}${styleText("inverse", s)}${r.join("")}`;
1298
+ const e = t2.slice(0, this.cursor), [s, ...r2] = t2.slice(this.cursor);
1299
+ return `${e}${styleText("inverse", s)}${r2.join("")}`;
1282
1300
  }
1283
1301
  get cursor() {
1284
1302
  return this._cursor;
@@ -1412,6 +1430,41 @@ var limitOptions = ({
1412
1430
  b2.push(n2);
1413
1431
  return u3 && b2.push(C2), b2;
1414
1432
  };
1433
+ var confirm = (i) => {
1434
+ const a2 = i.active ?? "Yes", s = i.inactive ?? "No";
1435
+ return new r({
1436
+ active: a2,
1437
+ inactive: s,
1438
+ signal: i.signal,
1439
+ input: i.input,
1440
+ output: i.output,
1441
+ initialValue: i.initialValue ?? true,
1442
+ render() {
1443
+ const e = i.withGuide ?? settings.withGuide, u3 = `${symbol(this.state)} `, l2 = e ? `${styleText2("gray", S_BAR)} ` : "", f2 = wrapTextWithPrefix(i.output, i.message, l2, u3), o2 = `${e ? `${styleText2("gray", S_BAR)}
1444
+ ` : ""}${f2}
1445
+ `, c2 = this.value ? a2 : s;
1446
+ switch (this.state) {
1447
+ case "submit": {
1448
+ const r2 = e ? `${styleText2("gray", S_BAR)} ` : "";
1449
+ return `${o2}${r2}${styleText2("dim", c2)}`;
1450
+ }
1451
+ case "cancel": {
1452
+ const r2 = e ? `${styleText2("gray", S_BAR)} ` : "";
1453
+ return `${o2}${r2}${styleText2(["strikethrough", "dim"], c2)}${e ? `
1454
+ ${styleText2("gray", S_BAR)}` : ""}`;
1455
+ }
1456
+ default: {
1457
+ const r2 = e ? `${styleText2("cyan", S_BAR)} ` : "", g2 = e ? styleText2("cyan", S_BAR_END) : "";
1458
+ return `${o2}${r2}${this.value ? `${styleText2("green", S_RADIO_ACTIVE)} ${a2}` : `${styleText2("dim", S_RADIO_INACTIVE)} ${styleText2("dim", a2)}`}${i.vertical ? e ? `
1459
+ ${styleText2("cyan", S_BAR)} ` : `
1460
+ ` : ` ${styleText2("dim", "/")} `}${this.value ? `${styleText2("dim", S_RADIO_INACTIVE)} ${styleText2("dim", s)}` : `${styleText2("green", S_RADIO_ACTIVE)} ${s}`}
1461
+ ${g2}
1462
+ `;
1463
+ }
1464
+ }
1465
+ }
1466
+ }).prompt();
1467
+ };
1415
1468
  var MULTISELECT_INSTRUCTIONS = [
1416
1469
  `${styleText2("dim", "\u2191/\u2193")} to navigate`,
1417
1470
  `${styleText2("dim", "Space:")} select`,
@@ -1704,6 +1757,598 @@ var PROJECT_MODE_DISPLAY = {
1704
1757
  "frontend-only": "Frontend only (no backend, no database)"
1705
1758
  };
1706
1759
 
1760
+ // src/welcomeComponents.ts
1761
+ var COMPONENTS_MODAL_CODE = `import type React from 'react';
1762
+ import { useState } from 'react';
1763
+ import { LuX as X, LuCopy as Copy, LuCheck as Check, LuCode as Code, LuCpu as Cpu } from 'react-icons/lu';
1764
+ import { motion } from 'motion/react';
1765
+ // import { playJumpSound, playScoreSound } from '../utils/audio'; // Removed to prevent error if not exists
1766
+
1767
+ interface ComponentsModalProps {
1768
+ isOpen: boolean;
1769
+ onClose: () => void;
1770
+ }
1771
+
1772
+ type ComponentState = Record<string, string | number | boolean | undefined>;
1773
+ type SetComponentState = (state: ComponentState) => void;
1774
+
1775
+ interface ComponentItem {
1776
+ id: string;
1777
+ name: string;
1778
+ description: string;
1779
+ preview: (state: ComponentState, setState: SetComponentState) => React.ReactNode;
1780
+ code: string;
1781
+ }
1782
+
1783
+ const BRUTALIST_COMPONENTS: ComponentItem[] = [
1784
+ {
1785
+ id: 'button',
1786
+ name: 'Brutalist Button',
1787
+ description: 'A solid, high-contrast action trigger with raw borders and active scaling.',
1788
+ code: \`<button class="bg-rose-600 hover:bg-white text-white hover:text-black px-6 py-3 font-mono text-xs font-bold uppercase transition-all duration-150 border border-transparent hover:border-black active:scale-95 shadow-[4px_4px_0px_0px_rgba(255,255,255,0.25)]">
1789
+ TRIGGER PIPELINE
1790
+ </button>\`,
1791
+ preview: (state, setState) => (
1792
+ <button
1793
+ type="button"
1794
+ onClick={() => {
1795
+ const clickCount = typeof state.clickCount === 'number' ? state.clickCount : 0;
1796
+ setState({ ...state, clickCount: clickCount + 1 });
1797
+ }}
1798
+ className="bg-brand-pink hover:bg-white text-white hover:text-black px-6 py-3 font-mono text-xs font-bold uppercase transition-all duration-150 border border-transparent hover:border-black active:scale-95 shadow-[4px_4px_0px_0px_rgba(255,255,255,0.25)] cursor-pointer"
1799
+ >
1800
+ TRIGGER PIPELINE ({typeof state.clickCount === 'number' ? state.clickCount : 0})
1801
+ </button>
1802
+ )
1803
+ },
1804
+ {
1805
+ id: 'badge',
1806
+ name: 'Performance Badge',
1807
+ description: 'A high-visibility mono badge with a pulsing live status indicator.',
1808
+ code: \`<div class="flex items-center gap-2 border border-emerald-500/30 bg-emerald-950/20 px-3 py-1 font-mono text-xs text-emerald-400">
1809
+ <span class="w-1.5 h-1.5 bg-emerald-500 rounded-full animate-ping"></span>
1810
+ OPERATIONAL :: 100%
1811
+ </div>\`,
1812
+ preview: (_state, _setState) => (
1813
+ <div className="flex items-center gap-2 border border-emerald-500/30 bg-emerald-950/20 px-3 py-1.5 font-mono text-xs text-emerald-400">
1814
+ <span className="relative flex h-2 w-2">
1815
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
1816
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
1817
+ </span>
1818
+ OPERATIONAL :: 100%
1819
+ </div>
1820
+ )
1821
+ },
1822
+ {
1823
+ id: 'switch',
1824
+ name: 'Monochrome Toggle',
1825
+ description: 'A structural, rectangular switch that flips states with tactile audio feedback.',
1826
+ code: \`<button class="flex items-center border border-white p-1 w-16 h-8 bg-zinc-900">
1827
+ <div class="w-6 h-6 bg-white transition-all transform translate-x-8"></div>
1828
+ </button>\`,
1829
+ preview: (state, setState) => {
1830
+ const isChecked = state.isChecked ?? false;
1831
+ const handleToggle = () => {
1832
+ setState({ ...state, isChecked: !isChecked });
1833
+ };
1834
+ return (
1835
+ <div className="flex items-center gap-4">
1836
+ <button
1837
+ type="button"
1838
+ onClick={handleToggle}
1839
+ className={\`flex items-center border-2 border-white p-0.5 w-16 h-8 transition-colors cursor-pointer \${isChecked ? 'bg-brand-pink' : 'bg-zinc-900'}\`}
1840
+ >
1841
+ <div className={\`w-6 h-6 bg-white transition-transform \${isChecked ? 'translate-x-8' : 'translate-x-0'}\`}></div>
1842
+ </button>
1843
+ <span className="font-mono text-xs uppercase text-gray-400">
1844
+ STATE: <span className="text-white font-bold">{isChecked ? 'ENABLED' : 'DISABLED'}</span>
1845
+ </span>
1846
+ </div>
1847
+ );
1848
+ }
1849
+ },
1850
+ {
1851
+ id: 'slider',
1852
+ name: 'Frequency Slider',
1853
+ description: 'A pixelated level adjuster with raw visual increments.',
1854
+ code: \`<div class="w-full max-w-xs font-mono">
1855
+ <input type="range" class="accent-rose-600 bg-zinc-800 h-2 w-full border border-zinc-700 appearance-none cursor-pointer" />
1856
+ </div>\`,
1857
+ preview: (state, setState) => {
1858
+ const value = typeof state.sliderVal === 'number' ? state.sliderVal : 60;
1859
+ return (
1860
+ <div className="w-full max-w-sm font-mono text-xs">
1861
+ <div className="flex justify-between mb-2">
1862
+ <span className="text-gray-400 uppercase">SYS_SPEED</span>
1863
+ <span className="text-brand-green font-bold">{value} MHz</span>
1864
+ </div>
1865
+ <input
1866
+ type="range"
1867
+ min="10"
1868
+ max="200"
1869
+ value={value}
1870
+ onChange={(e) => setState({ ...state, sliderVal: parseInt(e.target.value, 10) })}
1871
+ className="accent-brand-pink bg-zinc-900 h-2 w-full border border-zinc-700 appearance-none cursor-pointer"
1872
+ />
1873
+ <div className="flex justify-between mt-1 text-[9px] text-gray-600">
1874
+ <span>MIN</span>
1875
+ <span>MID_GRID</span>
1876
+ <span>MAX</span>
1877
+ </div>
1878
+ </div>
1879
+ );
1880
+ }
1881
+ },
1882
+ {
1883
+ id: 'input',
1884
+ name: 'Brutalist Input',
1885
+ description: 'Minimal text box with bright validation focus indicators.',
1886
+ code: \`<div class="relative font-mono">
1887
+ <input type="text" placeholder="ENTER NODE NAME..." class="bg-black border border-zinc-700 focus:border-rose-600 text-white px-4 py-2 w-full outline-none" />
1888
+ </div>\`,
1889
+ preview: (state, setState) => {
1890
+ const text = typeof state.inputValue === 'string' ? state.inputValue : '';
1891
+ return (
1892
+ <div className="w-full max-w-xs font-mono text-xs">
1893
+ <label className="block text-gray-500 uppercase mb-1.5">
1894
+ Node Configuration
1895
+ <input
1896
+ type="text"
1897
+ placeholder="ENTER NODE NAME..."
1898
+ value={text}
1899
+ onChange={(e) => setState({ ...state, inputValue: e.target.value })}
1900
+ className="mt-1.5 bg-black border border-zinc-700 focus:border-brand-pink text-white px-4 py-2 w-full outline-none"
1901
+ />
1902
+ </label>
1903
+ {text && (
1904
+ <p className="text-[10px] text-brand-green mt-1">\u2713 Validating: {text.toUpperCase()}.local</p>
1905
+ )}
1906
+ </div>
1907
+ );
1908
+ }
1909
+ }
1910
+ ];
1911
+
1912
+ export default function ComponentsModal({ isOpen, onClose }: ComponentsModalProps) {
1913
+ const [activeCompId, setActiveCompId] = useState('button');
1914
+ const [componentStates, setComponentStates] = useState<Record<string, ComponentState>>({});
1915
+ const [copiedId, setCopiedId] = useState<string | null>(null);
1916
+
1917
+ if (!isOpen) return null;
1918
+
1919
+ const fallbackComponent = BRUTALIST_COMPONENTS[0];
1920
+ if (!fallbackComponent) return null;
1921
+
1922
+ const activeComp = BRUTALIST_COMPONENTS.find(c => c.id === activeCompId) ?? fallbackComponent;
1923
+ const activeState = componentStates[activeCompId] || {};
1924
+
1925
+ const handleSetState = (newState: ComponentState) => {
1926
+ setComponentStates({
1927
+ ...componentStates,
1928
+ [activeCompId]: newState
1929
+ });
1930
+ };
1931
+
1932
+ const handleCopyCode = (code: string) => {
1933
+ navigator.clipboard.writeText(code);
1934
+ setCopiedId(activeCompId);
1935
+ setTimeout(() => setCopiedId(null), 2000);
1936
+ };
1937
+
1938
+ return (
1939
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/85 backdrop-blur-sm" id="components-modal-container">
1940
+ <motion.div
1941
+ initial={{ opacity: 0, scale: 0.95, y: 15 }}
1942
+ animate={{ opacity: 1, scale: 1, y: 0 }}
1943
+ exit={{ opacity: 0, scale: 0.95, y: 15 }}
1944
+ transition={{ duration: 0.2 }}
1945
+ className="w-full max-w-4xl h-[75vh] bg-black border-2 border-white flex flex-col relative"
1946
+ >
1947
+ <div className="flex items-center justify-between border-b border-white p-5">
1948
+ <div className="flex items-center gap-3">
1949
+ <Cpu className="w-5 h-5 text-rose-500" />
1950
+ <h2 className="text-xl font-bold font-mono tracking-tight uppercase">
1951
+ Rakta <span className="text-rose-500">Component Library</span>
1952
+ </h2>
1953
+ </div>
1954
+ <button
1955
+ type="button"
1956
+ onClick={onClose}
1957
+ className="p-2 border border-white hover:bg-rose-500 hover:text-white transition-colors cursor-pointer"
1958
+ >
1959
+ <X className="w-5 h-5" />
1960
+ </button>
1961
+ </div>
1962
+
1963
+ <div className="flex-1 flex flex-col md:flex-row overflow-hidden">
1964
+ <div className="w-full md:w-56 border-r border-white overflow-y-auto bg-[#080808] divide-y divide-white/20">
1965
+ {BRUTALIST_COMPONENTS.map((comp) => {
1966
+ const isSelected = comp.id === activeCompId;
1967
+ return (
1968
+ <button
1969
+ type="button"
1970
+ key={comp.id}
1971
+ onClick={() => setActiveCompId(comp.id)}
1972
+ className={\`w-full text-left p-4 transition-colors font-mono text-xs uppercase cursor-pointer \${
1973
+ isSelected
1974
+ ? 'bg-rose-500 text-white font-bold'
1975
+ : 'text-gray-400 hover:bg-white/5 hover:text-white'
1976
+ }\`}
1977
+ >
1978
+ {comp.name}
1979
+ </button>
1980
+ );
1981
+ })}
1982
+ </div>
1983
+
1984
+ <div className="flex-1 flex flex-col overflow-y-auto bg-black p-6 md:p-8">
1985
+ <div className="mb-6">
1986
+ <h3 className="text-xl font-bold text-white uppercase font-mono tracking-tight">{activeComp.name}</h3>
1987
+ <p className="text-sm text-gray-400 mt-1">{activeComp.description}</p>
1988
+ </div>
1989
+
1990
+ <div className="border border-white/30 bg-[#050505] p-12 flex items-center justify-center min-h-[160px] relative">
1991
+ <span className="absolute top-2 left-2 text-[8px] font-mono text-gray-600 uppercase tracking-widest">LIVE PLAYGROUND</span>
1992
+ {activeComp.preview(activeState, handleSetState)}
1993
+ </div>
1994
+
1995
+ <div className="mt-8 flex-1 flex flex-col min-h-[150px]">
1996
+ <div className="flex items-center justify-between bg-zinc-900 border-t border-x border-white/20 px-4 py-2">
1997
+ <span className="text-[10px] font-mono text-gray-500 uppercase tracking-widest flex items-center gap-1.5">
1998
+ <Code className="w-3.5 h-3.5" /> HTML/Tailwind Markup
1999
+ </span>
2000
+ <button
2001
+ type="button"
2002
+ onClick={() => handleCopyCode(activeComp.code)}
2003
+ className="flex items-center gap-1.5 text-[10px] font-mono text-rose-500 hover:text-white transition-colors cursor-pointer"
2004
+ >
2005
+ {copiedId === activeComp.id ? (
2006
+ <>
2007
+ <Check className="w-3.5 h-3.5 text-green-500" /> COPIED!
2008
+ </>
2009
+ ) : (
2010
+ <>
2011
+ <Copy className="w-3.5 h-3.5" /> COPY TO CLIPBOARD
2012
+ </>
2013
+ )}
2014
+ </button>
2015
+ </div>
2016
+ <pre className="flex-1 bg-zinc-950 border border-white/20 p-4 font-mono text-xs text-green-400 overflow-x-auto whitespace-pre leading-5 select-all">
2017
+ <code>{activeComp.code}</code>
2018
+ </pre>
2019
+ </div>
2020
+ </div>
2021
+ </div>
2022
+ </motion.div>
2023
+ </div>
2024
+ );
2025
+ }
2026
+ `;
2027
+ var CORAL_OBSTACLE_CODE = `interface CoralObstacleProps {
2028
+ position: 'TOP' | 'BOTTOM';
2029
+ height: number;
2030
+ width?: number;
2031
+ paletteIndex?: number;
2032
+ variant?: number;
2033
+ scaleX?: number;
2034
+ }
2035
+
2036
+ export default function CoralObstacle({
2037
+ position,
2038
+ height,
2039
+ width = 64,
2040
+ paletteIndex = 0,
2041
+ variant = 0,
2042
+ scaleX = 1
2043
+ }: CoralObstacleProps) {
2044
+ const defaultPalette = { primary: '#f43f5e', secondary: '#fda4af', shadow: '#9f1239', polyps: '#ffe4e6', name: 'rose' };
2045
+ const palettes = [
2046
+ defaultPalette,
2047
+ { primary: '#06b6d4', secondary: '#67e8f9', shadow: '#155e75', polyps: '#ecfeff', name: 'cyan' },
2048
+ { primary: '#a855f7', secondary: '#d8b4fe', shadow: '#581c87', polyps: '#faf5ff', name: 'amethyst' },
2049
+ { primary: '#f59e0b', secondary: '#fde047', shadow: '#78350f', polyps: '#fefce8', name: 'amber' },
2050
+ { primary: '#10b981', secondary: '#6ee7b7', shadow: '#065f46', polyps: '#e6fffa', name: 'emerald' },
2051
+ ];
2052
+ const palette = palettes[paletteIndex % palettes.length] ?? defaultPalette;
2053
+ const coralId = \`coralGrad-\${palette.name}-\${variant}\`;
2054
+
2055
+ return (
2056
+ <div
2057
+ className="absolute flex items-center justify-center pointer-events-none"
2058
+ style={{ height: \`\${height}px\`, width: \`\${width}px\`, transform: \`scaleX(\${scaleX})\` }}
2059
+ >
2060
+ <svg
2061
+ aria-hidden="true"
2062
+ focusable="false"
2063
+ viewBox="0 0 80 120"
2064
+ className="w-full h-full drop-shadow-[0_6px_14px_rgba(0,0,0,0.7)]"
2065
+ preserveAspectRatio="none"
2066
+ >
2067
+ <defs>
2068
+ <linearGradient id={coralId} x1="0%" y1="0%" x2="100%" y2="100%">
2069
+ <stop offset="0%" stopColor={palette.secondary} />
2070
+ <stop offset="50%" stopColor={palette.primary} />
2071
+ <stop offset="100%" stopColor={palette.shadow} />
2072
+ </linearGradient>
2073
+ </defs>
2074
+
2075
+ <g transform={position === 'TOP' ? 'scale(1, -1) translate(0, -120)' : undefined}>
2076
+ <path d="M 40 120 C 35 90, 20 80, 24 55 C 28 30, 15 20, 22 10 C 29 0, 37 15, 35 30 C 33 45, 42 55, 40 75 C 38 95, 42 110, 40 120 Z" fill={\`url(#\${coralId})\`} />
2077
+ <path d="M 40 120 C 45 95, 55 85, 52 65 C 49 45, 62 35, 58 20 C 54 5, 46 12, 48 25 C 50 38, 42 50, 44 70 C 46 90, 42 105, 40 120 Z" fill={\`url(#\${coralId})\`} />
2078
+ </g>
2079
+ </svg>
2080
+ </div>
2081
+ );
2082
+ }
2083
+ `;
2084
+ var DEPLOY_MODAL_CODE = `import { useState, useEffect, useRef } from 'react';
2085
+ import { LuX as X, LuPlay as Play, LuRotateCcw as RotateCcw, LuCloud as Cloud } from 'react-icons/lu';
2086
+ import { motion } from 'motion/react';
2087
+
2088
+ interface DeployModalProps {
2089
+ isOpen: boolean;
2090
+ onClose: () => void;
2091
+ }
2092
+
2093
+ const DEPLOY_LOGS = [
2094
+ { text: ':: RAKTA CORE PIPELINE INITIALIZED ::', type: 'system' },
2095
+ { text: '[SYS] Resolving package.json manifest dependencies...', type: 'info' },
2096
+ { text: '[VITE] Starting asset compilation & typescript check...', type: 'info' },
2097
+ { text: '[VITE] \u2713 Production dist/ folder generated successfully [384ms]', type: 'success' },
2098
+ { text: '[ESBUILD] Compiling full-stack backend entrypoint: server.ts', type: 'info' },
2099
+ { text: '[ESBUILD] \u2713 Bundled server.ts into dist/server.cjs [120ms]', type: 'success' },
2100
+ { text: '[CLOUD RUN] Pushing build payload to regional edge registry...', type: 'info' },
2101
+ { text: '[CLOUD RUN] Ingress configured: routing all traffic exclusively to Port 3000', type: 'info' },
2102
+ { text: ':: SYSTEM DEPLOYMENT COMPLETED ON EDGE NODES ::', type: 'success' },
2103
+ ];
2104
+
2105
+ export default function DeployModal({ isOpen, onClose }: DeployModalProps) {
2106
+ const [status, setStatus] = useState<'idle' | 'building' | 'success' | 'error'>('idle');
2107
+ const [logs, setLogs] = useState<Array<{ text: string; type: string }>>([]);
2108
+ const [progress, setProgress] = useState(0);
2109
+ const terminalEndRef = useRef<HTMLDivElement>(null);
2110
+
2111
+ useEffect(() => {
2112
+ if (terminalEndRef.current) {
2113
+ terminalEndRef.current.scrollIntoView({ behavior: 'smooth' });
2114
+ }
2115
+ });
2116
+
2117
+ if (!isOpen) return null;
2118
+
2119
+ const startDeployment = () => {
2120
+ setStatus('building');
2121
+ setLogs([]);
2122
+ setProgress(0);
2123
+
2124
+ let currentLogIndex = 0;
2125
+
2126
+ const runStep = () => {
2127
+ if (currentLogIndex < DEPLOY_LOGS.length) {
2128
+ const nextLog = DEPLOY_LOGS[currentLogIndex];
2129
+ if (nextLog) {
2130
+ setLogs(prev => [...prev, nextLog]);
2131
+ }
2132
+ setProgress(Math.round(((currentLogIndex + 1) / DEPLOY_LOGS.length) * 100));
2133
+ currentLogIndex++;
2134
+ setTimeout(runStep, Math.random() * 250 + 150);
2135
+ } else {
2136
+ setStatus('success');
2137
+ }
2138
+ };
2139
+ setTimeout(runStep, 400);
2140
+ };
2141
+
2142
+ const resetDeploy = () => {
2143
+ setStatus('idle');
2144
+ setLogs([]);
2145
+ setProgress(0);
2146
+ };
2147
+
2148
+ return (
2149
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/85 backdrop-blur-sm" id="deploy-modal-container">
2150
+ <motion.div
2151
+ initial={{ opacity: 0, scale: 0.95, y: 15 }}
2152
+ animate={{ opacity: 1, scale: 1, y: 0 }}
2153
+ exit={{ opacity: 0, scale: 0.95, y: 15 }}
2154
+ transition={{ duration: 0.2 }}
2155
+ className="w-full max-w-3xl h-[70vh] bg-black border-2 border-white flex flex-col relative"
2156
+ >
2157
+ <div className="flex items-center justify-between border-b border-white/30 p-5">
2158
+ <div className="flex items-center gap-3">
2159
+ <Cloud className="w-5 h-5 text-rose-500" />
2160
+ <h2 className="text-xl font-bold font-mono tracking-tight uppercase">
2161
+ Rakta <span className="text-rose-500">Edge Deployment</span>
2162
+ </h2>
2163
+ </div>
2164
+ <button
2165
+ type="button"
2166
+ onClick={onClose}
2167
+ className="p-2 border border-white/30 hover:bg-rose-500 hover:text-white transition-colors cursor-pointer"
2168
+ >
2169
+ <X className="w-5 h-5" />
2170
+ </button>
2171
+ </div>
2172
+
2173
+ <div className="bg-zinc-900 p-6 border-b border-white/20 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
2174
+ <div>
2175
+ <span className="text-[10px] font-mono text-gray-500 uppercase tracking-widest block">PIPELINE STATUS</span>
2176
+ <div className="flex items-center gap-2 mt-1">
2177
+ {status === 'idle' && <span className="font-mono text-sm text-yellow-500 font-bold uppercase">IDLE - READY TO LAUNCH</span>}
2178
+ {status === 'building' && <span className="font-mono text-sm text-rose-500 font-bold uppercase">COMPILES IN PROGRESS ({progress}%)</span>}
2179
+ {status === 'success' && <span className="font-mono text-sm text-green-500 font-bold uppercase">LIVE - PRODUCTION DEPLOYED</span>}
2180
+ </div>
2181
+ </div>
2182
+ <div className="flex items-center gap-3">
2183
+ {status === 'idle' && (
2184
+ <button type="button" onClick={startDeployment} className="bg-rose-500 hover:bg-white text-white hover:text-black px-6 py-2.5 font-mono text-xs font-bold uppercase transition-all duration-150 border border-transparent active:scale-95 flex items-center gap-2 cursor-pointer">
2185
+ <Play className="w-4 h-4 fill-current" /> INITIATE LAUNCH
2186
+ </button>
2187
+ )}
2188
+ {status === 'success' && (
2189
+ <button type="button" onClick={resetDeploy} className="border border-white hover:bg-white hover:text-black px-4 py-2.5 font-mono text-xs uppercase transition-all duration-150 flex items-center gap-1.5 cursor-pointer">
2190
+ <RotateCcw className="w-3.5 h-3.5" /> RE-BUILD PIPELINE
2191
+ </button>
2192
+ )}
2193
+ </div>
2194
+ </div>
2195
+
2196
+ <div className="flex-1 bg-black p-5 font-mono text-xs overflow-y-auto flex flex-col gap-2 select-text selection:bg-rose-500/30">
2197
+ {logs.map((log) => {
2198
+ let textColor = 'text-gray-400';
2199
+ if (log.type === 'system') textColor = 'text-rose-500 font-bold border-b border-rose-500/20 pb-1 mb-2';
2200
+ if (log.type === 'success') textColor = 'text-green-500 font-bold';
2201
+ if (log.type === 'info') textColor = 'text-gray-300';
2202
+ return <div key={\`\${log.type}-\${log.text}\`} className={\`leading-relaxed whitespace-pre-wrap \${textColor}\`}>{log.text}</div>;
2203
+ })}
2204
+ {status === 'idle' && <div className="text-gray-600 italic mt-4 text-center">Click 'INITIATE LAUNCH' to begin.</div>}
2205
+ <div ref={terminalEndRef} />
2206
+ </div>
2207
+ </motion.div>
2208
+ </div>
2209
+ );
2210
+ }
2211
+ `;
2212
+ var DOCS_MODAL_CODE = `import { LuX as X, LuBook as Book } from 'react-icons/lu';
2213
+ import { motion } from 'motion/react';
2214
+
2215
+ interface DocsModalProps {
2216
+ isOpen: boolean;
2217
+ onClose: () => void;
2218
+ }
2219
+
2220
+ export default function DocsModal({ isOpen, onClose }: DocsModalProps) {
2221
+ if (!isOpen) return null;
2222
+ return (
2223
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm">
2224
+ <motion.div initial={{ opacity: 0, y: 15 }} animate={{ opacity: 1, y: 0 }} className="w-full max-w-5xl h-[80vh] bg-black border-2 border-white flex flex-col relative">
2225
+ <div className="flex items-center justify-between border-b border-white/30 p-5">
2226
+ <div className="flex items-center gap-3">
2227
+ <Book className="w-5 h-5 text-rose-500" />
2228
+ <h2 className="text-xl font-bold font-mono tracking-tight uppercase">Rakta.js <span className="text-rose-500">System Manual</span></h2>
2229
+ </div>
2230
+ <button type="button" onClick={onClose} className="p-2 border border-white/30 hover:bg-rose-500 hover:text-white transition-colors cursor-pointer"><X className="w-5 h-5" /></button>
2231
+ </div>
2232
+ <div className="p-10 text-white font-mono text-sm leading-relaxed max-w-3xl">
2233
+ <h3 className="text-3xl font-extrabold text-rose-500 mb-6">Welcome to Rakta.js</h3>
2234
+ <p className="mb-4">You have successfully generated your project.</p>
2235
+ <p className="mb-4">Use the CLI generator to expand your codebase, or start editing files in the app/ directory.</p>
2236
+ <ul className="list-disc pl-5 mt-4 space-y-2 text-gray-400">
2237
+ <li>Instant Engine Boot: Sub-1ms load time.</li>
2238
+ <li>Brutalist Aesthetics natively supported.</li>
2239
+ <li>Hot Reload available globally via WebSockets.</li>
2240
+ </ul>
2241
+ </div>
2242
+ </motion.div>
2243
+ </div>
2244
+ );
2245
+ }
2246
+ `;
2247
+ var SHRIMP_CHARACTER_CODE = `interface ShrimpCharacterProps {
2248
+ status: 'IDLE' | 'SWIMMING' | 'JUMPING' | 'DEAD';
2249
+ playerY: number;
2250
+ rotation?: number;
2251
+ }
2252
+
2253
+ export default function ShrimpCharacter({ status, playerY, rotation }: ShrimpCharacterProps) {
2254
+ let bodyClass = '';
2255
+ const isDead = status === 'DEAD';
2256
+
2257
+ if (isDead) bodyClass = 'animate-bounce';
2258
+ else if (status === 'SWIMMING') bodyClass = 'animate-pulse';
2259
+ else bodyClass = '';
2260
+
2261
+ const finalRotation = rotation !== undefined ? rotation : (isDead ? 180 : 0);
2262
+ const shrimpColor = isDead ? 'fill-orange-400' : 'fill-rose-500';
2263
+
2264
+ return (
2265
+ <div
2266
+ className="relative w-16 h-16 flex items-center justify-center select-none pointer-events-none transition-transform duration-75"
2267
+ style={{ transform: \`translateY(\${-playerY}px) rotate(\${finalRotation}deg)\` }}
2268
+ >
2269
+ <svg aria-hidden="true" focusable="false" viewBox="0 0 100 100" className={\`w-full h-full drop-shadow-[0_4px_12px_rgba(244,63,94,0.3)] \${bodyClass}\`}>
2270
+ <path d="M 30 46 C 25 32, 45 22, 58 28 C 65 31, 62 48, 55 54 C 44 58, 35 55, 30 46 Z" className={shrimpColor} />
2271
+ <path d="M 55 30 C 65 24, 76 34, 74 46 C 72 54, 62 55, 55 48 Z" className={shrimpColor} />
2272
+ <circle cx="66" cy="38" r="4.5" fill={isDead ? '#18181b' : '#000'} />
2273
+ </svg>
2274
+ <style>{\`
2275
+ @keyframes bubble-up {
2276
+ 0% { transform: translateY(0) scale(1); opacity: 0.8; }
2277
+ 100% { transform: translateY(-20px) scale(1.5); opacity: 0; }
2278
+ }
2279
+ \`}</style>
2280
+ </div>
2281
+ );
2282
+ }
2283
+ `;
2284
+ var WELCOME_PAGE_CODE = `import { useState } from "react";
2285
+ import { LuBook as Book, LuCloud as Cloud, LuCpu as Cpu } from "react-icons/lu";
2286
+ import ComponentsModal from "./components/ComponentsModal";
2287
+ import DeployModal from "./components/DeployModal";
2288
+ import DocsModal from "./components/DocsModal";
2289
+ import ShrimpCharacter from "./components/ShrimpCharacter";
2290
+
2291
+ export default function WelcomePage() {
2292
+ const [modalOpen, setModalOpen] = useState<'components' | 'deploy' | 'docs' | null>(null);
2293
+
2294
+ return (
2295
+ <main className="min-h-screen bg-[#050505] flex flex-col items-center justify-center text-white font-mono p-4">
2296
+ <div className="absolute top-10 pointer-events-none opacity-20">
2297
+ <ShrimpCharacter status="IDLE" playerY={0} />
2298
+ </div>
2299
+
2300
+ <div className="text-center z-10 max-w-2xl">
2301
+ <h1 className="text-5xl md:text-7xl font-extrabold uppercase tracking-tighter mb-4">
2302
+ Welcome to <span className="text-rose-500">Rakta.js</span>
2303
+ </h1>
2304
+ <p className="text-gray-400 text-sm md:text-base mb-10 leading-relaxed">
2305
+ Small in size. Fierce in speed. Alive in every route.
2306
+ <br/>
2307
+ Your neo-brutalist web application is ready for action.
2308
+ </p>
2309
+
2310
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
2311
+ <button
2312
+ type="button"
2313
+ onClick={() => setModalOpen('components')}
2314
+ className="flex flex-col items-center justify-center gap-3 p-6 border-2 border-zinc-800 bg-zinc-900/50 hover:border-rose-500 hover:bg-rose-500/10 transition-colors cursor-pointer group"
2315
+ >
2316
+ <Cpu className="w-8 h-8 text-gray-400 group-hover:text-rose-500" />
2317
+ <span className="font-bold text-sm uppercase">Components</span>
2318
+ </button>
2319
+
2320
+ <button
2321
+ type="button"
2322
+ onClick={() => setModalOpen('docs')}
2323
+ className="flex flex-col items-center justify-center gap-3 p-6 border-2 border-zinc-800 bg-zinc-900/50 hover:border-blue-500 hover:bg-blue-500/10 transition-colors cursor-pointer group"
2324
+ >
2325
+ <Book className="w-8 h-8 text-gray-400 group-hover:text-blue-500" />
2326
+ <span className="font-bold text-sm uppercase">Documentation</span>
2327
+ </button>
2328
+
2329
+ <button
2330
+ type="button"
2331
+ onClick={() => setModalOpen('deploy')}
2332
+ className="flex flex-col items-center justify-center gap-3 p-6 border-2 border-zinc-800 bg-zinc-900/50 hover:border-green-500 hover:bg-green-500/10 transition-colors cursor-pointer group"
2333
+ >
2334
+ <Cloud className="w-8 h-8 text-gray-400 group-hover:text-green-500" />
2335
+ <span className="font-bold text-sm uppercase">Deployment</span>
2336
+ </button>
2337
+ </div>
2338
+ </div>
2339
+
2340
+ <div className="absolute bottom-6 text-[10px] text-gray-600 uppercase tracking-widest text-center">
2341
+ Running on http://localhost:3000 \u2022 Hot Reload Active
2342
+ </div>
2343
+
2344
+ {modalOpen === 'components' && <ComponentsModal isOpen={true} onClose={() => setModalOpen(null)} />}
2345
+ {modalOpen === 'deploy' && <DeployModal isOpen={true} onClose={() => setModalOpen(null)} />}
2346
+ {modalOpen === 'docs' && <DocsModal isOpen={true} onClose={() => setModalOpen(null)} />}
2347
+ </main>
2348
+ );
2349
+ }
2350
+ `;
2351
+
1707
2352
  // src/generator.ts
1708
2353
  var DEFAULT_METADATA_TITLE = "Rakta.js | Small in size. Fierce in speed. Alive in every route";
1709
2354
  var FAVICON_BYTES = readFileSync(new URL("../assets/favicon.ico", import.meta.url));
@@ -1803,7 +2448,7 @@ dist/
1803
2448
  function getFrontendOnlyFiles(projectConfig) {
1804
2449
  const { projectName, cssFramework } = projectConfig;
1805
2450
  const styleFileName = cssFramework === "sass" ? "globals.scss" : "globals.css";
1806
- return [
2451
+ const files = [
1807
2452
  {
1808
2453
  path: "package.json",
1809
2454
  content: JSON.stringify({
@@ -1819,9 +2464,11 @@ function getFrontendOnlyFiles(projectConfig) {
1819
2464
  typecheck: "tsc --noEmit"
1820
2465
  },
1821
2466
  dependencies: {
1822
- raktajs: "^0.1.5",
2467
+ raktajs: "^0.1.7",
1823
2468
  react: "^19.2.7",
1824
2469
  "react-dom": "^19.2.7",
2470
+ "react-icons": "^5.2.1",
2471
+ motion: "^12.0.0",
1825
2472
  ...getCssDependencies(cssFramework)
1826
2473
  },
1827
2474
  devDependencies: {
@@ -1893,12 +2540,24 @@ export default defineRaktaConfig({
1893
2540
  content: generateFrontendOnlyNotFound()
1894
2541
  },
1895
2542
  {
1896
- path: "app/components/raktaShrimpMascot.tsx",
1897
- content: generateShrimpMascotComponent()
2543
+ path: "app/components/ComponentsModal.tsx",
2544
+ content: COMPONENTS_MODAL_CODE
2545
+ },
2546
+ {
2547
+ path: "app/components/CoralObstacle.tsx",
2548
+ content: CORAL_OBSTACLE_CODE
2549
+ },
2550
+ {
2551
+ path: "app/components/DeployModal.tsx",
2552
+ content: DEPLOY_MODAL_CODE
2553
+ },
2554
+ {
2555
+ path: "app/components/DocsModal.tsx",
2556
+ content: DOCS_MODAL_CODE
1898
2557
  },
1899
2558
  {
1900
- path: "app/components/shrimpRunGame.tsx",
1901
- content: generateShrimpRunGameComponent()
2559
+ path: "app/components/ShrimpCharacter.tsx",
2560
+ content: SHRIMP_CHARACTER_CODE
1902
2561
  },
1903
2562
  {
1904
2563
  path: `styles/${styleFileName}`,
@@ -1913,11 +2572,12 @@ export default defineRaktaConfig({
1913
2572
  content: FAVICON_BYTES
1914
2573
  }
1915
2574
  ];
2575
+ return processFilesForLanguage(files, projectConfig.useTypeScript);
1916
2576
  }
1917
2577
  function getFullstackFrontendFiles(projectConfig) {
1918
2578
  const { projectName, cssFramework } = projectConfig;
1919
2579
  const styleFileName = cssFramework === "sass" ? "globals.scss" : "globals.css";
1920
- return [
2580
+ const files = [
1921
2581
  {
1922
2582
  path: "frontend/package.json",
1923
2583
  content: JSON.stringify({
@@ -1934,9 +2594,11 @@ function getFullstackFrontendFiles(projectConfig) {
1934
2594
  typecheck: "tsc --noEmit"
1935
2595
  },
1936
2596
  dependencies: {
1937
- raktajs: "^0.1.5",
2597
+ raktajs: "^0.1.7",
1938
2598
  react: "^19.2.7",
1939
2599
  "react-dom": "^19.2.7",
2600
+ "react-icons": "^5.2.1",
2601
+ motion: "^12.0.0",
1940
2602
  ...getCssDependencies(cssFramework)
1941
2603
  },
1942
2604
  devDependencies: {
@@ -2241,14 +2903,47 @@ export type UserSchema = typeof userSchema;
2241
2903
  content: FAVICON_BYTES
2242
2904
  },
2243
2905
  {
2244
- path: "frontend/components/ui/.gitkeep",
2245
- content: ""
2906
+ path: "frontend/components/ComponentsModal.tsx",
2907
+ content: COMPONENTS_MODAL_CODE
2246
2908
  },
2247
2909
  {
2248
- path: "frontend/components/layout/.gitkeep",
2249
- content: ""
2910
+ path: "frontend/components/CoralObstacle.tsx",
2911
+ content: CORAL_OBSTACLE_CODE
2912
+ },
2913
+ {
2914
+ path: "frontend/components/DeployModal.tsx",
2915
+ content: DEPLOY_MODAL_CODE
2916
+ },
2917
+ {
2918
+ path: "frontend/components/DocsModal.tsx",
2919
+ content: DOCS_MODAL_CODE
2920
+ },
2921
+ {
2922
+ path: "frontend/components/ShrimpCharacter.tsx",
2923
+ content: SHRIMP_CHARACTER_CODE
2250
2924
  }
2251
2925
  ];
2926
+ return processFilesForLanguage(files, projectConfig.useTypeScript);
2927
+ }
2928
+ function processFilesForLanguage(files, useTypeScript) {
2929
+ if (useTypeScript)
2930
+ return files;
2931
+ return files.map((file) => {
2932
+ if (typeof file.content === "string") {
2933
+ let path = file.path;
2934
+ if (path.endsWith(".tsx"))
2935
+ path = path.replace(".tsx", ".jsx");
2936
+ else if (path.endsWith(".ts"))
2937
+ path = path.replace(".ts", ".js");
2938
+ let code = file.content;
2939
+ code = code.replace(/^interface\s+\w+\s*\{[\s\S]*?\n\}\n/gm, "");
2940
+ code = code.replace(/^type\s+\w+\s*=[\s\S]*?;\n/gm, "");
2941
+ code = code.replace(/:\s*[A-Z]\w*(Props|State)\b/g, "");
2942
+ code = code.replace(/:\s*(Request|Response|Promise<[^>]+>|string|number|boolean|any|unknown)/g, "");
2943
+ return { ...file, path, content: code };
2944
+ }
2945
+ return file;
2946
+ });
2252
2947
  }
2253
2948
  function getBackendFiles(projectConfig) {
2254
2949
  return [
@@ -3121,8 +3816,11 @@ declare global {
3121
3816
  const useRef: typeof import("react").useRef;
3122
3817
  const useState: typeof import("react").useState;
3123
3818
 
3124
- const ShrimpRunGame: typeof import("./app/components/shrimpRunGame").default;
3125
- const RaktaShrimpMascot: typeof import("./app/components/raktaShrimpMascot").default;
3819
+ const ComponentsModal: typeof import("./app/components/ComponentsModal").default;
3820
+ const CoralObstacle: typeof import("./app/components/CoralObstacle").default;
3821
+ const DeployModal: typeof import("./app/components/DeployModal").default;
3822
+ const DocsModal: typeof import("./app/components/DocsModal").default;
3823
+ const ShrimpCharacter: typeof import("./app/components/ShrimpCharacter").default;
3126
3824
  }
3127
3825
 
3128
3826
  declare module "react/jsx-runtime" {
@@ -3165,54 +3863,8 @@ export default function RootLayout({ children }: RootLayoutProps) {
3165
3863
  }
3166
3864
  `;
3167
3865
  }
3168
- function generateFrontendOnlyPage(projectName) {
3169
- return `import ShrimpRunGame from "./components/shrimpRunGame";
3170
-
3171
- export default function HomePage() {
3172
- return (
3173
- <main className="mx-auto flex min-h-screen w-full max-w-4xl flex-col gap-6 px-4 py-16 sm:px-6 lg:px-8">
3174
- <section className="rounded-3xl border border-white/10 bg-[#0e111a] p-8 shadow-2xl shadow-red-950/20">
3175
- <p className="mb-2 text-xs font-bold uppercase tracking-[0.3em] text-red-600">
3176
- THE RED ROUTER FRAMEWORK
3177
- </p>
3178
- <h1 className="mb-3 text-4xl font-extrabold leading-tight text-white md:text-6xl">
3179
- Welcome to ${projectName}
3180
- </h1>
3181
- <p className="max-w-2xl text-base leading-7 text-slate-400">
3182
- Built with Rakta.js \u2014 Small in size. Fierce in speed. Alive in every route.
3183
- </p>
3184
-
3185
- <div className="mt-6 flex flex-wrap gap-3">
3186
- <click
3187
- to="/offline"
3188
- className="rounded-xl border border-red-500/40 px-4 py-2 text-sm font-semibold text-red-400 transition hover:border-red-400 hover:text-red-300"
3189
- >
3190
- Open offline page
3191
- </click>
3192
- <click
3193
- to="/dashboard"
3194
- className="rounded-xl bg-red-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-red-700"
3195
- >
3196
- View dashboard
3197
- </click>
3198
- </div>
3199
- </section>
3200
-
3201
- <section className="rounded-3xl border border-white/10 bg-[#0e111a] p-8 shadow-2xl shadow-red-950/20">
3202
- <p className="mb-2 text-xs font-bold uppercase tracking-[0.3em] text-red-600">
3203
- SHRIMPRUN
3204
- </p>
3205
- <h2 className="mb-1 text-2xl font-bold text-white">Play ShrimpRun</h2>
3206
- <p className="mb-6 text-sm leading-6 text-slate-400">
3207
- Press Space or click the game area to jump. Avoid the red obstacles!
3208
- </p>
3209
-
3210
- <ShrimpRunGame />
3211
- </section>
3212
- </main>
3213
- );
3214
- }
3215
- `;
3866
+ function generateFrontendOnlyPage(_projectName) {
3867
+ return WELCOME_PAGE_CODE;
3216
3868
  }
3217
3869
  function generateFrontendOnlyLoading() {
3218
3870
  return `export default function Loading() {
@@ -3282,585 +3934,8 @@ function generateFrontendOnlyNotFound() {
3282
3934
  }
3283
3935
  `;
3284
3936
  }
3285
- function generateShrimpMascotComponent() {
3286
- return `interface RaktaShrimpMascotProps {
3287
- readonly isJumping: boolean;
3288
- readonly isDead: boolean;
3289
- readonly style?: import("react").CSSProperties;
3290
- }
3291
-
3292
- /**
3293
- * RaktaShrimpMascot \u2014 The animated shrimp hero of ShrimpRun.
3294
- * Drawn entirely with inline SVG. No external assets required.
3295
- */
3296
- export default function RaktaShrimpMascot({
3297
- isJumping,
3298
- isDead,
3299
- style,
3300
- }: RaktaShrimpMascotProps) {
3301
- const bodyColor = isDead ? "#6b7280" : "#dc2626";
3302
- const eyeColor = isDead ? "#374151" : "#fff";
3303
- const legAnimation =
3304
- isJumping || isDead ? "none" : "shrimpLegs 0.3s steps(2) infinite";
3305
-
3306
- return (
3307
- <svg
3308
- viewBox="0 0 48 48"
3309
- width="48"
3310
- height="48"
3311
- style={{ display: "block", ...style }}
3312
- aria-label={
3313
- isDead ? "dead shrimp" : isJumping ? "shrimp jumping" : "running shrimp"
3314
- }
3315
- role="img"
3316
- >
3317
- <style>{\`
3318
- @keyframes shrimpLegs {
3319
- 0% { transform: translateY(0); }
3320
- 50% { transform: translateY(2px); }
3321
- }
3322
- \`}</style>
3323
-
3324
- {/* Body */}
3325
- <path
3326
- d="M8 30 Q10 14 24 12 Q38 10 40 22 Q42 32 32 36 Q20 40 8 30Z"
3327
- fill={bodyColor}
3328
- />
3329
-
3330
- {/* Shell segments */}
3331
- <path
3332
- d="M12 28 Q16 20 24 18 Q30 17 34 22"
3333
- stroke="#b91c1c"
3334
- strokeWidth="1.5"
3335
- fill="none"
3336
- opacity={isDead ? 0.3 : 0.6}
3337
- />
3338
- <path
3339
- d="M14 32 Q19 24 28 22 Q33 21 36 26"
3340
- stroke="#b91c1c"
3341
- strokeWidth="1.5"
3342
- fill="none"
3343
- opacity={isDead ? 0.3 : 0.6}
3344
- />
3345
-
3346
- {/* Eye */}
3347
- <circle cx="34" cy="18" r="4" fill="#1e293b" />
3348
- <circle cx="35" cy="17" r="2" fill={eyeColor} />
3349
-
3350
- {isDead && (
3351
- <>
3352
- <line
3353
- x1="32"
3354
- y1="16"
3355
- x2="36"
3356
- y2="20"
3357
- stroke="#374151"
3358
- strokeWidth="1.5"
3359
- />
3360
- <line
3361
- x1="36"
3362
- y1="16"
3363
- x2="32"
3364
- y2="20"
3365
- stroke="#374151"
3366
- strokeWidth="1.5"
3367
- />
3368
- </>
3369
- )}
3370
-
3371
- {/* Antennae */}
3372
- <line
3373
- x1="34"
3374
- y1="14"
3375
- x2="40"
3376
- y2="6"
3377
- stroke={bodyColor}
3378
- strokeWidth="1.5"
3379
- strokeLinecap="round"
3380
- />
3381
- <line
3382
- x1="32"
3383
- y1="13"
3384
- x2="36"
3385
- y2="4"
3386
- stroke={bodyColor}
3387
- strokeWidth="1.5"
3388
- strokeLinecap="round"
3389
- />
3390
-
3391
- {/* Tail */}
3392
- <path
3393
- d="M10 30 Q4 26 6 20"
3394
- stroke={bodyColor}
3395
- strokeWidth="2"
3396
- fill="none"
3397
- strokeLinecap="round"
3398
- />
3399
- <path
3400
- d="M10 30 Q2 30 4 36"
3401
- stroke={bodyColor}
3402
- strokeWidth="2"
3403
- fill="none"
3404
- strokeLinecap="round"
3405
- />
3406
- <path
3407
- d="M10 30 Q6 34 8 40"
3408
- stroke={bodyColor}
3409
- strokeWidth="2"
3410
- fill="none"
3411
- strokeLinecap="round"
3412
- />
3413
-
3414
- {/* Legs */}
3415
- <g
3416
- style={{
3417
- animation: legAnimation,
3418
- transformOrigin: "24px 36px",
3419
- }}
3420
- >
3421
- <line
3422
- x1="18"
3423
- y1="36"
3424
- x2="14"
3425
- y2="44"
3426
- stroke={bodyColor}
3427
- strokeWidth="1.5"
3428
- strokeLinecap="round"
3429
- />
3430
- <line
3431
- x1="22"
3432
- y1="38"
3433
- x2="18"
3434
- y2="46"
3435
- stroke={bodyColor}
3436
- strokeWidth="1.5"
3437
- strokeLinecap="round"
3438
- />
3439
- <line
3440
- x1="26"
3441
- y1="38"
3442
- x2="24"
3443
- y2="46"
3444
- stroke={bodyColor}
3445
- strokeWidth="1.5"
3446
- strokeLinecap="round"
3447
- />
3448
- <line
3449
- x1="30"
3450
- y1="37"
3451
- x2="28"
3452
- y2="45"
3453
- stroke={bodyColor}
3454
- strokeWidth="1.5"
3455
- strokeLinecap="round"
3456
- />
3457
- </g>
3458
- </svg>
3459
- );
3460
- }
3461
- `;
3462
- }
3463
- function generateShrimpRunGameComponent() {
3464
- return `import { useCallback, useEffect, useRef, useState } from "react";
3465
- import RaktaShrimpMascot from "./raktaShrimpMascot";
3466
- // \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3467
-
3468
- type GameStatus = "idle" | "running" | "dead";
3469
-
3470
- interface ObstacleState {
3471
- readonly id: number;
3472
- readonly xPosition: number;
3473
- readonly width: number;
3474
- readonly height: number;
3475
- }
3476
-
3477
- interface ShrimpState {
3478
- readonly yPosition: number;
3479
- readonly velocityY: number;
3480
- readonly isJumping: boolean;
3481
- }
3482
-
3483
- // \u2500\u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3484
-
3485
- const CANVAS_WIDTH = 640;
3486
- const CANVAS_HEIGHT = 160;
3487
- const GROUND_STRIP_HEIGHT = 4;
3488
- const SHRIMP_START_X = 60;
3489
- const SHRIMP_WIDTH = 48;
3490
- const SHRIMP_HEIGHT = 48;
3491
- const GRAVITY = 1.4;
3492
- const JUMP_VELOCITY = -18;
3493
- const INITIAL_OBSTACLE_SPEED = 5;
3494
- const SPEED_INCREMENT_PER_SCORE = 0.003;
3495
- const OBSTACLE_SPAWN_INTERVAL_MS = 1600;
3496
- const SCORE_TICK_MS = 80;
3497
- const COLLISION_MARGIN = 8;
3498
- const MAX_CONCURRENT_OBSTACLES = 3;
3499
- const FRAME_SKIP_THRESHOLD_MS = 100;
3500
-
3501
- // \u2500\u2500\u2500 Physics helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3502
-
3503
- function getObstacleSpeed(currentScore: number): number {
3504
- return INITIAL_OBSTACLE_SPEED + currentScore * SPEED_INCREMENT_PER_SCORE;
3505
- }
3506
-
3507
- function checkCollision(
3508
- shrimpYPosition: number,
3509
- obstacle: ObstacleState,
3510
- ): boolean {
3511
- const shrimpLeft = SHRIMP_START_X + COLLISION_MARGIN;
3512
- const shrimpRight = SHRIMP_START_X + SHRIMP_WIDTH - COLLISION_MARGIN;
3513
- const shrimpTop =
3514
- CANVAS_HEIGHT -
3515
- GROUND_STRIP_HEIGHT -
3516
- shrimpYPosition -
3517
- SHRIMP_HEIGHT +
3518
- COLLISION_MARGIN;
3519
- const shrimpBottom = CANVAS_HEIGHT - GROUND_STRIP_HEIGHT - shrimpYPosition;
3520
-
3521
- const obstacleLeft = obstacle.xPosition + COLLISION_MARGIN;
3522
- const obstacleRight = obstacle.xPosition + obstacle.width - COLLISION_MARGIN;
3523
- const obstacleTop = CANVAS_HEIGHT - GROUND_STRIP_HEIGHT - obstacle.height;
3524
- const obstacleBottom = CANVAS_HEIGHT - GROUND_STRIP_HEIGHT;
3525
-
3526
- return (
3527
- shrimpLeft < obstacleRight &&
3528
- shrimpRight > obstacleLeft &&
3529
- shrimpTop < obstacleBottom &&
3530
- shrimpBottom > obstacleTop
3531
- );
3532
- }
3533
-
3534
- // \u2500\u2500\u2500 Component \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3535
-
3536
- /**
3537
- * ShrimpRun \u2014 Default Rakta.js interactive starter game.
3538
- *
3539
- * Like the Chrome offline Dino game, but the dinosaur is an animated shrimp.
3540
- * Press Space or click the game canvas to jump. Avoid the red obstacles!
3541
- *
3542
- * Features:
3543
- * - React state only \u2014 no external game library
3544
- * - requestAnimationFrame game loop
3545
- * - Physics: gravity + jump velocity
3546
- * - Score that increases over time
3547
- * - Speed ramps up as score grows
3548
- * - Collision detection with margin
3549
- * - High score tracked in component state
3550
- * - Keyboard (Space) and click/tap support
3551
- * - Accessible button game canvas
3552
- * - SVG shrimp mascot \u2014 no external assets
3553
- */
3554
- export default function ShrimpRunGame() {
3555
- const [gameStatus, setGameStatus] = useState<GameStatus>("idle");
3556
- const [score, setScore] = useState(0);
3557
- const [highScore, setHighScore] = useState(0);
3558
- const [obstacles, setObstacles] = useState<ObstacleState[]>([]);
3559
- const [shrimp, setShrimp] = useState<ShrimpState>({
3560
- yPosition: 0,
3561
- velocityY: 0,
3562
- isJumping: false,
3563
- });
3564
-
3565
- const gameStatusRef = useRef<GameStatus>("idle");
3566
- const scoreRef = useRef(0);
3567
- const shrimpRef = useRef<ShrimpState>({
3568
- yPosition: 0,
3569
- velocityY: 0,
3570
- isJumping: false,
3571
- });
3572
- const obstaclesRef = useRef<ObstacleState[]>([]);
3573
- const obstacleIdRef = useRef(0);
3574
- const animationFrameRef = useRef<number>(0);
3575
- const lastObstacleTimeRef = useRef(0);
3576
- const lastScoreTickRef = useRef(0);
3577
-
3578
- const jump = useCallback((): void => {
3579
- if (gameStatusRef.current === "dead") {
3580
- return;
3581
- }
3582
-
3583
- if (gameStatusRef.current === "idle") {
3584
- gameStatusRef.current = "running";
3585
- setGameStatus("running");
3586
- }
3587
-
3588
- if (!shrimpRef.current.isJumping) {
3589
- const nextShrimp: ShrimpState = {
3590
- ...shrimpRef.current,
3591
- velocityY: JUMP_VELOCITY,
3592
- isJumping: true,
3593
- };
3594
-
3595
- shrimpRef.current = nextShrimp;
3596
- setShrimp(nextShrimp);
3597
- }
3598
- }, []);
3599
-
3600
- const resetGame = useCallback((): void => {
3601
- const freshShrimp: ShrimpState = {
3602
- yPosition: 0,
3603
- velocityY: 0,
3604
- isJumping: false,
3605
- };
3606
-
3607
- shrimpRef.current = freshShrimp;
3608
- obstaclesRef.current = [];
3609
- obstacleIdRef.current = 0;
3610
- scoreRef.current = 0;
3611
- gameStatusRef.current = "idle";
3612
- lastObstacleTimeRef.current = 0;
3613
- lastScoreTickRef.current = 0;
3614
-
3615
- setShrimp(freshShrimp);
3616
- setObstacles([]);
3617
- setScore(0);
3618
- setGameStatus("idle");
3619
- }, []);
3620
-
3621
- useEffect(() => {
3622
- let previousTimestamp = 0;
3623
-
3624
- function gameTick(timestamp: number): void {
3625
- if (gameStatusRef.current !== "running") {
3626
- animationFrameRef.current = requestAnimationFrame(gameTick);
3627
- return;
3628
- }
3629
-
3630
- const deltaTime = timestamp - previousTimestamp;
3631
- previousTimestamp = timestamp;
3632
-
3633
- if (deltaTime > FRAME_SKIP_THRESHOLD_MS) {
3634
- animationFrameRef.current = requestAnimationFrame(gameTick);
3635
- return;
3636
- }
3637
-
3638
- const currentShrimp = shrimpRef.current;
3639
- let nextVelocityY = currentShrimp.velocityY + GRAVITY;
3640
- let nextYPosition = currentShrimp.yPosition - nextVelocityY;
3641
-
3642
- if (nextYPosition <= 0) {
3643
- nextYPosition = 0;
3644
- nextVelocityY = 0;
3645
- }
3646
-
3647
- const nextShrimp: ShrimpState = {
3648
- yPosition: nextYPosition,
3649
- velocityY: nextVelocityY,
3650
- isJumping: nextYPosition > 0,
3651
- };
3652
-
3653
- shrimpRef.current = nextShrimp;
3654
- setShrimp(nextShrimp);
3655
-
3656
- const obstacleSpeed = getObstacleSpeed(scoreRef.current);
3657
-
3658
- const movedObstacles = obstaclesRef.current
3659
- .map(
3660
- (obstacle): ObstacleState => ({
3661
- ...obstacle,
3662
- xPosition: obstacle.xPosition - obstacleSpeed,
3663
- }),
3664
- )
3665
- .filter((obstacle) => obstacle.xPosition + obstacle.width > -10);
3666
-
3667
- if (
3668
- timestamp - lastObstacleTimeRef.current > OBSTACLE_SPAWN_INTERVAL_MS &&
3669
- movedObstacles.length < MAX_CONCURRENT_OBSTACLES
3670
- ) {
3671
- const obstacleHeight = 30 + Math.floor(Math.random() * 30);
3672
- const obstacleWidth = 20 + Math.floor(Math.random() * 20);
3673
-
3674
- movedObstacles.push({
3675
- id: obstacleIdRef.current,
3676
- xPosition: CANVAS_WIDTH + 20,
3677
- width: obstacleWidth,
3678
- height: obstacleHeight,
3679
- });
3680
-
3681
- obstacleIdRef.current += 1;
3682
- lastObstacleTimeRef.current = timestamp;
3683
- }
3684
-
3685
- obstaclesRef.current = movedObstacles;
3686
- setObstacles([...movedObstacles]);
3687
-
3688
- for (const obstacle of movedObstacles) {
3689
- if (checkCollision(nextShrimp.yPosition, obstacle)) {
3690
- gameStatusRef.current = "dead";
3691
- setGameStatus("dead");
3692
- setHighScore((previousHighScore: number) =>
3693
- Math.max(previousHighScore, scoreRef.current),
3694
- );
3695
- animationFrameRef.current = requestAnimationFrame(gameTick);
3696
- return;
3697
- }
3698
- }
3699
-
3700
- if (timestamp - lastScoreTickRef.current > SCORE_TICK_MS) {
3701
- scoreRef.current += 1;
3702
- setScore(scoreRef.current);
3703
- lastScoreTickRef.current = timestamp;
3704
- }
3705
-
3706
- animationFrameRef.current = requestAnimationFrame(gameTick);
3707
- }
3708
-
3709
- animationFrameRef.current = requestAnimationFrame(gameTick);
3710
-
3711
- return () => {
3712
- cancelAnimationFrame(animationFrameRef.current);
3713
- };
3714
- }, []);
3715
-
3716
- useEffect(() => {
3717
- function handleKeyDown(keyboardEvent: KeyboardEvent): void {
3718
- if (keyboardEvent.code === "Space") {
3719
- keyboardEvent.preventDefault();
3720
- jump();
3721
- }
3722
- }
3723
-
3724
- window.addEventListener("keydown", handleKeyDown);
3725
-
3726
- return () => {
3727
- window.removeEventListener("keydown", handleKeyDown);
3728
- };
3729
- }, [jump]);
3730
-
3731
- const shrimpBottomOffset = GROUND_STRIP_HEIGHT + shrimp.yPosition;
3732
- const isDead = gameStatus === "dead";
3733
- const isIdle = gameStatus === "idle";
3734
- const isRunning = gameStatus === "running";
3735
-
3736
- return (
3737
- <div className="flex flex-col items-start gap-4 py-4">
3738
- <div className="flex flex-wrap items-center gap-8">
3739
- <span className="font-mono text-xl font-bold tabular-nums text-red-600">
3740
- Score: {score}
3741
- </span>
3742
- {highScore > 0 && (
3743
- <span className="text-sm text-slate-400">Best: {highScore}</span>
3744
- )}
3745
- </div>
3746
-
3747
- <button
3748
- type="button"
3749
- className="relative block max-w-full cursor-pointer select-none overflow-hidden rounded-2xl border-2 border-red-600/30 bg-[#0e111a] p-0 text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
3750
- aria-label="ShrimpRun game area. Click or press Space to jump."
3751
- onClick={jump}
3752
- onKeyDown={(keyboardEvent: import("react").KeyboardEvent) => {
3753
- if (keyboardEvent.code === "Space") {
3754
- keyboardEvent.preventDefault();
3755
- jump();
3756
- }
3757
- }}
3758
- style={{
3759
- width: CANVAS_WIDTH,
3760
- height: CANVAS_HEIGHT,
3761
- }}
3762
- >
3763
- <span
3764
- className="absolute bottom-0 left-0 w-full rounded-sm bg-red-600"
3765
- style={{
3766
- height: GROUND_STRIP_HEIGHT,
3767
- }}
3768
- />
3769
-
3770
- <span
3771
- className="absolute shrimp-sprite"
3772
- style={{
3773
- left: SHRIMP_START_X,
3774
- bottom: shrimpBottomOffset,
3775
- width: SHRIMP_WIDTH,
3776
- height: SHRIMP_HEIGHT,
3777
- transform: isDead
3778
- ? "rotate(18deg) translateY(8px)"
3779
- : shrimp.isJumping
3780
- ? "rotate(-8deg) translateY(-4px)"
3781
- : "rotate(0deg)",
3782
- transition: "transform 120ms ease",
3783
- }}
3784
- >
3785
- <RaktaShrimpMascot isJumping={shrimp.isJumping} isDead={isDead} />
3786
- </span>
3787
-
3788
- {obstacles.map((obstacle) => (
3789
- <span
3790
- key={obstacle.id}
3791
- className="absolute shrimp-run-obstacle"
3792
- style={{
3793
- left: obstacle.xPosition,
3794
- bottom: GROUND_STRIP_HEIGHT,
3795
- width: obstacle.width,
3796
- height: obstacle.height,
3797
- }}
3798
- />
3799
- ))}
3800
-
3801
- {isIdle && (
3802
- <span className="pointer-events-none absolute inset-0 flex items-center justify-center text-sm text-slate-400">
3803
- Press Space or click to start
3804
- </span>
3805
- )}
3806
-
3807
- {isDead && (
3808
- <span className="absolute inset-0 flex flex-col items-center justify-center gap-2 bg-black/75">
3809
- <span className="text-lg font-bold tracking-widest text-red-600">
3810
- GAME OVER
3811
- </span>
3812
- <span className="text-sm text-slate-400">Score: {score}</span>
3813
- </span>
3814
- )}
3815
-
3816
- {isRunning && score > 0 && score % 50 === 0 && (
3817
- <span className="absolute right-3 top-2 text-xs font-bold tracking-widest text-red-600 opacity-80">
3818
- {score}!
3819
- </span>
3820
- )}
3821
- </button>
3822
-
3823
- <p className="min-h-5 text-sm text-slate-400">
3824
- {isIdle && "\uD83E\uDD90 Click or press Space to make the shrimp jump!"}
3825
- {isRunning && "\uD83E\uDD90 Don't hit the obstacles!"}
3826
- {isDead && "The shrimp got cooked. Try again!"}
3827
- </p>
3828
-
3829
- {isDead && (
3830
- <button
3831
- type="button"
3832
- className="w-fit rounded-lg bg-red-600 px-6 py-2 font-semibold text-white transition hover:bg-red-700 active:bg-red-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
3833
- onClick={resetGame}
3834
- >
3835
- Restart
3836
- </button>
3837
- )}
3838
- </div>
3839
- );
3840
- }
3841
- `;
3842
- }
3843
- function generateFullstackHomePage(projectName) {
3844
- return `import React from "react";
3845
-
3846
- export default function HomePage() {
3847
- return (
3848
- <main className="page-shell">
3849
- <section className="hero-card">
3850
- <p className="eyebrow">THE RED ROUTER FRAMEWORK</p>
3851
- <h1>Welcome to ${projectName}</h1>
3852
- <p>
3853
- Built with Rakta.js \u2014 Small in size. Fierce in speed. Alive in every route.
3854
- </p>
3855
- <div className="button-row">
3856
- <a href="/about">About</a>
3857
- <a href="/blog">Blog</a>
3858
- </div>
3859
- </section>
3860
- </main>
3861
- );
3862
- }
3863
- `;
3937
+ function generateFullstackHomePage(_projectName) {
3938
+ return WELCOME_PAGE_CODE;
3864
3939
  }
3865
3940
  function getDatabaseDependencies(selectedDatabase) {
3866
3941
  switch (selectedDatabase) {
@@ -4174,6 +4249,13 @@ async function promptProjectMode() {
4174
4249
  });
4175
4250
  return getPromptValue(promptResult);
4176
4251
  }
4252
+ async function promptTypeScript() {
4253
+ const promptResult = await confirm({
4254
+ message: "Would you like to use TypeScript? (recommended)",
4255
+ initialValue: true
4256
+ });
4257
+ return getPromptValue(promptResult);
4258
+ }
4177
4259
  async function promptCssFramework() {
4178
4260
  const promptResult = await select({
4179
4261
  message: "Choose a CSS framework:",
@@ -4312,12 +4394,14 @@ async function promptDatabase() {
4312
4394
  }
4313
4395
  async function runPrompts(projectName) {
4314
4396
  const projectMode = await promptProjectMode();
4397
+ const useTypeScript = await promptTypeScript();
4315
4398
  const cssFramework = await promptCssFramework();
4316
4399
  const renderMode = await promptRenderMode();
4317
4400
  if (projectMode === "frontend-only") {
4318
4401
  return {
4319
4402
  projectName,
4320
4403
  projectMode,
4404
+ useTypeScript,
4321
4405
  cssFramework,
4322
4406
  renderMode,
4323
4407
  backendFramework: "gaman",
@@ -4329,6 +4413,7 @@ async function runPrompts(projectName) {
4329
4413
  return {
4330
4414
  projectName,
4331
4415
  projectMode,
4416
+ useTypeScript,
4332
4417
  cssFramework,
4333
4418
  renderMode,
4334
4419
  backendFramework,
@@ -4346,21 +4431,21 @@ function getProjectNameFromArgs(cliArgs) {
4346
4431
  }
4347
4432
  function formatFullstackCommands() {
4348
4433
  return [
4349
- `${import_picocolors.default.dim("# Terminal 1")}`,
4350
- "cd frontend",
4351
- "bun install",
4352
- "bun run dev",
4434
+ import_picocolors.default.dim("# Terminal 1"),
4435
+ import_picocolors.default.cyan("cd frontend"),
4436
+ import_picocolors.default.cyan("bun install"),
4437
+ import_picocolors.default.cyan("bun run dev"),
4353
4438
  "",
4354
- `${import_picocolors.default.dim("# Terminal 2")}`,
4355
- "cd backend",
4356
- "bun install",
4357
- "bun run dev"
4358
- ].join(`
4359
- `);
4439
+ import_picocolors.default.dim("# Terminal 2"),
4440
+ import_picocolors.default.cyan("cd backend"),
4441
+ import_picocolors.default.cyan("bun install"),
4442
+ import_picocolors.default.cyan("bun run dev")
4443
+ ].map((line) => line.length === 0 ? "" : ` ${line}`).join(`
4444
+ `);
4360
4445
  }
4361
4446
  function formatFrontendOnlyCommands(projectName) {
4362
- return [`cd ${projectName}`, "bun install", "bun run dev"].join(`
4363
- `);
4447
+ return [`cd ${projectName}`, "bun install", "bun run dev"].map((command) => ` ${import_picocolors.default.cyan(command)}`).join(`
4448
+ `);
4364
4449
  }
4365
4450
  function printSuccessMessage(projectConfig) {
4366
4451
  const modeLabel = PROJECT_MODE_DISPLAY[projectConfig.projectMode];
@@ -4381,7 +4466,7 @@ function printSuccessMessage(projectConfig) {
4381
4466
 
4382
4467
  ${import_picocolors.default.bold("Next steps:")}
4383
4468
 
4384
- ${nextSteps}
4469
+ ${nextSteps}
4385
4470
 
4386
4471
  ${import_picocolors.default.bold("Frontend:")} ${import_picocolors.default.cyan("http://localhost:3000")}
4387
4472
  ${isFullstack ? `${import_picocolors.default.bold("Backend:")} ${import_picocolors.default.cyan("http://localhost:4000")}` : ""}
@@ -4421,4 +4506,4 @@ Error: ${errorMessage}
4421
4506
  process.exit(1);
4422
4507
  });
4423
4508
 
4424
- //# debugId=7E0E06BF6F6CB6FA64756E2164756E21
4509
+ //# debugId=2E17890E5DBF1B0764756E2164756E21