create-rakta-app 0.1.7 → 0.1.9

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,34 +886,16 @@ 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
- }
907
889
  var _ = {
908
890
  Y: { type: "year", len: 4 },
909
891
  M: { type: "month", len: 2 },
910
892
  D: { type: "day", len: 2 }
911
893
  };
912
- function M(r2) {
913
- return [...r2].map((t2) => _[t2]);
894
+ function M(r) {
895
+ return [...r].map((t2) => _[t2]);
914
896
  }
915
- function P(r2) {
916
- const i = new Intl.DateTimeFormat(r2, {
897
+ function P(r) {
898
+ const i = new Intl.DateTimeFormat(r, {
917
899
  year: "numeric",
918
900
  month: "2-digit",
919
901
  day: "2-digit"
@@ -923,32 +905,32 @@ function P(r2) {
923
905
  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 });
924
906
  return { segments: s, separator: n };
925
907
  }
926
- function p(r2) {
927
- return Number.parseInt((r2 || "0").replace(/_/g, "0"), 10) || 0;
908
+ function p(r) {
909
+ return Number.parseInt((r || "0").replace(/_/g, "0"), 10) || 0;
928
910
  }
929
- function f(r2) {
911
+ function f(r) {
930
912
  return {
931
- year: p(r2.year),
932
- month: p(r2.month),
933
- day: p(r2.day)
913
+ year: p(r.year),
914
+ month: p(r.month),
915
+ day: p(r.day)
934
916
  };
935
917
  }
936
- function c(r2, t2) {
937
- return new Date(r2 || 2001, t2 || 1, 0).getDate();
918
+ function c(r, t2) {
919
+ return new Date(r || 2001, t2 || 1, 0).getDate();
938
920
  }
939
- function b(r2) {
940
- const { year: t2, month: i, day: s } = f(r2);
921
+ function b(r) {
922
+ const { year: t2, month: i, day: s } = f(r);
941
923
  if (!t2 || t2 < 0 || t2 > 9999 || !i || i < 1 || i > 12 || !s || s < 1)
942
924
  return;
943
925
  const n = new Date(Date.UTC(t2, i - 1, s));
944
926
  if (!(n.getUTCFullYear() !== t2 || n.getUTCMonth() !== i - 1 || n.getUTCDate() !== s))
945
927
  return { year: t2, month: i, day: s };
946
928
  }
947
- function C(r2) {
948
- const t2 = b(r2);
929
+ function C(r) {
930
+ const t2 = b(r);
949
931
  return t2 ? new Date(Date.UTC(t2.year, t2.month - 1, t2.day)) : undefined;
950
932
  }
951
- function T2(r2, t2, i, s) {
933
+ function T2(r, t2, i, s) {
952
934
  const n = i ? {
953
935
  year: i.getUTCFullYear(),
954
936
  month: i.getUTCMonth() + 1,
@@ -958,7 +940,7 @@ function T2(r2, t2, i, s) {
958
940
  month: s.getUTCMonth() + 1,
959
941
  day: s.getUTCDate()
960
942
  } : null;
961
- return r2 === "year" ? { min: n?.year ?? 1, max: e?.year ?? 9999 } : r2 === "month" ? {
943
+ return r === "year" ? { min: n?.year ?? 1, max: e?.year ?? 9999 } : r === "month" ? {
962
944
  min: n && t2.year === n.year ? n.month : 1,
963
945
  max: e && t2.year === e.year ? e.month : 12
964
946
  } : {
@@ -1131,26 +1113,26 @@ var u$1 = class u extends V {
1131
1113
  cursor = 0;
1132
1114
  #t;
1133
1115
  getGroupItems(t2) {
1134
- return this.options.filter((r2) => r2.group === t2);
1116
+ return this.options.filter((r) => r.group === t2);
1135
1117
  }
1136
1118
  isGroupSelected(t2) {
1137
- const r2 = this.getGroupItems(t2), e = this.value;
1138
- return e === undefined ? false : r2.every((s) => e.includes(s.value));
1119
+ const r = this.getGroupItems(t2), e = this.value;
1120
+ return e === undefined ? false : r.every((s) => e.includes(s.value));
1139
1121
  }
1140
1122
  toggleValue() {
1141
1123
  const t2 = this.options[this.cursor];
1142
1124
  if (this.value === undefined && (this.value = []), t2.group === true) {
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));
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));
1145
1127
  } else {
1146
- const r2 = this.value.includes(t2.value);
1147
- this.value = r2 ? this.value.filter((e) => e !== t2.value) : [...this.value, t2.value];
1128
+ const r = this.value.includes(t2.value);
1129
+ this.value = r ? this.value.filter((e) => e !== t2.value) : [...this.value, t2.value];
1148
1130
  }
1149
1131
  }
1150
1132
  constructor(t2) {
1151
1133
  super(t2, false);
1152
- const { options: r2 } = t2;
1153
- this.#t = t2.selectableGroups !== false, this.options = Object.entries(r2).flatMap(([e, s]) => [
1134
+ const { options: r } = t2;
1135
+ this.#t = t2.selectableGroups !== false, this.options = Object.entries(r).flatMap(([e, s]) => [
1154
1136
  { value: e, group: true, label: e },
1155
1137
  ...s.map((i) => ({ ...i, group: e }))
1156
1138
  ]), this.value = [...t2.initialValues ?? []], this.cursor = Math.max(this.options.findIndex(({ value: e }) => e === t2.cursorAt), this.#t ? 0 : 1), this.on("cursor", (e) => {
@@ -1188,10 +1170,10 @@ class h extends V {
1188
1170
  const t2 = this.userInput;
1189
1171
  if (this.cursor >= t2.length)
1190
1172
  return `${t2}\u2588`;
1191
- const s = t2.slice(0, this.cursor), r2 = t2[this.cursor], i = t2.slice(this.cursor + 1);
1192
- return r2 === `
1173
+ const s = t2.slice(0, this.cursor), r = t2[this.cursor], i = t2.slice(this.cursor + 1);
1174
+ return r === `
1193
1175
  ` ? `${s}\u2588
1194
- ${i}` : `${s}${styleText("inverse", r2)}${i}`;
1176
+ ${i}` : `${s}${styleText("inverse", r)}${i}`;
1195
1177
  }
1196
1178
  get cursor() {
1197
1179
  return this._cursor;
@@ -1224,8 +1206,8 @@ ${i}` : `${s}${styleText("inverse", r2)}${i}`;
1224
1206
  if (this.#s)
1225
1207
  return this.focused === "submit" ? true : (this.#r(`
1226
1208
  `), this._cursor++, false);
1227
- const r2 = this.#t;
1228
- return this.#t = true, r2 && this.cursor === this.userInput.length ? (this.userInput[this.cursor - 1] === `
1209
+ const r = this.#t;
1210
+ return this.#t = true, r && this.cursor === this.userInput.length ? (this.userInput[this.cursor - 1] === `
1229
1211
  ` && (this._setUserInput(this.userInput.slice(0, this.cursor - 1) + this.userInput.slice(this.cursor)), this._cursor--), true) : (this.#r(`
1230
1212
  `), this._cursor++, false);
1231
1213
  }
@@ -1234,12 +1216,12 @@ ${i}` : `${s}${styleText("inverse", r2)}${i}`;
1234
1216
  super({
1235
1217
  ...t2,
1236
1218
  initialUserInput: s
1237
- }, false), s !== undefined && (this._cursor = s.length), this.#s = t2.showSubmit ?? false, this.on("key", (r2, i) => {
1219
+ }, false), s !== undefined && (this._cursor = s.length), this.#s = t2.showSubmit ?? false, this.on("key", (r, i) => {
1238
1220
  if (i?.name && o$1.has(i.name)) {
1239
1221
  this.#t = false, this.#i(i.name);
1240
1222
  return;
1241
1223
  }
1242
- if (r2 === "\t" && this.#s) {
1224
+ if (r === "\t" && this.#s) {
1243
1225
  this.focused = this.focused === "editor" ? "submit" : "editor";
1244
1226
  return;
1245
1227
  }
@@ -1252,10 +1234,10 @@ ${i}` : `${s}${styleText("inverse", r2)}${i}`;
1252
1234
  this._setUserInput(this.userInput.slice(0, this.cursor) + this.userInput.slice(this.cursor + 1));
1253
1235
  return;
1254
1236
  }
1255
- r2 && (this.#s && this.focused === "submit" && (this.focused = "editor"), this.#r(r2 ?? ""), this._cursor++);
1237
+ r && (this.#s && this.focused === "submit" && (this.focused = "editor"), this.#r(r ?? ""), this._cursor++);
1256
1238
  }
1257
- }), this.on("userInput", (r2) => {
1258
- this._setValue(r2);
1239
+ }), this.on("userInput", (r) => {
1240
+ this._setValue(r);
1259
1241
  }), this.on("finalize", () => {
1260
1242
  this.value || (this.value = t2.defaultValue), this.value === undefined && (this.value = "");
1261
1243
  });
@@ -1295,8 +1277,8 @@ class n extends V {
1295
1277
  const t2 = this.userInput;
1296
1278
  if (this.cursor >= t2.length)
1297
1279
  return `${this.userInput}\u2588`;
1298
- const e = t2.slice(0, this.cursor), [s, ...r2] = t2.slice(this.cursor);
1299
- return `${e}${styleText("inverse", s)}${r2.join("")}`;
1280
+ const e = t2.slice(0, this.cursor), [s, ...r] = t2.slice(this.cursor);
1281
+ return `${e}${styleText("inverse", s)}${r.join("")}`;
1300
1282
  }
1301
1283
  get cursor() {
1302
1284
  return this._cursor;
@@ -1430,41 +1412,6 @@ var limitOptions = ({
1430
1412
  b2.push(n2);
1431
1413
  return u3 && b2.push(C2), b2;
1432
1414
  };
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
- };
1468
1415
  var MULTISELECT_INSTRUCTIONS = [
1469
1416
  `${styleText2("dim", "\u2191/\u2193")} to navigate`,
1470
1417
  `${styleText2("dim", "Space:")} select`,
@@ -1719,313 +1666,131 @@ function writeProjectFiles(projectRoot, generatedFiles) {
1719
1666
  // src/generator.ts
1720
1667
  import { readFileSync } from "fs";
1721
1668
 
1722
- // src/types.ts
1723
- var CSS_DISPLAY = {
1724
- tailwind: "Tailwind CSS",
1725
- bootstrap: "Bootstrap",
1726
- sass: "SASS",
1727
- none: "None"
1728
- };
1729
- var BACKEND_DISPLAY = {
1730
- gaman: "Gaman.js",
1731
- express: "Express.js",
1732
- nest: "Nest.js",
1733
- adonis: "Adonis.js"
1734
- };
1735
- var DATABASE_DISPLAY = {
1736
- postgresql: "PostgreSQL",
1737
- mysql: "MySQL",
1738
- mongodb: "MongoDB",
1739
- firebase: "Firebase",
1740
- sqlite: "SQLite",
1741
- mariadb: "MariaDB",
1742
- redis: "Redis",
1743
- planetscale: "PlanetScale",
1744
- neon: "Neon",
1745
- turso: "Turso"
1746
- };
1747
- var RENDER_MODE_DISPLAY = {
1748
- csr: "CSR \u2014 Client-Side Rendering",
1749
- ssr: "SSR \u2014 Server-Side Rendering",
1750
- ssg: "SSG \u2014 Static Site Generation",
1751
- csg: "CSG \u2014 Configurable Static Generation",
1752
- spa: "SPA \u2014 Single Page Application",
1753
- hybrid: "Hybrid \u2014 Mixed per-route"
1754
- };
1755
- var PROJECT_MODE_DISPLAY = {
1756
- fullstack: "Fullstack app (frontend + backend + database)",
1757
- "frontend-only": "Frontend only (no backend, no database)"
1758
- };
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;
1669
+ // src/starter.ts
1670
+ var STARTER_TYPES_CODE = `export interface SystemMetric {
1671
+ name: string;
1672
+ value: string | number;
1673
+ status: "nominal" | "warning" | "critical";
1770
1674
  }
1771
1675
 
1772
- type ComponentState = Record<string, string | number | boolean | undefined>;
1773
- type SetComponentState = (state: ComponentState) => void;
1774
-
1775
- interface ComponentItem {
1776
- id: string;
1676
+ export interface GameHighScore {
1777
1677
  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
- ];
1678
+ score: number;
1679
+ date: string;
1680
+ }
1911
1681
 
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);
1682
+ export type AestheticUnit = "LENIS-MODERN" | "RETRO-CYBER" | "NEO-BRUTALIST";
1683
+ `;
1684
+ var STARTER_AUDIO_CODE = `let audioContext: AudioContext | null = null;
1916
1685
 
1917
- if (!isOpen) return null;
1686
+ function getAudioContext(): AudioContext | null {
1687
+ if (typeof window === "undefined") return null;
1918
1688
 
1919
- const fallbackComponent = BRUTALIST_COMPONENTS[0];
1920
- if (!fallbackComponent) return null;
1689
+ const AudioContextCtor = window.AudioContext ?? window.webkitAudioContext;
1690
+ if (!AudioContextCtor) return null;
1921
1691
 
1922
- const activeComp = BRUTALIST_COMPONENTS.find(c => c.id === activeCompId) ?? fallbackComponent;
1923
- const activeState = componentStates[activeCompId] || {};
1692
+ audioContext ??= new AudioContextCtor();
1693
+ return audioContext;
1694
+ }
1924
1695
 
1925
- const handleSetState = (newState: ComponentState) => {
1926
- setComponentStates({
1927
- ...componentStates,
1928
- [activeCompId]: newState
1929
- });
1930
- };
1696
+ function playTone(frequency: number, durationMs: number, type: OscillatorType): void {
1697
+ const context = getAudioContext();
1698
+ if (!context) return;
1931
1699
 
1932
- const handleCopyCode = (code: string) => {
1933
- navigator.clipboard.writeText(code);
1934
- setCopiedId(activeCompId);
1935
- setTimeout(() => setCopiedId(null), 2000);
1936
- };
1700
+ const oscillator = context.createOscillator();
1701
+ const gain = context.createGain();
1937
1702
 
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>
1703
+ oscillator.type = type;
1704
+ oscillator.frequency.setValueAtTime(frequency, context.currentTime);
1705
+ gain.gain.setValueAtTime(0.045, context.currentTime);
1706
+ gain.gain.exponentialRampToValueAtTime(0.001, context.currentTime + durationMs / 1000);
1962
1707
 
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>
1708
+ oscillator.connect(gain);
1709
+ gain.connect(context.destination);
1710
+ oscillator.start();
1711
+ oscillator.stop(context.currentTime + durationMs / 1000);
1712
+ }
1983
1713
 
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>
1714
+ export function playJumpSound(): void {
1715
+ playTone(560, 90, "triangle");
1716
+ }
1989
1717
 
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>
1718
+ export function playScoreSound(): void {
1719
+ playTone(880, 120, "square");
1720
+ }
1721
+ `;
1722
+ var STARTER_SHRIMP_CHARACTER_CODE = `interface ShrimpCharacterProps {
1723
+ status: "IDLE" | "SWIMMING" | "JUMPING" | "DEAD";
1724
+ playerY: number;
1725
+ rotation?: number;
1726
+ }
1994
1727
 
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>
1728
+ export default function ShrimpCharacter({
1729
+ status,
1730
+ playerY,
1731
+ rotation,
1732
+ }: ShrimpCharacterProps) {
1733
+ const isDead = status === "DEAD";
1734
+ const isSwimming = status === "SWIMMING";
1735
+ const finalRotation = rotation ?? (isDead ? 180 : status === "JUMPING" ? -10 : 0);
1736
+ const shrimpColor = isDead ? "fill-orange-400" : "fill-rose-500";
1737
+
1738
+ return (
1739
+ <div
1740
+ className="relative flex h-16 w-16 select-none items-center justify-center transition-transform duration-75"
1741
+ style={{ transform: \`translateY(\${-playerY}px) rotate(\${finalRotation}deg)\` }}
1742
+ >
1743
+ <svg
1744
+ aria-label={isDead ? "Shrimp failed the run" : "Rakta shrimp runner"}
1745
+ role="img"
1746
+ viewBox="0 0 100 100"
1747
+ className={\`h-full w-full drop-shadow-[0_4px_12px_rgba(244,63,94,0.32)] \${isSwimming ? "animate-pulse" : ""}\`}
1748
+ >
1749
+ <path
1750
+ 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"
1751
+ className={shrimpColor}
1752
+ />
1753
+ <path
1754
+ d="M 55 30 C 65 24, 76 34, 74 46 C 72 54, 62 55, 55 48 Z"
1755
+ className={shrimpColor}
1756
+ />
1757
+ <path
1758
+ d="M 29 51 C 21 57, 16 61, 10 62"
1759
+ stroke={isDead ? "#fb923c" : "#f43f5e"}
1760
+ strokeWidth="4"
1761
+ strokeLinecap="round"
1762
+ fill="none"
1763
+ />
1764
+ <path
1765
+ d="M 31 55 C 25 64, 22 70, 17 76"
1766
+ stroke={isDead ? "#fb923c" : "#f43f5e"}
1767
+ strokeWidth="3"
1768
+ strokeLinecap="round"
1769
+ fill="none"
1770
+ />
1771
+ <circle cx="66" cy="38" r="4.5" fill={isDead ? "#18181b" : "#000"} />
1772
+ {!isDead && <circle cx="67.5" cy="36.5" r="1.2" fill="#fff" />}
1773
+ <path
1774
+ d="M 62 30 C 70 21, 82 19, 90 24"
1775
+ stroke="#fb7185"
1776
+ strokeWidth="2"
1777
+ strokeLinecap="round"
1778
+ fill="none"
1779
+ />
1780
+ <path
1781
+ d="M 63 33 C 73 29, 84 31, 91 39"
1782
+ stroke="#fb7185"
1783
+ strokeWidth="2"
1784
+ strokeLinecap="round"
1785
+ fill="none"
1786
+ />
1787
+ </svg>
2023
1788
  </div>
2024
1789
  );
2025
1790
  }
2026
1791
  `;
2027
- var CORAL_OBSTACLE_CODE = `interface CoralObstacleProps {
2028
- position: 'TOP' | 'BOTTOM';
1792
+ var STARTER_CORAL_OBSTACLE_CODE = `interface CoralObstacleProps {
1793
+ position: "TOP" | "BOTTOM";
2029
1794
  height: number;
2030
1795
  width?: number;
2031
1796
  paletteIndex?: number;
@@ -2033,329 +1798,542 @@ var CORAL_OBSTACLE_CODE = `interface CoralObstacleProps {
2033
1798
  scaleX?: number;
2034
1799
  }
2035
1800
 
2036
- export default function CoralObstacle({
2037
- position,
2038
- height,
1801
+ export default function CoralObstacle({
1802
+ position,
1803
+ height,
2039
1804
  width = 64,
2040
- paletteIndex = 0,
2041
- variant = 0,
2042
- scaleX = 1
1805
+ paletteIndex = 0,
1806
+ variant = 0,
1807
+ scaleX = 1,
2043
1808
  }: CoralObstacleProps) {
2044
- const defaultPalette = { primary: '#f43f5e', secondary: '#fda4af', shadow: '#9f1239', polyps: '#ffe4e6', name: 'rose' };
1809
+ const fallbackPalette = {
1810
+ primary: "#f43f5e",
1811
+ secondary: "#fda4af",
1812
+ shadow: "#9f1239",
1813
+ polyps: "#ffe4e6",
1814
+ name: "rose",
1815
+ };
2045
1816
  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' },
1817
+ fallbackPalette,
1818
+ { primary: "#06b6d4", secondary: "#67e8f9", shadow: "#155e75", polyps: "#ecfeff", name: "cyan" },
1819
+ { primary: "#a855f7", secondary: "#d8b4fe", shadow: "#581c87", polyps: "#faf5ff", name: "amethyst" },
1820
+ { primary: "#f59e0b", secondary: "#fde047", shadow: "#78350f", polyps: "#fefce8", name: "amber" },
1821
+ { primary: "#10b981", secondary: "#6ee7b7", shadow: "#065f46", polyps: "#e6fffa", name: "emerald" },
2051
1822
  ];
2052
- const palette = palettes[paletteIndex % palettes.length] ?? defaultPalette;
1823
+
1824
+ const palette = palettes[paletteIndex % palettes.length] ?? fallbackPalette;
2053
1825
  const coralId = \`coralGrad-\${palette.name}-\${variant}\`;
1826
+ const flipped = position === "TOP" ? "scale(1, -1) translate(0, -120)" : undefined;
2054
1827
 
2055
1828
  return (
2056
- <div
1829
+ <div
2057
1830
  className="absolute flex items-center justify-center pointer-events-none"
2058
1831
  style={{ height: \`\${height}px\`, width: \`\${width}px\`, transform: \`scaleX(\${scaleX})\` }}
2059
1832
  >
2060
- <svg
1833
+ <svg
2061
1834
  aria-hidden="true"
2062
1835
  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)]"
1836
+ viewBox="0 0 80 120"
1837
+ className="h-full w-full drop-shadow-[0_6px_14px_rgba(0,0,0,0.7)]"
2065
1838
  preserveAspectRatio="none"
2066
1839
  >
2067
1840
  <defs>
2068
1841
  <linearGradient id={coralId} x1="0%" y1="0%" x2="100%" y2="100%">
2069
1842
  <stop offset="0%" stopColor={palette.secondary} />
2070
- <stop offset="50%" stopColor={palette.primary} />
1843
+ <stop offset="52%" stopColor={palette.primary} />
2071
1844
  <stop offset="100%" stopColor={palette.shadow} />
2072
1845
  </linearGradient>
1846
+ <filter id={\`coralGlow-\${variant}\`}>
1847
+ <feGaussianBlur stdDeviation="1.8" result="coloredBlur" />
1848
+ <feMerge>
1849
+ <feMergeNode in="coloredBlur" />
1850
+ <feMergeNode in="SourceGraphic" />
1851
+ </feMerge>
1852
+ </filter>
2073
1853
  </defs>
2074
1854
 
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})\`} />
1855
+ <g transform={flipped} filter={\`url(#coralGlow-\${variant})\`}>
1856
+ <path
1857
+ d="M 35 120 C 30 100, 15 90, 10 70 C 5 50, 20 40, 15 25 C 10 10, 25 5, 30 15 C 35 30, 28 45, 38 65 Q 42 90, 40 120"
1858
+ fill={palette.shadow}
1859
+ opacity="0.85"
1860
+ />
1861
+ <path
1862
+ d="M 45 120 C 50 105, 65 95, 70 75 C 75 55, 60 45, 65 30 C 70 15, 55 10, 50 20 C 45 35, 52 45, 42 65 Q 40 90, 45 120"
1863
+ fill={palette.shadow}
1864
+ opacity="0.85"
1865
+ />
1866
+ <path
1867
+ 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"
1868
+ fill={\`url(#\${coralId})\`}
1869
+ />
1870
+ <path
1871
+ 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"
1872
+ fill={\`url(#\${coralId})\`}
1873
+ />
1874
+ <circle cx="22" cy="11" r="3.5" fill={palette.polyps} />
1875
+ <circle cx="58" cy="21" r="3.5" fill={palette.polyps} />
1876
+ <circle cx="30" cy="15" r="2.5" fill={palette.secondary} />
1877
+ <circle cx="50" cy="20" r="2.5" fill={palette.secondary} />
2078
1878
  </g>
2079
1879
  </svg>
2080
1880
  </div>
2081
1881
  );
2082
1882
  }
2083
1883
  `;
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
- ];
1884
+ var STARTER_PAGE_CODE = `import { useCallback, useEffect, useRef, useState } from "react";
1885
+ import {
1886
+ LuActivity,
1887
+ LuCode2,
1888
+ LuCpu,
1889
+ LuGauge,
1890
+ LuRocket,
1891
+ LuShieldCheck,
1892
+ LuZap,
1893
+ } from "react-icons/lu";
1894
+ import CoralObstacle from "./components/CoralObstacle";
1895
+ import ShrimpCharacter from "./components/ShrimpCharacter";
1896
+ import type { AestheticUnit } from "./types";
1897
+ import { playJumpSound, playScoreSound } from "./utils/audio";
1898
+
1899
+ type GameStatus = "IDLE" | "SWIMMING" | "JUMPING" | "DEAD";
1900
+ type SimSpeed = "NORMAL" | "FAST" | "TURBO";
1901
+
1902
+ const GAME_WIDTH = 720;
1903
+ const GAME_HEIGHT = 280;
1904
+ const SHRIMP_X = 82;
1905
+ const SHRIMP_SIZE = 58;
1906
+ const GRAVITY = 0.82;
1907
+ const JUMP_FORCE = 15;
1908
+
1909
+ function getSpeedValue(speed: SimSpeed): number {
1910
+ if (speed === "TURBO") return 8.2;
1911
+ if (speed === "FAST") return 6.4;
1912
+ return 5.2;
1913
+ }
2104
1914
 
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);
1915
+ export default function WelcomePage() {
1916
+ const [status, setStatus] = useState<GameStatus>("IDLE");
1917
+ const [score, setScore] = useState(0);
1918
+ const [bestScore, setBestScore] = useState(() => {
1919
+ if (typeof localStorage === "undefined") return 0;
1920
+ return parseInt(localStorage.getItem("rakta_shrimprun_highscore") || "0", 10);
1921
+ });
1922
+ const [playerY, setPlayerY] = useState(0);
1923
+ const [rotation, setRotation] = useState(0);
1924
+ const [obstacleX, setObstacleX] = useState(GAME_WIDTH + 120);
1925
+ const [obstacleHeight, setObstacleHeight] = useState(84);
1926
+ const [obstacleWidth, setObstacleWidth] = useState(58);
1927
+ const [obstaclePalette, setObstaclePalette] = useState(0);
1928
+ const [obstacleVariant, setObstacleVariant] = useState(0);
1929
+ const [obstacleScaleX, setObstacleScaleX] = useState(1);
1930
+ const [aestheticUnit, setAestheticUnit] = useState<AestheticUnit>("LENIS-MODERN");
1931
+ const [simSpeed, setSimSpeed] = useState<SimSpeed>("NORMAL");
1932
+ const [lowLatencyMode, setLowLatencyMode] = useState(true);
1933
+ const [configToast, setConfigToast] = useState<string | null>(null);
1934
+
1935
+ const statusRef = useRef(status);
1936
+ const velocityRef = useRef(0);
1937
+ const playerYRef = useRef(0);
1938
+ const obstacleXRef = useRef(GAME_WIDTH + 120);
1939
+ const scoreRef = useRef(0);
1940
+ const lastTimeRef = useRef(0);
1941
+ const animationRef = useRef<number | null>(null);
1942
+ const toastTimerRef = useRef<number | null>(null);
2110
1943
 
2111
1944
  useEffect(() => {
2112
- if (terminalEndRef.current) {
2113
- terminalEndRef.current.scrollIntoView({ behavior: 'smooth' });
1945
+ statusRef.current = status;
1946
+ }, [status]);
1947
+
1948
+ const showConfigToast = useCallback((message: string) => {
1949
+ setConfigToast(message);
1950
+ if (toastTimerRef.current !== null) {
1951
+ window.clearTimeout(toastTimerRef.current);
1952
+ }
1953
+ toastTimerRef.current = window.setTimeout(() => setConfigToast(null), 1400);
1954
+ }, []);
1955
+
1956
+ const resetObstacle = useCallback(() => {
1957
+ obstacleXRef.current = GAME_WIDTH + 80 + Math.random() * 220;
1958
+ setObstacleX(obstacleXRef.current);
1959
+ setObstacleHeight(58 + Math.floor(Math.random() * 82));
1960
+ setObstacleWidth(42 + Math.floor(Math.random() * 32));
1961
+ setObstaclePalette((value) => (value + 1) % 5);
1962
+ setObstacleVariant((value) => (value + 1) % 3);
1963
+ setObstacleScaleX(Math.random() > 0.5 ? 1 : -1);
1964
+ }, []);
1965
+
1966
+ const startRun = useCallback(() => {
1967
+ if (statusRef.current === "DEAD") {
1968
+ scoreRef.current = 0;
1969
+ setScore(0);
1970
+ playerYRef.current = 0;
1971
+ velocityRef.current = 0;
1972
+ setPlayerY(0);
1973
+ resetObstacle();
2114
1974
  }
2115
- });
2116
1975
 
2117
- if (!isOpen) return null;
1976
+ statusRef.current = "SWIMMING";
1977
+ setStatus("SWIMMING");
1978
+ }, [resetObstacle]);
2118
1979
 
2119
- const startDeployment = () => {
2120
- setStatus('building');
2121
- setLogs([]);
2122
- setProgress(0);
1980
+ const jump = useCallback(() => {
1981
+ if (statusRef.current === "IDLE" || statusRef.current === "DEAD") {
1982
+ startRun();
1983
+ }
1984
+
1985
+ if (playerYRef.current <= 4) {
1986
+ velocityRef.current = JUMP_FORCE;
1987
+ setStatus("JUMPING");
1988
+ playJumpSound();
1989
+ }
1990
+ }, [startRun]);
1991
+
1992
+ useEffect(() => {
1993
+ function onKeyDown(event: KeyboardEvent): void {
1994
+ if (event.code === "Space") {
1995
+ event.preventDefault();
1996
+ jump();
1997
+ }
1998
+ }
1999
+
2000
+ window.addEventListener("keydown", onKeyDown);
2001
+ return () => window.removeEventListener("keydown", onKeyDown);
2002
+ }, [jump]);
2003
+
2004
+ useEffect(() => {
2005
+ function frame(timestamp: number): void {
2006
+ const delta = Math.min(timestamp - lastTimeRef.current, 32) / 16.67;
2007
+ lastTimeRef.current = timestamp;
2123
2008
 
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]);
2009
+ if (statusRef.current === "SWIMMING" || statusRef.current === "JUMPING") {
2010
+ velocityRef.current -= GRAVITY * delta;
2011
+ playerYRef.current = Math.max(0, playerYRef.current + velocityRef.current * delta);
2012
+
2013
+ if (playerYRef.current === 0 && velocityRef.current < 0) {
2014
+ velocityRef.current = 0;
2015
+ setStatus("SWIMMING");
2016
+ }
2017
+
2018
+ obstacleXRef.current -= getSpeedValue(simSpeed) * (lowLatencyMode ? 1.08 : 0.92) * delta;
2019
+
2020
+ if (obstacleXRef.current < -90) {
2021
+ resetObstacle();
2022
+ scoreRef.current += 7;
2023
+ setScore(scoreRef.current);
2024
+ playScoreSound();
2025
+ }
2026
+
2027
+ scoreRef.current += 1;
2028
+ setScore(scoreRef.current);
2029
+
2030
+ const shrimpLeft = SHRIMP_X + 8;
2031
+ const shrimpRight = SHRIMP_X + SHRIMP_SIZE - 8;
2032
+ const shrimpBottom = GAME_HEIGHT - 54 - playerYRef.current;
2033
+ const shrimpTop = shrimpBottom - SHRIMP_SIZE + 12;
2034
+ const obstacleLeft = obstacleXRef.current + 8;
2035
+ const obstacleRight = obstacleXRef.current + obstacleWidth - 8;
2036
+ const obstacleTop = GAME_HEIGHT - 48 - obstacleHeight;
2037
+ const obstacleBottom = GAME_HEIGHT - 48;
2038
+ const didCollide =
2039
+ shrimpLeft < obstacleRight &&
2040
+ shrimpRight > obstacleLeft &&
2041
+ shrimpTop < obstacleBottom &&
2042
+ shrimpBottom > obstacleTop;
2043
+
2044
+ if (didCollide) {
2045
+ statusRef.current = "DEAD";
2046
+ setStatus("DEAD");
2047
+ setRotation(180);
2048
+ const nextBest = Math.max(bestScore, scoreRef.current);
2049
+ setBestScore(nextBest);
2050
+ localStorage.setItem("rakta_shrimprun_highscore", String(nextBest));
2051
+ } else {
2052
+ setRotation(playerYRef.current > 0 ? -8 : 0);
2131
2053
  }
2132
- setProgress(Math.round(((currentLogIndex + 1) / DEPLOY_LOGS.length) * 100));
2133
- currentLogIndex++;
2134
- setTimeout(runStep, Math.random() * 250 + 150);
2135
- } else {
2136
- setStatus('success');
2054
+
2055
+ setPlayerY(playerYRef.current);
2056
+ setObstacleX(obstacleXRef.current);
2137
2057
  }
2058
+
2059
+ animationRef.current = requestAnimationFrame(frame);
2060
+ }
2061
+
2062
+ animationRef.current = requestAnimationFrame(frame);
2063
+ return () => {
2064
+ if (animationRef.current !== null) cancelAnimationFrame(animationRef.current);
2138
2065
  };
2139
- setTimeout(runStep, 400);
2140
- };
2066
+ }, [bestScore, lowLatencyMode, obstacleHeight, obstacleWidth, resetObstacle, simSpeed]);
2141
2067
 
2142
- const resetDeploy = () => {
2143
- setStatus('idle');
2144
- setLogs([]);
2145
- setProgress(0);
2146
- };
2068
+ const obstacleSizeClass =
2069
+ obstacleHeight < 82 ? "KECIL" : obstacleHeight < 120 ? "SEDANG" : "BESAR";
2070
+ const obstaclePos = "BOTTOM" as const;
2147
2071
 
2148
2072
  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>
2073
+ <div className="min-h-screen bg-[#050505] text-white">
2074
+ <div className="scanline" />
2075
+ <main className="mx-auto flex w-full max-w-7xl flex-col gap-10 px-6 py-8 md:px-10">
2076
+ <nav className="flex items-center justify-between border-b border-surface-stroke pb-5">
2077
+ <div className="flex items-center gap-3 font-mono text-sm font-extrabold tracking-tight">
2078
+ <span className="grid h-8 w-8 place-items-center border border-brand-pink bg-brand-pink text-black">
2079
+ R
2080
+ </span>
2081
+ Rakta.js
2163
2082
  </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>
2083
+ <div className="hidden items-center gap-6 font-mono text-[11px] font-bold uppercase tracking-wider text-[#b5b5b5] md:flex">
2084
+ <a href="#features" className="transition hover:text-white">Features</a>
2085
+ <a href="#shrimprun" className="transition hover:text-white">ShrimpRun</a>
2086
+ <a href="#start" className="transition hover:text-white">Start</a>
2087
+ </div>
2088
+ </nav>
2172
2089
 
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">
2090
+ <section className="grid min-h-[540px] items-center gap-10 border-b border-surface-stroke py-12 md:grid-cols-[1.08fr_0.92fr]">
2174
2091
  <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>}
2092
+ <p className="mb-5 font-mono text-[11px] font-bold uppercase tracking-[0.32em] text-brand-pink">
2093
+ THE RED ROUTER FRAMEWORK
2094
+ </p>
2095
+ <h1 className="max-w-4xl text-6xl font-black uppercase leading-[0.88] tracking-normal text-white md:text-8xl">
2096
+ Small in size. Fierce in speed.
2097
+ </h1>
2098
+ <p className="mt-7 max-w-2xl text-sm leading-7 text-[#b5b5b5] md:text-base">
2099
+ Rakta.js is ready. React, Bun, file routes, hot reload, metadata, favicon,
2100
+ and the ShrimpRun starter are already wired for your first route.
2101
+ </p>
2102
+ <div className="mt-8 flex flex-wrap gap-3">
2103
+ <a
2104
+ href="#shrimprun"
2105
+ className="inline-flex h-11 items-center gap-2 border border-brand-pink bg-brand-pink px-5 font-mono text-xs font-extrabold uppercase text-white transition hover:bg-white hover:text-black"
2106
+ >
2107
+ <LuRocket className="h-4 w-4" />
2108
+ Play ShrimpRun
2109
+ </a>
2110
+ <a
2111
+ href="#start"
2112
+ className="inline-flex h-11 items-center gap-2 border border-surface-stroke px-5 font-mono text-xs font-extrabold uppercase text-white transition hover:border-white"
2113
+ >
2114
+ <LuCode2 className="h-4 w-4" />
2115
+ Start Building
2116
+ </a>
2180
2117
  </div>
2181
2118
  </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
2119
 
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>
2120
+ <div className="grid gap-3 border border-surface-stroke bg-surface-card p-5">
2121
+ {[
2122
+ ["Routing", "File-based routes with fast client navigation", LuZap],
2123
+ ["Metadata", "Default title, favicon, and SEO shell included", LuShieldCheck],
2124
+ ["Runtime", "Bun-powered dev server on localhost:3000", LuCpu],
2125
+ ].map(([title, text, Icon]) => (
2126
+ <div key={String(title)} className="flex gap-4 border border-white/5 bg-black p-4">
2127
+ <Icon className="mt-1 h-5 w-5 text-brand-pink" />
2128
+ <div>
2129
+ <h2 className="font-mono text-xs font-extrabold uppercase">{title}</h2>
2130
+ <p className="mt-1 text-xs leading-5 text-[#b5b5b5]">{text}</p>
2131
+ </div>
2132
+ </div>
2133
+ ))}
2229
2134
  </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 = '';
2135
+ </section>
2136
+
2137
+ <section id="features" className="grid grid-cols-1 gap-0 border border-surface-stroke md:grid-cols-3">
2138
+ {[
2139
+ ["01", "HOT RELOAD", "Pages, components, styles, layouts, stores, and route updates refresh immediately.", LuActivity],
2140
+ ["02", "REACT ICONS", "Starter icons use react-icons/lu only. No lucide-react dependency ships.", LuCode2],
2141
+ ["03", "LOCAL FIRST", "The CLI prints and serves localhost:3000 for the frontend starter.", LuGauge],
2142
+ ].map(([number, title, text, Icon]) => (
2143
+ <div key={String(title)} className="border-b border-surface-stroke p-7 md:border-b-0 md:border-r last:md:border-r-0">
2144
+ <span className="font-mono text-[10px] font-bold text-brand-pink">{number}</span>
2145
+ <Icon className="my-8 h-6 w-6 text-white" />
2146
+ <h2 className="font-mono text-xl font-extrabold uppercase">{title}</h2>
2147
+ <p className="mt-3 text-xs leading-6 text-[#b5b5b5]">{text}</p>
2148
+ </div>
2149
+ ))}
2150
+ </section>
2260
2151
 
2261
- const finalRotation = rotation !== undefined ? rotation : (isDead ? 180 : 0);
2262
- const shrimpColor = isDead ? 'fill-orange-400' : 'fill-rose-500';
2152
+ <section id="shrimprun" className="grid gap-5">
2153
+ <div>
2154
+ <p className="font-mono text-[10px] font-bold uppercase tracking-[0.28em] text-brand-pink">
2155
+ SHRIMPRUN SIMULATION
2156
+ </p>
2157
+ <h2 className="mt-3 text-4xl font-black uppercase md:text-6xl">
2158
+ Avoid the coral.
2159
+ </h2>
2160
+ </div>
2263
2161
 
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";
2162
+ <button
2163
+ type="button"
2164
+ onClick={jump}
2165
+ className="relative h-[280px] overflow-hidden border-2 border-surface-stroke bg-black text-left outline-none transition focus-visible:border-brand-pink"
2166
+ aria-label="ShrimpRun game area. Press Space or click to jump."
2167
+ >
2168
+ <div className="bg-grid-glow absolute inset-0 opacity-80" />
2169
+ <div className="absolute inset-x-0 bottom-0 h-12 border-t border-cyan-400/20 bg-cyan-400/5" />
2170
+ <div className="seaweed-waving-left-1 absolute bottom-8 left-9 h-16 w-3 bg-emerald-500/70" />
2171
+ <div className="seaweed-waving-left-2 absolute bottom-8 left-16 h-24 w-3 bg-cyan-500/60" />
2172
+ <div className="seaweed-waving-right-1 absolute bottom-8 right-20 h-20 w-3 bg-emerald-500/70" />
2173
+
2174
+ <div className="absolute left-6 top-5 z-20 flex gap-4 font-mono text-[11px] font-bold uppercase">
2175
+ <span>Score: {score}</span>
2176
+ <span className="text-brand-pink">Best: {bestScore}</span>
2177
+ <span>{status}</span>
2178
+ </div>
2290
2179
 
2291
- export default function WelcomePage() {
2292
- const [modalOpen, setModalOpen] = useState<'components' | 'deploy' | 'docs' | null>(null);
2180
+ <div
2181
+ className="absolute z-20"
2182
+ style={{ left: SHRIMP_X, bottom: 48 + playerY }}
2183
+ >
2184
+ <ShrimpCharacter status={status} playerY={0} rotation={rotation} />
2185
+ </div>
2293
2186
 
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>
2187
+ <div className="absolute z-10" style={{ left: obstacleX, bottom: 48 }}>
2188
+ <CoralObstacle
2189
+ position={obstaclePos}
2190
+ height={obstacleHeight}
2191
+ width={obstacleWidth}
2192
+ paletteIndex={obstaclePalette}
2193
+ variant={obstacleVariant}
2194
+ scaleX={obstacleScaleX}
2195
+ />
2196
+ </div>
2309
2197
 
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>
2198
+ {status === "IDLE" && (
2199
+ <div className="absolute inset-0 z-30 grid place-items-center bg-black/55 font-mono text-xs font-bold uppercase tracking-[0.2em] text-white">
2200
+ Press Space or click to start
2201
+ </div>
2202
+ )}
2203
+ {status === "DEAD" && (
2204
+ <div className="absolute inset-0 z-30 grid place-items-center bg-black/75 text-center font-mono">
2205
+ <div>
2206
+ <p className="text-3xl font-black text-brand-pink">SYSTEM IMPACT</p>
2207
+ <p className="mt-2 text-xs uppercase text-[#b5b5b5]">Click to reboot the run</p>
2208
+ </div>
2209
+ </div>
2210
+ )}
2327
2211
  </button>
2328
2212
 
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>
2213
+ <div className="flex flex-col justify-between gap-4 font-mono text-[10px] text-zinc-500 md:flex-row md:items-center">
2214
+ <div className="flex flex-wrap items-center gap-3">
2215
+ <span className="font-extrabold text-white">SHRIMPRUN {aestheticUnit.replace("-", " ")}</span>
2216
+ <span>CORAL:</span>
2217
+ <span className="border border-brand-pink/30 bg-brand-pink/5 px-2 py-0.5 font-bold text-brand-pink">
2218
+ {obstacleSizeClass}
2219
+ </span>
2220
+ </div>
2339
2221
 
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>
2222
+ <div className="flex flex-wrap items-center gap-3">
2223
+ {(["LENIS-MODERN", "RETRO-CYBER", "NEO-BRUTALIST"] as AestheticUnit[]).map((unit) => (
2224
+ <button
2225
+ type="button"
2226
+ key={unit}
2227
+ onClick={() => {
2228
+ setAestheticUnit(unit);
2229
+ showConfigToast(\`STYLE SET: \${unit}\`);
2230
+ playScoreSound();
2231
+ }}
2232
+ className={\`px-2 py-1 font-bold transition \${aestheticUnit === unit ? "bg-white text-black" : "border border-surface-stroke text-zinc-500 hover:text-white"}\`}
2233
+ >
2234
+ {unit}
2235
+ </button>
2236
+ ))}
2237
+ {(["NORMAL", "FAST", "TURBO"] as const).map((speed) => (
2238
+ <button
2239
+ type="button"
2240
+ key={speed}
2241
+ onClick={() => {
2242
+ setSimSpeed(speed);
2243
+ showConfigToast(\`SPEED SET: \${speed}\`);
2244
+ playScoreSound();
2245
+ }}
2246
+ className={\`px-2 py-1 font-bold transition \${simSpeed === speed ? "bg-brand-pink text-white" : "border border-surface-stroke text-zinc-500 hover:text-white"}\`}
2247
+ >
2248
+ {speed}
2249
+ </button>
2250
+ ))}
2251
+ <button
2252
+ type="button"
2253
+ onClick={() => {
2254
+ setLowLatencyMode((value) => !value);
2255
+ showConfigToast(\`LATENCY MODE: \${!lowLatencyMode ? "LOW" : "STANDARD"}\`);
2256
+ playJumpSound();
2257
+ }}
2258
+ className={\`px-2 py-1 font-bold transition \${lowLatencyMode ? "bg-cyan-400 text-black" : "border border-surface-stroke text-zinc-500"}\`}
2259
+ >
2260
+ LOW LATENCY {lowLatencyMode ? "ON" : "OFF"}
2261
+ </button>
2262
+ </div>
2263
+ </div>
2343
2264
 
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>
2265
+ {configToast && (
2266
+ <div className="fixed bottom-8 left-1/2 z-50 -translate-x-1/2 border border-brand-pink bg-black px-4 py-2 font-mono text-[10px] font-bold uppercase tracking-widest text-brand-pink">
2267
+ {configToast}
2268
+ </div>
2269
+ )}
2270
+ </section>
2271
+
2272
+ <section id="start" className="border-t border-surface-stroke py-10">
2273
+ <p className="font-mono text-[10px] font-bold uppercase tracking-[0.28em] text-brand-pink">
2274
+ NEXT MOVE
2275
+ </p>
2276
+ <h2 className="mt-3 text-3xl font-black uppercase md:text-5xl">Edit app/page and ship.</h2>
2277
+ <p className="mt-4 max-w-2xl text-sm leading-7 text-[#b5b5b5]">
2278
+ This starter is intentionally immediate: no popups, no placeholder flow,
2279
+ and no hidden modal dependencies. Your first route is already alive.
2280
+ </p>
2281
+ </section>
2282
+ </main>
2283
+ </div>
2348
2284
  );
2349
2285
  }
2350
2286
  `;
2351
2287
 
2288
+ // src/types.ts
2289
+ var CSS_DISPLAY = {
2290
+ tailwind: "Tailwind CSS",
2291
+ bootstrap: "Bootstrap",
2292
+ sass: "SASS",
2293
+ none: "None"
2294
+ };
2295
+ var BACKEND_DISPLAY = {
2296
+ gaman: "Gaman.js",
2297
+ express: "Express.js",
2298
+ nest: "Nest.js",
2299
+ adonis: "Adonis.js"
2300
+ };
2301
+ var DATABASE_DISPLAY = {
2302
+ postgresql: "PostgreSQL",
2303
+ mysql: "MySQL",
2304
+ mongodb: "MongoDB",
2305
+ firebase: "Firebase",
2306
+ sqlite: "SQLite",
2307
+ mariadb: "MariaDB",
2308
+ redis: "Redis",
2309
+ planetscale: "PlanetScale",
2310
+ neon: "Neon",
2311
+ turso: "Turso"
2312
+ };
2313
+ var RENDER_MODE_DISPLAY = {
2314
+ csr: "CSR \u2014 Client-Side Rendering",
2315
+ ssr: "SSR \u2014 Server-Side Rendering",
2316
+ ssg: "SSG \u2014 Static Site Generation",
2317
+ csg: "CSG \u2014 Configurable Static Generation",
2318
+ spa: "SPA \u2014 Single Page Application",
2319
+ hybrid: "Hybrid \u2014 Mixed per-route"
2320
+ };
2321
+ var PROJECT_MODE_DISPLAY = {
2322
+ fullstack: "Fullstack app (frontend + backend + database)",
2323
+ "frontend-only": "Frontend only (no backend, no database)"
2324
+ };
2325
+ var PROJECT_LANGUAGE_DISPLAY = {
2326
+ typescript: "TypeScript / TSX",
2327
+ javascript: "JavaScript / JSX"
2328
+ };
2329
+
2352
2330
  // src/generator.ts
2353
2331
  var DEFAULT_METADATA_TITLE = "Rakta.js | Small in size. Fierce in speed. Alive in every route";
2354
2332
  var FAVICON_BYTES = readFileSync(new URL("../assets/favicon.ico", import.meta.url));
2355
2333
  function getRootFiles(projectConfig) {
2356
- const { projectName, projectMode } = projectConfig;
2334
+ const { projectName, projectMode, useTypeScript } = projectConfig;
2357
2335
  const workspaces = projectMode === "fullstack" ? ["frontend", "backend", "shared"] : [];
2358
- return [
2336
+ const files = [
2359
2337
  {
2360
2338
  path: "package.json",
2361
2339
  content: JSON.stringify({
@@ -2370,36 +2348,16 @@ function getRootFiles(projectConfig) {
2370
2348
  "build:backend": "cd backend && bun run build",
2371
2349
  build: "bun run build:frontend && bun run build:backend",
2372
2350
  start: "cd backend && bun run start",
2373
- typecheck: "cd frontend && bun run typecheck && cd ../backend && bun run typecheck"
2351
+ ...useTypeScript ? {
2352
+ typecheck: "cd frontend && bun run typecheck && cd ../backend && bun run typecheck"
2353
+ } : {}
2374
2354
  } : {
2375
2355
  dev: "rakta dev",
2376
2356
  build: "rakta build",
2377
2357
  start: "rakta start",
2378
- typecheck: "tsc --noEmit"
2379
- },
2380
- description: `${projectName} \u2014 built with Rakta.js`
2381
- }, null, 2)
2382
- },
2383
- {
2384
- path: "tsconfig.base.json",
2385
- content: JSON.stringify({
2386
- compilerOptions: {
2387
- target: "ESNext",
2388
- module: "ESNext",
2389
- moduleResolution: "Bundler",
2390
- jsx: "react-jsx",
2391
- lib: ["ESNext", "DOM", "DOM.Iterable"],
2392
- strict: true,
2393
- noUncheckedIndexedAccess: true,
2394
- exactOptionalPropertyTypes: true,
2395
- skipLibCheck: true,
2396
- esModuleInterop: true,
2397
- allowSyntheticDefaultImports: true,
2398
- resolveJsonModule: true,
2399
- verbatimModuleSyntax: true,
2400
- isolatedModules: true
2358
+ ...useTypeScript ? { typecheck: "tsc --noEmit" } : {}
2401
2359
  },
2402
- exclude: ["node_modules", "dist", "**/dist/**"]
2360
+ description: `${projectName} \xE2\u20AC\u201D built with Rakta.js`
2403
2361
  }, null, 2)
2404
2362
  },
2405
2363
  {
@@ -2444,10 +2402,37 @@ dist/
2444
2402
  content: generateProjectReadme(projectConfig)
2445
2403
  }
2446
2404
  ];
2405
+ if (useTypeScript) {
2406
+ files.splice(1, 0, {
2407
+ path: "tsconfig.base.json",
2408
+ content: JSON.stringify({
2409
+ compilerOptions: {
2410
+ target: "ESNext",
2411
+ module: "ESNext",
2412
+ moduleResolution: "Bundler",
2413
+ jsx: "react-jsx",
2414
+ lib: ["ESNext", "DOM", "DOM.Iterable"],
2415
+ strict: true,
2416
+ noUncheckedIndexedAccess: true,
2417
+ exactOptionalPropertyTypes: true,
2418
+ skipLibCheck: true,
2419
+ esModuleInterop: true,
2420
+ allowSyntheticDefaultImports: true,
2421
+ resolveJsonModule: true,
2422
+ verbatimModuleSyntax: true,
2423
+ isolatedModules: true
2424
+ },
2425
+ exclude: ["node_modules", "dist", "**/dist/**"]
2426
+ }, null, 2)
2427
+ });
2428
+ }
2429
+ return files;
2447
2430
  }
2448
2431
  function getFrontendOnlyFiles(projectConfig) {
2449
- const { projectName, cssFramework } = projectConfig;
2432
+ const { projectName, cssFramework, useTypeScript } = projectConfig;
2450
2433
  const styleFileName = cssFramework === "sass" ? "globals.scss" : "globals.css";
2434
+ const pageExtension = useTypeScript ? "tsx" : "jsx";
2435
+ const scriptExtension = useTypeScript ? "ts" : "js";
2451
2436
  const files = [
2452
2437
  {
2453
2438
  path: "package.json",
@@ -2461,49 +2446,27 @@ function getFrontendOnlyFiles(projectConfig) {
2461
2446
  build: "rakta build",
2462
2447
  start: "rakta start",
2463
2448
  routes: "rakta routes",
2464
- typecheck: "tsc --noEmit"
2449
+ ...useTypeScript ? { typecheck: "tsc --noEmit" } : {}
2465
2450
  },
2466
2451
  dependencies: {
2467
- raktajs: "^0.1.7",
2452
+ raktajs: "^0.1.9",
2468
2453
  react: "^19.2.7",
2469
2454
  "react-dom": "^19.2.7",
2470
- "react-icons": "^5.2.1",
2471
- motion: "^12.0.0",
2455
+ "react-icons": "^5.7.0",
2472
2456
  ...getCssDependencies(cssFramework)
2473
2457
  },
2474
- devDependencies: {
2458
+ devDependencies: useTypeScript ? {
2475
2459
  "@types/react": "^19.2.17",
2476
2460
  "@types/react-dom": "^19.2.3",
2477
2461
  typescript: "^6.0.3",
2478
2462
  ...getCssDevDependencies(cssFramework)
2463
+ } : {
2464
+ ...getCssDevDependencies(cssFramework)
2479
2465
  }
2480
2466
  }, null, 2)
2481
2467
  },
2482
2468
  {
2483
- path: "tsconfig.json",
2484
- content: JSON.stringify({
2485
- extends: "./tsconfig.base.json",
2486
- compilerOptions: {
2487
- outDir: "./dist",
2488
- rootDir: "./",
2489
- types: ["react", "react-dom"]
2490
- },
2491
- include: [
2492
- "rakta-env.d.ts",
2493
- "app/**/*",
2494
- "components/**/*",
2495
- "styles/**/*",
2496
- "rakta.config.ts"
2497
- ],
2498
- exclude: ["node_modules", "dist"]
2499
- }, null, 2)
2500
- },
2501
- {
2502
- path: "rakta-env.d.ts",
2503
- content: generateFrontendOnlyRaktaEnv()
2504
- },
2505
- {
2506
- path: "rakta.config.ts",
2469
+ path: `rakta.config.${scriptExtension}`,
2507
2470
  content: `import { defineRaktaConfig } from "raktajs";
2508
2471
 
2509
2472
  export default defineRaktaConfig({
@@ -2520,44 +2483,40 @@ export default defineRaktaConfig({
2520
2483
  `
2521
2484
  },
2522
2485
  {
2523
- path: "app/layout.tsx",
2486
+ path: `app/layout.${pageExtension}`,
2524
2487
  content: generateFrontendOnlyLayout()
2525
2488
  },
2526
2489
  {
2527
- path: "app/page.tsx",
2490
+ path: `app/page.${pageExtension}`,
2528
2491
  content: generateFrontendOnlyPage(projectName)
2529
2492
  },
2530
2493
  {
2531
- path: "app/loading.tsx",
2494
+ path: `app/loading.${pageExtension}`,
2532
2495
  content: generateFrontendOnlyLoading()
2533
2496
  },
2534
2497
  {
2535
- path: "app/error.tsx",
2498
+ path: `app/error.${pageExtension}`,
2536
2499
  content: generateFrontendOnlyError()
2537
2500
  },
2538
2501
  {
2539
- path: "app/notFound.tsx",
2502
+ path: `app/notFound.${pageExtension}`,
2540
2503
  content: generateFrontendOnlyNotFound()
2541
2504
  },
2542
2505
  {
2543
- path: "app/components/ComponentsModal.tsx",
2544
- content: COMPONENTS_MODAL_CODE
2545
- },
2546
- {
2547
- path: "app/components/CoralObstacle.tsx",
2548
- content: CORAL_OBSTACLE_CODE
2506
+ path: `app/components/CoralObstacle.${pageExtension}`,
2507
+ content: STARTER_CORAL_OBSTACLE_CODE
2549
2508
  },
2550
2509
  {
2551
- path: "app/components/DeployModal.tsx",
2552
- content: DEPLOY_MODAL_CODE
2510
+ path: `app/components/ShrimpCharacter.${pageExtension}`,
2511
+ content: STARTER_SHRIMP_CHARACTER_CODE
2553
2512
  },
2554
2513
  {
2555
- path: "app/components/DocsModal.tsx",
2556
- content: DOCS_MODAL_CODE
2514
+ path: `app/utils/audio.${scriptExtension}`,
2515
+ content: STARTER_AUDIO_CODE
2557
2516
  },
2558
2517
  {
2559
- path: "app/components/ShrimpCharacter.tsx",
2560
- content: SHRIMP_CHARACTER_CODE
2518
+ path: `app/types.${scriptExtension}`,
2519
+ content: STARTER_TYPES_CODE
2561
2520
  },
2562
2521
  {
2563
2522
  path: `styles/${styleFileName}`,
@@ -2572,12 +2531,36 @@ export default defineRaktaConfig({
2572
2531
  content: FAVICON_BYTES
2573
2532
  }
2574
2533
  ];
2575
- return processFilesForLanguage(files, projectConfig.useTypeScript);
2534
+ if (useTypeScript) {
2535
+ files.splice(1, 0, {
2536
+ path: "tsconfig.json",
2537
+ content: JSON.stringify({
2538
+ extends: "./tsconfig.base.json",
2539
+ compilerOptions: {
2540
+ outDir: "./dist",
2541
+ rootDir: "./",
2542
+ types: ["react", "react-dom"]
2543
+ },
2544
+ include: [
2545
+ "rakta-env.d.ts",
2546
+ "app/**/*",
2547
+ "components/**/*",
2548
+ "styles/**/*",
2549
+ "rakta.config.ts"
2550
+ ],
2551
+ exclude: ["node_modules", "dist"]
2552
+ }, null, 2)
2553
+ }, {
2554
+ path: "rakta-env.d.ts",
2555
+ content: generateFrontendOnlyRaktaEnv()
2556
+ });
2557
+ }
2558
+ return processFilesForLanguage(files, useTypeScript);
2576
2559
  }
2577
2560
  function getFullstackFrontendFiles(projectConfig) {
2578
2561
  const { projectName, cssFramework } = projectConfig;
2579
2562
  const styleFileName = cssFramework === "sass" ? "globals.scss" : "globals.css";
2580
- const files = [
2563
+ return [
2581
2564
  {
2582
2565
  path: "frontend/package.json",
2583
2566
  content: JSON.stringify({
@@ -2594,11 +2577,9 @@ function getFullstackFrontendFiles(projectConfig) {
2594
2577
  typecheck: "tsc --noEmit"
2595
2578
  },
2596
2579
  dependencies: {
2597
- raktajs: "^0.1.7",
2580
+ raktajs: "^0.1.9",
2598
2581
  react: "^19.2.7",
2599
2582
  "react-dom": "^19.2.7",
2600
- "react-icons": "^5.2.1",
2601
- motion: "^12.0.0",
2602
2583
  ...getCssDependencies(cssFramework)
2603
2584
  },
2604
2585
  devDependencies: {
@@ -2636,7 +2617,7 @@ export default defineRaktaConfig({
2636
2617
  appName: "${projectName}",
2637
2618
  seo: {
2638
2619
  defaultTitle: "${DEFAULT_METADATA_TITLE}",
2639
- defaultDescription: "Built with Rakta.js \u2014 Small in size. Fierce in speed. Alive in every route.",
2620
+ defaultDescription: "Built with Rakta.js \xE2\u20AC\u201D Small in size. Fierce in speed. Alive in every route.",
2640
2621
  },
2641
2622
  render: {
2642
2623
  defaultMode: "csr",
@@ -2892,7 +2873,7 @@ export type UserSchema = typeof userSchema;
2892
2873
  },
2893
2874
  {
2894
2875
  path: `frontend/styles/${styleFileName}`,
2895
- content: getCssGlobals(cssFramework)
2876
+ content: getFrontendOnlyCssGlobals(cssFramework)
2896
2877
  },
2897
2878
  {
2898
2879
  path: "frontend/public/.gitkeep",
@@ -2903,46 +2884,35 @@ export type UserSchema = typeof userSchema;
2903
2884
  content: FAVICON_BYTES
2904
2885
  },
2905
2886
  {
2906
- path: "frontend/components/ComponentsModal.tsx",
2907
- content: COMPONENTS_MODAL_CODE
2908
- },
2909
- {
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
2887
+ path: "frontend/components/ui/.gitkeep",
2888
+ content: ""
2920
2889
  },
2921
2890
  {
2922
- path: "frontend/components/ShrimpCharacter.tsx",
2923
- content: SHRIMP_CHARACTER_CODE
2891
+ path: "frontend/components/layout/.gitkeep",
2892
+ content: ""
2924
2893
  }
2925
2894
  ];
2926
- return processFilesForLanguage(files, projectConfig.useTypeScript);
2895
+ }
2896
+ function stripTypeScriptSyntax(code) {
2897
+ return code.replace(/^import\s+type\s+.+;\r?\n/gm, "").replace(/^export\s+interface\s+\w+\s*\{[\s\S]*?\r?\n\}\r?\n/gm, "").replace(/^interface\s+\w+\s*\{[\s\S]*?\r?\n\}\r?\n/gm, "").replace(/^export\s+type\s+\w+\s*=[\s\S]*?;\r?\n/gm, "").replace(/^type\s+\w+\s*=[\s\S]*?;\r?\n/gm, "").replace(/<("[^"]+"|'[^']+'|\w+)(\s*\|\s*("[^"]+"|'[^']+'|\w+))*\s*>/g, "").replace(/\s+as\s+const/g, "").replace(/\s+as\s+[A-Za-z0-9_<>,[\]\s|&".']+/g, "").replace(/\breadonly\s+/g, "").replace(/(\(|,|\{)\s*([A-Za-z_$][\w$]*)\s*:\s*[A-Za-z_$][\w$]*(?:\[\])?(?:\s*\|\s*[A-Za-z_$][\w$]*(?:\[\])?)*/g, "$1 $2").replace(/\)\s*:\s*(?:void|string|number|boolean|Promise<[^>]+>|[A-Za-z_$][\w$]*(?:\[\])?)/g, ")").replace(/(const|let)\s+([A-Za-z_$][\w$]*)\s*:\s*[^=;]+=/g, "$1 $2 =");
2927
2898
  }
2928
2899
  function processFilesForLanguage(files, useTypeScript) {
2929
- if (useTypeScript)
2900
+ if (useTypeScript) {
2930
2901
  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 };
2902
+ }
2903
+ return files.filter((file) => !file.path.endsWith(".d.ts")).map((file) => {
2904
+ if (typeof file.content !== "string") {
2905
+ return file;
2944
2906
  }
2945
- return file;
2907
+ let path = file.path;
2908
+ if (path.endsWith(".tsx"))
2909
+ path = path.replace(/\.tsx$/, ".jsx");
2910
+ else if (path.endsWith(".ts"))
2911
+ path = path.replace(/\.ts$/, ".js");
2912
+ return {
2913
+ path,
2914
+ content: stripTypeScriptSyntax(file.content)
2915
+ };
2946
2916
  });
2947
2917
  }
2948
2918
  function getBackendFiles(projectConfig) {
@@ -3367,430 +3337,98 @@ function getCssDevDependencies(cssFramework) {
3367
3337
  }
3368
3338
  }
3369
3339
  function getFrontendOnlyCssGlobals(cssFramework) {
3370
- const cssImport = cssFramework === "tailwind" ? `@import "tailwindcss";
3340
+ const cssImport = cssFramework === "tailwind" ? `@import url("https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600;700;800&display=swap");
3341
+ @import "tailwindcss";
3371
3342
 
3372
3343
  ` : cssFramework === "bootstrap" ? `@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css");
3373
3344
 
3374
- ` : cssFramework === "sass" ? `$color-primary: #dc2626;
3345
+ ` : cssFramework === "sass" ? `$color-primary: #e11d48;
3375
3346
  $color-background: #050505;
3376
- $color-foreground: #f8fafc;
3347
+ $color-foreground: #fafafa;
3377
3348
 
3378
3349
  ` : "";
3379
- return `${cssImport}:root {
3380
- --color-primary: #dc2626;
3381
- --color-background: #050505;
3382
- --color-foreground: #f8fafc;
3383
- --color-surface: #0e111a;
3384
- --color-border: rgba(255, 255, 255, 0.1);
3385
- --color-muted: #94a3b8;
3386
- color-scheme: dark;
3350
+ return `${cssImport}@theme {
3351
+ --font-sans: "Geist", ui-sans-serif, system-ui, sans-serif;
3352
+ --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
3353
+ --color-brand-pink: #e11d48;
3354
+ --color-brand-green: #00ff00;
3355
+ --color-surface-bg: #000000;
3356
+ --color-surface-card: #0d0d0d;
3357
+ --color-surface-stroke: #1f1f1f;
3387
3358
  }
3388
3359
 
3389
- html {
3390
- scroll-behavior: smooth;
3360
+ :root {
3361
+ color-scheme: dark;
3362
+ background: #050505;
3363
+ color: #fafafa;
3364
+ font-family: var(--font-sans);
3391
3365
  }
3392
3366
 
3367
+ * { box-sizing: border-box; }
3368
+ html { scroll-behavior: smooth; }
3393
3369
  body {
3394
3370
  margin: 0;
3395
3371
  min-width: 320px;
3396
3372
  min-height: 100vh;
3397
- color: var(--color-foreground);
3398
- background: var(--color-background);
3399
- font-family:
3400
- ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
3401
- Roboto, sans-serif;
3373
+ background: #050505;
3374
+ color: #fafafa;
3375
+ overflow-x: hidden;
3402
3376
  -webkit-font-smoothing: antialiased;
3403
3377
  }
3378
+ button, a { font: inherit; }
3379
+ a { color: inherit; text-decoration: none; }
3380
+ #rakta-root { min-height: 100vh; }
3404
3381
 
3405
- *,
3406
- *::before,
3407
- *::after {
3408
- box-sizing: border-box;
3409
- }
3410
-
3411
- @keyframes shrimpLegs {
3412
- 0% {
3413
- transform: translateY(0);
3414
- }
3415
-
3416
- 50% {
3417
- transform: translateY(2px);
3418
- }
3419
- }
3420
-
3421
- #rakta-root {
3422
- min-height: 100vh;
3423
- }
3424
-
3425
- main {
3426
- width: min(1120px, calc(100% - 32px));
3427
- min-height: 100vh;
3428
- margin: 0 auto;
3429
- padding: 56px 0;
3430
- }
3431
-
3432
- section {
3433
- margin: 0 0 24px;
3434
- border: 1px solid rgba(248, 113, 113, 0.18);
3435
- border-radius: 18px;
3436
- background:
3437
- linear-gradient(135deg, rgba(220, 38, 38, 0.16), transparent 34%),
3438
- #0e111a;
3439
- box-shadow: 0 24px 80px rgba(127, 29, 29, 0.2);
3440
- padding: clamp(24px, 4vw, 44px);
3441
- }
3442
-
3443
- p {
3444
- margin: 0 0 12px;
3445
- color: #cbd5e1;
3446
- line-height: 1.7;
3447
- }
3448
-
3449
- h1,
3450
- h2 {
3451
- margin: 0 0 12px;
3452
- color: #ffffff;
3453
- line-height: 1;
3454
- }
3455
-
3456
- h1 {
3457
- max-width: 760px;
3458
- font-size: clamp(42px, 8vw, 84px);
3459
- letter-spacing: 0;
3460
- }
3461
-
3462
- h2 {
3463
- font-size: clamp(28px, 4vw, 44px);
3464
- }
3465
-
3466
- click {
3467
- display: inline-flex;
3468
- align-items: center;
3469
- justify-content: center;
3470
- min-height: 42px;
3471
- margin: 16px 12px 0 0;
3472
- border: 1px solid rgba(248, 113, 113, 0.42);
3473
- border-radius: 12px;
3474
- padding: 0 16px;
3475
- color: #fecaca;
3476
- font-weight: 700;
3477
- text-decoration: none;
3478
- cursor: pointer;
3479
- transition:
3480
- transform 160ms ease,
3481
- border-color 160ms ease,
3482
- background 160ms ease;
3483
- }
3484
-
3485
- click:hover {
3486
- border-color: #f87171;
3487
- background: rgba(220, 38, 38, 0.12);
3488
- transform: translateY(-1px);
3489
- }
3490
-
3491
- button {
3492
- font: inherit;
3493
- }
3494
-
3495
- .relative {
3496
- position: relative;
3497
- }
3498
-
3499
- .absolute {
3500
- position: absolute;
3501
- }
3502
-
3503
- .inset-0 {
3504
- inset: 0;
3505
- }
3382
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
3383
+ ::-webkit-scrollbar-track { background: #000000; }
3384
+ ::-webkit-scrollbar-thumb { background: #e11d48; border-radius: 0; }
3385
+ ::-webkit-scrollbar-thumb:hover { background: #be123c; }
3506
3386
 
3507
- .bottom-0 {
3508
- bottom: 0;
3387
+ @keyframes scanline {
3388
+ 0% { transform: translateY(-100%); }
3389
+ 100% { transform: translateY(100vh); }
3509
3390
  }
3510
-
3511
- .left-0 {
3391
+ .scanline {
3392
+ position: fixed;
3393
+ top: 0;
3512
3394
  left: 0;
3513
- }
3514
-
3515
- .top-2 {
3516
- top: 8px;
3517
- }
3518
-
3519
- .right-3 {
3520
- right: 12px;
3521
- }
3522
-
3523
- .block {
3524
- display: block;
3525
- }
3526
-
3527
- .flex {
3528
- display: flex;
3529
- }
3530
-
3531
- .flex-col {
3532
- flex-direction: column;
3533
- }
3534
-
3535
- .flex-wrap {
3536
- flex-wrap: wrap;
3537
- }
3538
-
3539
- .items-center {
3540
- align-items: center;
3541
- }
3542
-
3543
- .items-start {
3544
- align-items: flex-start;
3545
- }
3546
-
3547
- .justify-center {
3548
- justify-content: center;
3549
- }
3550
-
3551
- .gap-2 {
3552
- gap: 8px;
3553
- }
3554
-
3555
- .gap-4 {
3556
- gap: 16px;
3557
- }
3558
-
3559
- .gap-8 {
3560
- gap: 32px;
3561
- }
3562
-
3563
- .w-full {
3395
+ z-index: 999;
3564
3396
  width: 100%;
3565
- }
3566
-
3567
- .max-w-full {
3568
- max-width: 100%;
3569
- }
3570
-
3571
- .overflow-hidden {
3572
- overflow: hidden;
3573
- }
3574
-
3575
- .select-none {
3576
- user-select: none;
3577
- }
3578
-
3579
- .cursor-pointer {
3580
- cursor: pointer;
3581
- }
3582
-
3583
- .text-left {
3584
- text-align: left;
3585
- }
3586
-
3587
- .text-sm {
3588
- font-size: 14px;
3589
- }
3590
-
3591
- .text-lg {
3592
- font-size: 18px;
3593
- }
3594
-
3595
- .text-xl {
3596
- font-size: 20px;
3597
- }
3598
-
3599
- .font-bold {
3600
- font-weight: 800;
3601
- }
3602
-
3603
- .font-semibold {
3604
- font-weight: 700;
3605
- }
3606
-
3607
- .font-mono {
3608
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
3609
- }
3610
-
3611
- .text-white {
3612
- color: #fff;
3613
- }
3614
-
3615
- .text-slate-400 {
3616
- color: #94a3b8;
3617
- }
3618
-
3619
- .text-red-600 {
3620
- color: #dc2626;
3621
- }
3622
-
3623
- .bg-red-600 {
3624
- background: #dc2626;
3625
- }
3626
-
3627
- .bg-black\\/75 {
3628
- background: rgba(0, 0, 0, 0.75);
3629
- }
3630
-
3631
- .pointer-events-none {
3397
+ height: 120px;
3632
3398
  pointer-events: none;
3399
+ background: linear-gradient(0deg, rgba(225, 29, 72, 0.08) 0%, rgba(225, 29, 72, 0) 100%);
3400
+ opacity: 0.8;
3401
+ animation: scanline 8s linear infinite;
3633
3402
  }
3634
3403
 
3635
- .rounded-sm {
3636
- border-radius: 2px;
3637
- }
3638
-
3639
- .rounded-lg {
3640
- border-radius: 8px;
3641
- }
3642
-
3643
- .min-h-5 {
3644
- min-height: 20px;
3645
- }
3646
-
3647
- .tabular-nums {
3648
- font-variant-numeric: tabular-nums;
3649
- }
3650
-
3651
- button[aria-label^="ShrimpRun"] {
3652
- border: 2px solid rgba(248, 113, 113, 0.28);
3653
- border-radius: 16px;
3654
- background:
3655
- linear-gradient(180deg, rgba(15, 23, 42, 0.96), rgba(7, 10, 18, 0.98)),
3656
- radial-gradient(circle at 18% 22%, rgba(248, 113, 113, 0.16), transparent 30%);
3657
- box-shadow:
3658
- inset 0 0 0 1px rgba(255, 255, 255, 0.04),
3659
- 0 20px 60px rgba(0, 0, 0, 0.28);
3660
- }
3661
-
3662
- button[aria-label^="ShrimpRun"] > .bottom-0.left-0 {
3663
- background: linear-gradient(90deg, #7f1d1d, #dc2626, #fecaca);
3664
- }
3665
-
3666
- button:not([aria-label]) {
3667
- border: 0;
3668
- border-radius: 10px;
3669
- background: #dc2626;
3670
- color: #fff;
3671
- cursor: pointer;
3672
- font-weight: 800;
3673
- }
3674
-
3675
- .shrimp-sprite {
3676
- filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.32));
3404
+ .bg-grid-glow {
3405
+ background-size: 40px 40px;
3406
+ background-image:
3407
+ linear-gradient(to right, rgba(255, 255, 255, 0.04) 1px, transparent 1px),
3408
+ linear-gradient(to bottom, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
3677
3409
  }
3678
3410
 
3679
- .shrimp-run-obstacle {
3680
- border-radius: 8px 8px 2px 2px;
3681
- background:
3682
- radial-gradient(circle at 50% 8px, #fecaca 0 4px, transparent 5px),
3683
- linear-gradient(135deg, #fb7185 0%, #dc2626 46%, #7f1d1d 100%);
3684
- box-shadow:
3685
- inset -6px -8px 0 rgba(69, 10, 10, 0.32),
3686
- 0 0 0 2px rgba(254, 202, 202, 0.18);
3411
+ @keyframes seaweed-wave-1 {
3412
+ 0% { transform: skewX(-14deg) rotate(-8deg) scaleY(0.96); }
3413
+ 50% { transform: skewX(0deg) rotate(0deg) scaleY(1.04); }
3414
+ 100% { transform: skewX(14deg) rotate(8deg) scaleY(0.96); }
3687
3415
  }
3688
-
3689
- .shrimp-run-obstacle::before,
3690
- .shrimp-run-obstacle::after {
3691
- position: absolute;
3692
- bottom: 8px;
3693
- width: 10px;
3694
- height: 22px;
3695
- border-radius: 999px 999px 4px 4px;
3696
- background: #ef4444;
3697
- content: "";
3698
- }
3699
-
3700
- .shrimp-run-obstacle::before {
3701
- left: -7px;
3702
- transform: rotate(-18deg);
3703
- }
3704
-
3705
- .shrimp-run-obstacle::after {
3706
- right: -7px;
3707
- transform: rotate(18deg);
3708
- }
3709
-
3710
- @media (max-width: 720px) {
3711
- main {
3712
- width: min(100% - 20px, 1120px);
3713
- padding: 24px 0;
3714
- }
3715
-
3716
- section {
3717
- padding: 20px;
3718
- }
3719
- }
3720
- `;
3721
- }
3722
- function getCssGlobals(cssFramework) {
3723
- const sharedBase = `
3724
- :root {
3725
- --color-primary: #dc2626;
3726
- --color-background: #050505;
3727
- --color-foreground: #f8fafc;
3728
- }
3729
-
3730
- * {
3731
- box-sizing: border-box;
3416
+ @keyframes seaweed-wave-2 {
3417
+ 0% { transform: skewX(10deg) rotate(6deg) scaleY(1.04); }
3418
+ 50% { transform: skewX(-2deg) rotate(-2deg) scaleY(0.96); }
3419
+ 100% { transform: skewX(-10deg) rotate(-6deg) scaleY(1.04); }
3732
3420
  }
3733
-
3734
- body {
3735
- margin: 0;
3736
- color: var(--color-foreground);
3737
- background: var(--color-background);
3738
- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3739
- }
3740
-
3741
- .page-shell {
3742
- width: min(100% - 2rem, 960px);
3743
- margin: 0 auto;
3744
- padding: 4rem 0;
3745
- }
3746
-
3747
- .hero-card,
3748
- .feature-card {
3749
- border: 1px solid rgba(255, 255, 255, 0.1);
3750
- background: #0e111a;
3751
- border-radius: 24px;
3752
- padding: 2rem;
3753
- margin-bottom: 1.5rem;
3754
- }
3755
-
3756
- .eyebrow {
3757
- color: #dc2626;
3758
- font-weight: 700;
3759
- letter-spacing: 0.12em;
3760
- font-size: 0.75rem;
3761
- text-transform: uppercase;
3762
- }
3763
-
3764
- .button-row {
3765
- display: flex;
3766
- gap: 0.75rem;
3767
- flex-wrap: wrap;
3768
- margin-top: 1rem;
3769
- }
3770
-
3771
- a {
3772
- color: #dc2626;
3421
+ .seaweed-waving-left-1,
3422
+ .seaweed-waving-right-1 {
3423
+ transform-origin: bottom center !important;
3424
+ animation: seaweed-wave-1 3.2s infinite ease-in-out alternate !important;
3773
3425
  }
3774
-
3775
- button {
3776
- cursor: pointer;
3426
+ .seaweed-waving-left-2,
3427
+ .seaweed-waving-right-2 {
3428
+ transform-origin: bottom center !important;
3429
+ animation: seaweed-wave-2 3.8s infinite ease-in-out alternate !important;
3777
3430
  }
3778
3431
  `;
3779
- switch (cssFramework) {
3780
- case "tailwind":
3781
- return `@import "tailwindcss";
3782
- ${sharedBase}`;
3783
- case "bootstrap":
3784
- return `@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css");
3785
- ${sharedBase}`;
3786
- case "sass":
3787
- return `$color-primary: #dc2626;
3788
- $color-background: #050505;
3789
- $color-foreground: #f8fafc;
3790
- ${sharedBase}`;
3791
- case "none":
3792
- return sharedBase;
3793
- }
3794
3432
  }
3795
3433
  function generateFrontendOnlyRaktaEnv() {
3796
3434
  return `declare module "*.css";
@@ -3810,17 +3448,6 @@ declare global {
3810
3448
  click: RaktaClickAttributes;
3811
3449
  }
3812
3450
  }
3813
-
3814
- const useCallback: typeof import("react").useCallback;
3815
- const useEffect: typeof import("react").useEffect;
3816
- const useRef: typeof import("react").useRef;
3817
- const useState: typeof import("react").useState;
3818
-
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;
3824
3451
  }
3825
3452
 
3826
3453
  declare module "react/jsx-runtime" {
@@ -3864,7 +3491,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
3864
3491
  `;
3865
3492
  }
3866
3493
  function generateFrontendOnlyPage(_projectName) {
3867
- return WELCOME_PAGE_CODE;
3494
+ return STARTER_PAGE_CODE;
3868
3495
  }
3869
3496
  function generateFrontendOnlyLoading() {
3870
3497
  return `export default function Loading() {
@@ -3934,8 +3561,27 @@ function generateFrontendOnlyNotFound() {
3934
3561
  }
3935
3562
  `;
3936
3563
  }
3937
- function generateFullstackHomePage(_projectName) {
3938
- return WELCOME_PAGE_CODE;
3564
+ function generateFullstackHomePage(projectName) {
3565
+ return `import React from "react";
3566
+
3567
+ export default function HomePage() {
3568
+ return (
3569
+ <main className="page-shell">
3570
+ <section className="hero-card">
3571
+ <p className="eyebrow">THE RED ROUTER FRAMEWORK</p>
3572
+ <h1>Welcome to ${projectName}</h1>
3573
+ <p>
3574
+ Built with Rakta.js \xE2\u20AC\u201D Small in size. Fierce in speed. Alive in every route.
3575
+ </p>
3576
+ <div className="button-row">
3577
+ <a href="/about">About</a>
3578
+ <a href="/blog">Blog</a>
3579
+ </div>
3580
+ </section>
3581
+ </main>
3582
+ );
3583
+ }
3584
+ `;
3939
3585
  }
3940
3586
  function getDatabaseDependencies(selectedDatabase) {
3941
3587
  switch (selectedDatabase) {
@@ -4136,7 +3782,7 @@ function generateProjectReadme(projectConfig) {
4136
3782
  if (projectMode === "frontend-only") {
4137
3783
  return `# ${projectName}
4138
3784
 
4139
- Built with Rakta.js \u2014 Small in size. Fierce in speed. Alive in every route.
3785
+ Built with Rakta.js \xE2\u20AC\u201D Small in size. Fierce in speed. Alive in every route.
4140
3786
 
4141
3787
  ## Stack
4142
3788
 
@@ -4155,12 +3801,12 @@ bun run dev
4155
3801
 
4156
3802
  ## ShrimpRun
4157
3803
 
4158
- Your starter includes ShrimpRun \u2014 an interactive game where a shrimp dodges obstacles. Press Space or click to jump!
3804
+ Your starter includes ShrimpRun \xE2\u20AC\u201D an interactive game where a shrimp dodges obstacles. Press Space or click to jump!
4159
3805
  `;
4160
3806
  }
4161
3807
  return `# ${projectName}
4162
3808
 
4163
- Built with Rakta.js \u2014 Small in size. Fierce in speed. Alive in every route.
3809
+ Built with Rakta.js \xE2\u20AC\u201D Small in size. Fierce in speed. Alive in every route.
4164
3810
 
4165
3811
  ## Stack
4166
3812
 
@@ -4249,13 +3895,6 @@ async function promptProjectMode() {
4249
3895
  });
4250
3896
  return getPromptValue(promptResult);
4251
3897
  }
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
- }
4259
3898
  async function promptCssFramework() {
4260
3899
  const promptResult = await select({
4261
3900
  message: "Choose a CSS framework:",
@@ -4282,6 +3921,24 @@ async function promptCssFramework() {
4282
3921
  });
4283
3922
  return getPromptValue(promptResult);
4284
3923
  }
3924
+ async function promptProjectLanguage() {
3925
+ const promptResult = await select({
3926
+ message: "Choose a language:",
3927
+ options: [
3928
+ {
3929
+ value: "typescript",
3930
+ label: PROJECT_LANGUAGE_DISPLAY.typescript,
3931
+ hint: "recommended"
3932
+ },
3933
+ {
3934
+ value: "javascript",
3935
+ label: PROJECT_LANGUAGE_DISPLAY.javascript
3936
+ }
3937
+ ],
3938
+ initialValue: "typescript"
3939
+ });
3940
+ return getPromptValue(promptResult);
3941
+ }
4285
3942
  async function promptRenderMode() {
4286
3943
  const promptResult = await select({
4287
3944
  message: "Choose a render mode:",
@@ -4394,13 +4051,15 @@ async function promptDatabase() {
4394
4051
  }
4395
4052
  async function runPrompts(projectName) {
4396
4053
  const projectMode = await promptProjectMode();
4397
- const useTypeScript = await promptTypeScript();
4054
+ const language = await promptProjectLanguage();
4398
4055
  const cssFramework = await promptCssFramework();
4399
4056
  const renderMode = await promptRenderMode();
4057
+ const useTypeScript = language === "typescript";
4400
4058
  if (projectMode === "frontend-only") {
4401
4059
  return {
4402
4060
  projectName,
4403
4061
  projectMode,
4062
+ language,
4404
4063
  useTypeScript,
4405
4064
  cssFramework,
4406
4065
  renderMode,
@@ -4413,6 +4072,7 @@ async function runPrompts(projectName) {
4413
4072
  return {
4414
4073
  projectName,
4415
4074
  projectMode,
4075
+ language,
4416
4076
  useTypeScript,
4417
4077
  cssFramework,
4418
4078
  renderMode,
@@ -4449,6 +4109,7 @@ function formatFrontendOnlyCommands(projectName) {
4449
4109
  }
4450
4110
  function printSuccessMessage(projectConfig) {
4451
4111
  const modeLabel = PROJECT_MODE_DISPLAY[projectConfig.projectMode];
4112
+ const languageLabel = PROJECT_LANGUAGE_DISPLAY[projectConfig.language];
4452
4113
  const cssLabel = CSS_DISPLAY[projectConfig.cssFramework];
4453
4114
  const renderLabel = RENDER_MODE_DISPLAY[projectConfig.renderMode];
4454
4115
  const isFullstack = projectConfig.projectMode === "fullstack";
@@ -4459,6 +4120,7 @@ function printSuccessMessage(projectConfig) {
4459
4120
  ${import_picocolors.default.bold(import_picocolors.default.green("Project created!"))}
4460
4121
 
4461
4122
  ${import_picocolors.default.dim("Mode:")} ${modeLabel}
4123
+ ${import_picocolors.default.dim("Lang:")} ${languageLabel}
4462
4124
  ${import_picocolors.default.dim("CSS:")} ${cssLabel}
4463
4125
  ${import_picocolors.default.dim("Render:")} ${renderLabel}
4464
4126
  ${backendLine}
@@ -4506,4 +4168,4 @@ Error: ${errorMessage}
4506
4168
  process.exit(1);
4507
4169
  });
4508
4170
 
4509
- //# debugId=2E17890E5DBF1B0764756E2164756E21
4171
+ //# debugId=AC42DEC47A292C4F64756E2164756E21